上篇我们以加载一张网络图片为例,讲解了Glide加载一张图片的整体流程。为了更连贯的理解流程我们略过了一些细节,包括缓存功能,本篇我们来讲解Glide的二级缓存机制。
缓存流程是穿插在Glide整体加载流程中的,所以建议读这篇为文章之前先了解上篇文章《Glide 系列(三) Glide源码整体流程梳理》。
由于Glide的代码复杂,我们先回顾下Glide缓存相关的用法,和整体上回顾下Glide的加载流程的主要类。
Glide缓存功能相关用法
设置内存缓存开关:
skipMemoryCache(true)
设置磁盘缓存模式:
diskCacheStrategy(DiskCacheStrategy.NONE)
可以设置4种模式:
- DiskCacheStrategy.NONE:表示不缓存任何内容。
- DiskCacheStrategy.SOURCE:表示只缓存原始图片。
- DiskCacheStrategy.RESULT:表示只缓存转换过后的图片(默认选项)。
- DiskCacheStrategy.ALL :表示既缓存原始图片,也缓存转换过后的图片。
Glide加载图片主要类
这里只简单介绍主干流程,方便介绍缓存功能是定位源码,详细流程请移步上篇文章。
首先我们想下一个图片框架,应该包含哪几个模块:
- 对外接口:封装该框架的功能接口,一般为单例模式。
- 获取图片Request:为每个图片加载创建一个Request,用来准备数据、请求图片资源。
- 异步处理:不管从网络还是从本地读取图片都是耗时操作,需要在子线程中完成。
- 网络连接 :网络获取图片的必备模块。
- 解码 :获得图片流后要解码得到图片对象。
上面几部分是图片框架所必备的模块,Glide也不例外,接下来看看Glide各模块对应的主要类:
- 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缓存机制的理解。