Glide 系列(四) Glide缓存机制

上篇我们以加载一张网络图片为例,讲解了Glide加载一张图片的整体流程。为了更连贯的理解流程我们略过了一些细节,包括缓存功能,本篇我们来讲解Glide的二级缓存机制。
缓存流程是穿插在Glide整体加载流程中的,所以建议读这篇为文章之前先了解上篇文章《Glide 系列(三) Glide源码整体流程梳理》。
由于Glide的代码复杂,我们先回顾下Glide缓存相关的用法,和整体上回顾下Glide的加载流程的主要类。

Glide缓存功能相关用法

设置内存缓存开关:

skipMemoryCache(true)

设置磁盘缓存模式:

diskCacheStrategy(DiskCacheStrategy.NONE)

可以设置4种模式:

  • DiskCacheStrategy.NONE:表示不缓存任何内容。
  • DiskCacheStrategy.SOURCE:表示只缓存原始图片。
  • DiskCacheStrategy.RESULT:表示只缓存转换过后的图片(默认选项)。
  • DiskCacheStrategy.ALL :表示既缓存原始图片,也缓存转换过后的图片。

Glide加载图片主要类

这里只简单介绍主干流程,方便介绍缓存功能是定位源码,详细流程请移步上篇文章。
首先我们想下一个图片框架,应该包含哪几个模块:

功能模块.png
  • 对外接口:封装该框架的功能接口,一般为单例模式。
  • 获取图片Request:为每个图片加载创建一个Request,用来准备数据、请求图片资源。
  • 异步处理:不管从网络还是从本地读取图片都是耗时操作,需要在子线程中完成。
  • 网络连接 :网络获取图片的必备模块。
  • 解码 :获得图片流后要解码得到图片对象。

上面几部分是图片框架所必备的模块,Glide也不例外,接下来看看Glide各模块对应的主要类:

Glide功能模块主要类.png
  • Glide:Glide除了是接口封装类,还负责创建全局使用的工具和组件。
  • GenericRequest:为每个图片加载创建一个Request,初始化这张图片的转码器、图片变换器、图片展示器target等,当然这个过程实在GenericRequestBuilder的实现类里完成的。
  • Engine:异步处理总调度器。EnginJob负责线程管理,EngineRunnable是一个异步处理线程。DecodeJob是真正线程里获取和处理图片的地方。
  • HttpUrlFetcher :获取网络流,使用的是HttpURLConnection。
  • Decoder :读取网络流后,解码得到Bitmap或者gifResource。因为加载图片类型不同,这快分支较多,学习Glide初级阶段,这块可以先不再详细分析。
    接下来我们进入主题,缓存的代码在上面流程图里的什么位置?内存缓存的操作应该是在异步处理之前,磁盘缓存是耗时操作应该是在异步处理中完成。

Glide内存缓存源码分析

内存存缓存的 读存都在Engine类中完成。

Glide内存缓存的特点

内存缓存使用弱引用和LruCache结合完成的,弱引用来缓存的是正在使用中的图片。图片封装类Resources内部有个计数器判断是该图片否正在使用。

Glide内存缓存的流程
  • 读:是先从lruCache取,取不到再从弱引用中取;
  • 存:内存缓存取不到,从网络拉取回来先放在弱引用里,渲染图片,图片对象Resources使用计数加一;
  • 渲染完图片,图片对象Resources使用计数减一,如果计数为0,图片缓存从弱引用中删除,放入lruCache缓存。

具体看源码:
上篇提到,Engine在加载流程的中的入口方法是load方法:

