Glide缓存机制

前言

本文基于Glide v3.7.0源码分析,Glide v4.0大致流程和v3.7.0差不多,在一些技术细节上有修改。主要内容有:

  • 内存缓存读取
  • 内存缓存写入
  • 缓存引用计数
  • 硬盘缓存读取
  • 硬盘缓存写入

内存缓存读取

内存缓存相关代码主要在Engine.java中

public <T, Z, R> LoadStatus load(Key signature, int width, int height, DataFetcher<T> fetcher,
        DataLoadProvider<T, Z> loadProvider, Transformation<Z> transformation, ResourceTranscoder<Z, R> transcoder,
        Priority priority, boolean isMemoryCacheable, DiskCacheStrategy diskCacheStrategy, ResourceCallback cb) {
    Util.assertMainThread();
    long startTime = LogTime.getLogTime();

    final String id = fetcher.getId();
    EngineKey key = keyFactory.buildKey(id, signature, width, height, loadProvider.getCacheDecoder(),
            loadProvider.getSourceDecoder(), transformation, loadProvider.getEncoder(),
            transcoder, loadProvider.getSourceEncoder());

    // 从LruCache中寻找缓存
    EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);
    if (cached != null) {
        cb.onResourceReady(cached);
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            logWithTimeAndKey("Loaded resource from cache", startTime, key);
        }
        return null;
    }

    // LruCache中没有找到缓存,从弱引用中寻找缓存
    EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);
    if (active != null) {
        cb.onResourceReady(active);
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            logWithTimeAndKey("Loaded resource from active resources", startTime, key);
        }
        return null;
    }

    ...
}

两级内存缓存:先从LruCache中寻找,如果找到了缓存,将图片移出LruCache,加入activeResources弱引用缓存。如果在LruCache中没找到的话到activeResources弱引用缓存中寻找。如果在内存缓存中找到,则引用计数加1。使用中的图片用弱引用缓存来管理,没有使用的图片用LruCache来管理,判断图片有没有使用的依据之一是引用计数,当引用计数等于0时,将图片从弱引用缓存中移走,加入LruCache中。

内存缓存写入

图片会先写入到activeResources弱引用缓存中。

@Override
public void onEngineJobComplete(Key key, EngineResource<?> resource) {
    Util.assertMainThread();
    // A null resource indicates that the load failed, usually due to an exception.
    if (resource != null) {
        resource.setResourceListener(key, this);
        if (resource.isCacheable()) {
            activeResources.put(key, new ResourceWeakReference(key, resource, getReferenceQueue()));
        }
    }
    jobs.remove(key);
}

当引用计数为0的时候会将图片放到LruCache中。

@Override
public void onResourceReleased(Key cacheKey, EngineResource resource) {
    Util.assertMainThread();
    activeResources.remove(cacheKey);
    if (resource.isCacheable()) {
        cache.put(cacheKey, resource);
    } else {
        resourceRecycler.recycle(resource);
    }
}

缓存引用计数

这里其实就有个问题了,当我们滑动图片列表的时候,被回收的View中的图片自然也用不到了,我们并没有人为的去把计数器减1,那么Glide又是怎么知道图片已经没有被引用了,从而将它放到LruCache中的呢?其实Glide并不知道,但activeResources包含的值是图片资源的弱引用。

private final Map<Key, WeakReference<EngineResource<?>>> activeResources;

