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加载图片主要类

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

《Glide 系列(四) Glide缓存机制》 功能模块.png

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

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

《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缓存机制的理解。

    原文作者:野生的安卓兽
    原文地址: https://www.jianshu.com/p/17644406396b
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