public class Engine implements EngineJobListener,
        MemoryCache.ResourceRemovedListener,
        EngineResource.ResourceListener {
    ...    

    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();
        //生成缓存的key
        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;
        }
        //从弱引用获取图片
        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;
        }

        EngineJob current = jobs.get(key);
        if (current != null) {
            current.addCallback(cb);
            if (Log.isLoggable(TAG, Log.VERBOSE)) {
                logWithTimeAndKey("Added to existing load", startTime, key);
            }
            return new LoadStatus(cb, current);
        }

        EngineJob engineJob = engineJobFactory.build(key, isMemoryCacheable);
        DecodeJob<T, Z, R> decodeJob = new DecodeJob<T, Z, R>(key, width, height, fetcher, loadProvider, transformation,
                transcoder, diskCacheProvider, diskCacheStrategy, priority);
        EngineRunnable runnable = new EngineRunnable(engineJob, decodeJob, priority);
        jobs.put(key, engineJob);
        engineJob.addCallback(cb);
        engineJob.start(runnable);

        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            logWithTimeAndKey("Started new load", startTime, key);
        }
        return new LoadStatus(cb, engineJob);
    }

    ...
}

上面是从内存缓存中读取图片的主流程:

  • 生成缓存的key。
  • 从LruCache获取缓存图片。
  • LruCache没取到,从弱引用获取图片。
  • 内存缓存取不到,进入异步处理。

我们具体看取图片的两个方法loadFromCache()和loadFromActiveResources()。loadFromCache使用的就是LruCache算法,loadFromActiveResources使用的就是弱引用。我们来看一下它们的源码:

public class Engine implements EngineJobListener,
        MemoryCache.ResourceRemovedListener,
        EngineResource.ResourceListener {

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

    private EngineResource<?> loadFromCache(Key key, boolean isMemoryCacheable) {
        if (!isMemoryCacheable) {
            return null;
        }
        EngineResource<?> cached = getEngineResourceFromCache(key);
        if (cached != null) {
            cached.acquire();
            activeResources.put(key, new ResourceWeakReference(key, cached, getReferenceQueue()));
        }
        return cached;
    }

    private EngineResource<?> getEngineResourceFromCache(Key key) {
        Resource<?> cached = cache.remove(key);
        final EngineResource result;
        if (cached == null) {
            result = null;
        } else if (cached instanceof EngineResource) {
            result = (EngineResource) cached;
        } else {
            result = new EngineResource(cached, true /*isCacheable*/);
        }
        return result;
    }

    private EngineResource<?> loadFromActiveResources(Key key, boolean isMemoryCacheable) {
        if (!isMemoryCacheable) {
            return null;
        }
        EngineResource<?> active = null;
        WeakReference<EngineResource<?>> activeRef = activeResources.get(key);
        if (activeRef != null) {
            active = activeRef.get();
            if (active != null) {
                active.acquire();
            } else {
                activeResources.remove(key);
            }
        }
        return active;
    }

    ...
}

loadFromCache()方法:

  • 首先就判断isMemoryCacheable是不是false,如果是false的话就直接返回null。这就是skipMemoryCache()方法设置的是否内存缓存已被禁用。
  • 然后调用getEngineResourceFromCache()方法来获取缓存。在这个方法中,会从中获取图片缓存LruResourceCache,LruResourceCache其实使用的就是LruCache算法实现的缓存。
  • 当我们从LruResourceCache中获取到缓存图片之后会将它从缓存中移除,将缓存图片存储到activeResources当中。activeResources就是弱引用的HashMap,用来缓存正在使用中的图片。

loadFromActiveResources()方法:

  • 就是从activeResources这个activeResources当中取值的。使用activeResources来缓存正在使用中的图片,用来保护正在使用中的图片不会被LruCache算法回收掉。

这样我们把从内存读取图片缓存的流程搞清了,那是什么时候存储的呢。想想什么时候合适?是不是应该在异步处理获取到图片后,再缓存到内存?
参考上一篇,EngineJob 获取到图片后 会回调Engine的onEngineJobComplete()。我们来看下做了什么:

public class Engine implements EngineJobListener,
        MemoryCache.ResourceRemovedListener,
        EngineResource.ResourceListener {
    ...    

    @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);
    }

    ...
}