当滑动图片列表的时候,系统会根据需要将这些图片资源给回收掉,所以activeResources.get(key).get()得到的就会为null,为null也没有必要添加到LruCache中了。
但是这样子一来,activeResources中就会有很多没有用的item了,而它们又没有被移除掉。为了解决这个问题,Glide用了MessageQueue.IdleHandler这个利器来解决这个问题。
(IdleHandler也可以用来解决App启动延时加载的问题,具体可以看Android 启动优化之延时加载

private static class RefQueueIdleHandler implements MessageQueue.IdleHandler {
    private final Map<Key, WeakReference<EngineResource<?>>> activeResources;
    private final ReferenceQueue<EngineResource<?>> queue;

    public RefQueueIdleHandler(Map<Key, WeakReference<EngineResource<?>>> activeResources,
            ReferenceQueue<EngineResource<?>> queue) {
        this.activeResources = activeResources;
        this.queue = queue;
    }

    @Override
    public boolean queueIdle() {
        ResourceWeakReference ref = (ResourceWeakReference) queue.poll();
        if (ref != null) {
            activeResources.remove(ref.key);
        }

        return true;
    }
}

在Glide v4.0版本,Glide不是用IdleHandler来解决这个问题的了,而是开了一个线程优先级为Process.THREAD_PRIORITY_BACKGROUND的线程,然后再通过MainHandler发送消息到主线程中去处理的,技术实现不同,但目的是一样的。

硬盘缓存读取

当从两级内存缓存中都获取不到图片的时候,会开启线程,尝试从硬盘中获取缓存。根据硬盘缓存策略,可以只缓存转换过后的图片,也可以缓存原始图片。所以从硬盘中获取缓存的时候,会有两种方法,分别对应DiskCacheStrategy.RESULT和DiskCacheStrategy.SOURCE。相关代码在DecodeJob.java中。

private Resource<?> decodeFromCache() throws Exception {
    Resource<?> result = null;
    try {
        // DiskCacheStrategy.RESULT
        result = decodeJob.decodeResultFromCache();
    } catch (Exception e) {
        if (Log.isLoggable(TAG, Log.DEBUG)) {
            Log.d(TAG, "Exception decoding result from cache: " + e);
        }
    }
    if (result == null) {
        // DiskCacheStrategy.SOURCE
        result = decodeJob.decodeSourceFromCache();
    }
    return result;
}

硬盘缓存写入

在没有任何缓存的情况下,会解码原始图片

public Resource<Z> decodeFromSource() throws Exception {
    // 解码原始数据
    Resource<T> decoded = decodeSource();
    // 根据需要转换图片
    return transformEncodeAndTranscode(decoded);
}

缓存原始图片:

private Resource<T> decodeSource() throws Exception {
    Resource<T> decoded = null;
    try {
        long startTime = LogTime.getLogTime();
        final A data = fetcher.loadData(priority);
        if (isCancelled) {
            return null;
        }
        decoded = decodeFromSourceData(data);
    } finally {
        fetcher.cleanup();
    }
    return decoded;
}

private Resource<T> decodeFromSourceData(A data) throws IOException {
    final Resource<T> decoded;
    if (diskCacheStrategy.cacheSource()) {
        decoded = cacheAndDecodeSourceData(data);
    } else {
        long startTime = LogTime.getLogTime();
        decoded = loadProvider.getSourceDecoder().decode(data, width, height);
    }
    return decoded;
}

private Resource<T> cacheAndDecodeSourceData(A data) throws IOException {
    long startTime = LogTime.getLogTime();
    SourceWriter<A> writer = new SourceWriter<A>(loadProvider.getSourceEncoder(), data);
    diskCacheProvider.getDiskCache().put(resultKey.getOriginalKey(), writer);
    startTime = LogTime.getLogTime();
    Resource<T> result = loadFromCache(resultKey.getOriginalKey());
    return result;
}

可以看到,开启了原始图片缓存的情况下,Glide选择将原始图片先写入硬盘缓存,然后再从硬盘缓存中加载图片。如果没有开启原始图片缓存,则直接解码原始数据。

缓存结果图片:

private Resource<Z> transformEncodeAndTranscode(Resource<T> decoded) {
    long startTime = LogTime.getLogTime();
    // 先对图片进行转换
    Resource<T> transformed = transform(decoded);
    // 缓存转换后的图片
    writeTransformedToCache(transformed);
    startTime = LogTime.getLogTime();
    Resource<Z> result = transcode(transformed);
    return result;
}

private void writeTransformedToCache(Resource<T> transformed) {
    if (transformed == null || !diskCacheStrategy.cacheResult()) {
        return;
    }
    long startTime = LogTime.getLogTime();
    SourceWriter<Resource<T>> writer = new SourceWriter<Resource<T>>(loadProvider.getEncoder(), transformed);
    diskCacheProvider.getDiskCache().put(resultKey, writer);
}
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 229,732评论 6 539
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 99,214评论 3 426
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 177,781评论 0 382
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 63,588评论 1 316
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 72,315评论 6 410
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 55,699评论 1 327
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 43,698评论 3 446
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 42,882评论 0 289
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 49,441评论 1 335
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 41,189评论 3 356
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 43,388评论 1 372
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 38,933评论 5 363
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 44,613评论 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 35,023评论 0 28
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 36,310评论 1 293
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 52,112评论 3 398
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 48,334评论 2 377

推荐阅读更多精彩内容

  • 上篇我们以加载一张网络图片为例,讲解了Glide加载一张图片的整体流程。为了更连贯的理解流程我们略过了一些细节,包...
    嘎啦果安卓兽阅读 51,035评论 7 46
  • Android缓存机制:如果没有缓存,在大量的网络请求从远程获取图片时会造成网络流量的浪费,加载速度较慢,用户体验...
    芒果味的你呀阅读 4,481评论 13 22
  • 7.1 压缩图片 一、基础知识 1、图片的格式 jpg:最常见的图片格式。色彩还原度比较好,可以支持适当压缩后保持...
    AndroidMaster阅读 2,537评论 0 13
  • 深夜,大宝已熟睡,看着她圆圆的脸,卷卷的睫毛,微皱的眉头,我为白天对她的恶劣态度感到愧疚极了。 清晨,我叫她起床,...
    A米空间阅读 169评论 2 1
  • 女子本是购买力极强的性别,这分工不同的意义,我想还是留给专业的人去解释方为上策。 这里我只讲女子关于在淘宝,或者说...
    女子的智慧阅读 278评论 0 2