在onEngineJobComplete()方法里将正在加载的图片放到弱引用缓存。那什么时候放在LruCache里呢?当然是在使用完,那什么时候使用完呢?

那我们来看EngineResource这个类是怎么标记自己是否在被使用的。EngineResource是用一个acquired变量用来记录图片被引用的次数,调用acquire()方法会让变量加1,调用release()方法会让变量减1,代码如下所示:

class EngineResource<Z> implements Resource<Z> {

    private int acquired;
    ...

    void acquire() {
        if (isRecycled) {
            throw new IllegalStateException("Cannot acquire a recycled resource");
        }
        if (!Looper.getMainLooper().equals(Looper.myLooper())) {
            throw new IllegalThreadStateException("Must call acquire on the main thread");
        }
        ++acquired;
    }

    void release() {
        if (acquired <= 0) {
            throw new IllegalStateException("Cannot release a recycled or not yet acquired resource");
        }
        if (!Looper.getMainLooper().equals(Looper.myLooper())) {
            throw new IllegalThreadStateException("Must call release on the main thread");
        }
        if (--acquired == 0) {
            listener.onResourceReleased(key, this);
        }
    }
}

可以看出当引用计数acquired变量为0,就是没有在使用了,然后调用了 listener.onResourceReleased(key, this);
这个listener就是Engine对象,我们来看下它的onResourceReleased()方法:

public class Engine implements EngineJobListener,
        MemoryCache.ResourceRemovedListener,
        EngineResource.ResourceListener {

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

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

    ...
}

做了三件事:

  • 从弱引用删除图片缓存
  • 是否支持缓存,缓存到LruCache缓存
  • 不支持缓存直接调用垃圾回收,回收图片

到这里内存缓存的读和存的流程就介绍完了,根据源码回头看看我们之前列的Glide内存缓存流程,就清晰很多了。

Glide磁盘缓存源码分析

Glide磁盘缓存流程

先列下主流程,再具体看代码

  • 读:先找处理后(result)的图片,没有的话再找原图。
  • 存:先存原图,再存处理后的图。

注意一点:diskCacheStrategy设置的的缓存模式即影响读取,也影响存储。

具体看源码:
源码入口位置是在EngineRunnable的run()方法,run()方法中调用到decode()方法,decode()方法的源码:

private Resource<?> decode() throws Exception {
    if (isDecodingFromCache()) {
        //从磁盘缓存读取图片
        return decodeFromCache();
    } else { 
        //从原始位置读取图片
        return decodeFromSource();
    }
}

来看一下decodeFromCache()方法的源码,如下所示:

private Resource<?> decodeFromCache() throws Exception {
    Resource<?> result = null;
    try {
        //先尝试读取处理后的缓存图
        result = decodeJob.decodeResultFromCache();
    } catch (Exception e) {
        if (Log.isLoggable(TAG, Log.DEBUG)) {
            Log.d(TAG, "Exception decoding result from cache: " + e);
        }
    }
    if (result == null) {
       //再尝试读取原图的缓存图
        result = decodeJob.decodeSourceFromCache();
    }
    return result;
}

处理后的缓存图和原图缓存图对应的是DiskCacheStrategy.RESULT和DiskCacheStrategy.SOURCE这两个缓存模式。
到DecodeJob具体看下这两个读取磁盘缓存的方法,decodeResultFromCache()和decodeSourceFromCache():

public Resource<Z> decodeResultFromCache() throws Exception {
    if (!diskCacheStrategy.cacheResult()) {
        return null;
    }
    long startTime = LogTime.getLogTime();
    Resource<T> transformed = loadFromCache(resultKey);
    startTime = LogTime.getLogTime();
    Resource<Z> result = transcode(transformed);
    return result;
}

public Resource<Z> decodeSourceFromCache() throws Exception {
    if (!diskCacheStrategy.cacheSource()) {
        return null;
    }
    long startTime = LogTime.getLogTime();
    Resource<T> decoded = loadFromCache(resultKey.getOriginalKey());
    return transformEncodeAndTranscode(decoded);
}

这里两个方法都先判断了是否是对应的缓存模式,不是则读取失败。这里我们不关注transform 和transcode的相关功能,只分析缓存功能。两个缓存方法都调用到了loadFromCache()方法,只是传入的key不同。一个是处理后图片的key,一个是原始图片的key。
继续看loadFromCache()方法的源码:

private Resource<T> loadFromCache(Key key) throws IOException {
    File cacheFile = diskCacheProvider.getDiskCache().get(key);
    if (cacheFile == null) {
        return null;
    }
    Resource<T> result = null;
    try {
        result = loadProvider.getCacheDecoder().decode(cacheFile, width, height);
    } finally {
        if (result == null) {
            diskCacheProvider.getDiskCache().delete(key);
        }
    }
    return result;
}

源码中可以看到我们是从diskCacheProvider.getDiskCache()中读取的缓存,diskCacheProvider.getDiskCache()获得的是DiskLruCache工具类的实例,然后从DiskLruCache获取缓存。之后的decode不是本篇的关注点,先不分析。
到这里我们把从磁盘缓存读取缓存的流程讲完了,那什么时候存入的呢?肯定是在从原始位置获取图片后,我们回到decodeFromSource()方法,一步步看进去:

public Resource<Z> decodeFromSource() throws Exception {
    Resource<T> decoded = decodeSource();
    return transformEncodeAndTranscode(decoded);
}

decodeSource()顾名思义是用来解析原图片的,而transformEncodeAndTranscode()则是用来对图片进行转换和转码的。我们先来看decodeSource()方法:

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;
}

decodeSource()方法中获取图片后,调用到decodeFromSourceData()方法,然后判断是否缓存原图,是的话就调用到cacheAndDecodeSourceData(A data)方法。看进去,还是调用了 diskCacheProvider.getDiskCache()获取DiskLruCache工具类的实例。然后调用put方法缓存了原图。

到此我们缓存了原图,处理后的图片是什么时候缓存的?肯定是在图片处理之后,在transformEncodeAndTranscode()方法中:

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);
}

transformEncodeAndTranscode中先对图片进行了转换,然后调用writeTransformedToCache(transformed);判断是否缓存处理后的图片,是就对处理后的图片进行了缓存。调用的同样是DiskLruCache实例的put()方法,不过这里用的缓存Key是resultKey。
至此图片磁盘缓存都讲解完了,对照源码看下之前的Glide磁盘缓存流程是不是清晰了很多。
本篇我们主要讲解了Glide的二级缓存机制,虽然代码比较长,但是基本流程比较清晰,大家通过对流程的梳理,加深对Glide缓存机制的理解。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容

  • Android缓存机制:如果没有缓存,在大量的网络请求从远程获取图片时会造成网络流量的浪费,加载速度较慢,用户体验...
    芒果味的你呀阅读 4,425评论 13 22
  • 学习来源:郭霖大师博客地址 1、图片加载框架挺多,如Volley、Glide、Picasso、Fresco、本次是...
    子谦宝宝阅读 1,739评论 0 6
  • 7.1 压缩图片 一、基础知识 1、图片的格式 jpg:最常见的图片格式。色彩还原度比较好,可以支持适当压缩后保持...
    AndroidMaster阅读 2,479评论 0 13
  • 小编做头条也有也有一段时间了,根据以往发表的文章总结下来发现,想要写出爆文,标题最关键。而想写出今日头条爆文爆文标...
    无比简单阅读 963评论 0 0
  • 今天上语文课了时候,戴老师让我坐到老师下边听讲,我听会了之后,戴老师就让我当小老师了。当小老师的时候,我觉得很开...
    小狐狸的麻麻阅读 274评论 0 2