说到缓存,都会想到内存缓存 LruCache 和磁盘缓存 DiskLruCache,两者都是基于 LRU(Lest Resently Used)算法并使用 LinkedHashMap 实现的,不同的是前者是保存在内存中,后者是保存在磁盘文件中。Glide也不例外照样使用了这两种缓存,本文不讲 LruCache 和 DiskLruCache 具体的实现原理,从写入和读取缓存的角度解析 Glide 的缓存策略。Glide 默认是使用内存缓存,如果要使用磁盘缓存或者屏蔽内存缓存可以如下设置:
RequestOptions options = new RequestOptions()
.skipMemoryCache(true) //屏蔽内存缓存
.diskCacheStrategy(DiskCacheStrategy.ALL); //使用磁盘缓存
Glide.with(fragment)
.load(url)
.apply(options)
.into(imageView);
DiskCacheStrategy 的4个抽象方法:
public abstract boolean isDataCacheable(DataSource dataSource);
public abstract boolean isResourceCacheable(boolean isFromAlternateCacheKey,
DataSource dataSource, EncodeStrategy encodeStrategy);
public abstract boolean decodeCachedResource();
public abstract boolean decodeCachedData();
- isDataCacheable()
返回true,表示缓存原始的没有进行修改过的图片; - isResourceCacheable()
返回true,表示缓存最终转化的图片; - decodeCacheResource()
返回ture,表示解码缓存的最终转化的图片; - decodeCacheData()
返回true,表示解码缓存的没有进行修改过的图片;
DiskCacheStrategy 有5种缓存类型:
- ALL
远程图片资源使用 DATA 和 RESOURCE 缓存,本地图片只使用 RESOURCE 缓存; - NONE
不缓存任何图片资源; - DATA
将检索到的原始图片资源(解码之前的)写入磁盘缓存; - RESOURCE
将检索到的原始图片资源解码之后(压缩或转换)再写入磁盘缓存; - AUTOMATIC
根据图片源自动选取缓存策略,数据源可以是:DataFetcher、EncodeStrategy、ResourceEncoder;
1. 内存缓存
Glide 的内存缓存中有两级,一部分是弱引用缓存,一部分是 LruCache,弱引用缓存使用 WeakReference 修饰引用的图片,用于缓存正在使用中的图片;LruCache 就是常见的内存缓存,保存当前应用使用过但不是正在使用中的图片。
1.1 缓存读取
前面分析图片请求流程时,在 Egine.load() 方法内忽略了缓存部分,回到里面:
EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations,
resourceClass, transcodeClass, options);
EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);
if (active != null) {
cb.onResourceReady(active, DataSource.MEMORY_CACHE);
if (VERBOSE_IS_LOGGABLE) {
logWithTimeAndKey("Loaded resource from active resources", startTime, key);
}
return null;
}
EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);
if (cached != null) {
cb.onResourceReady(cached, DataSource.MEMORY_CACHE);
if (VERBOSE_IS_LOGGABLE) {
logWithTimeAndKey("Loaded resource from cache", startTime, key);
}
return null;
}
图片进行网络请求前首先会调用 loadFromActiveResources() 获取应用正在使用的资源,资源是保存在一个弱引用(WeakReference)的 HashMap 里:
如果获取到的资源为空,接着调用 loadFromCache() 从内存缓存 LruCache 中获取并移除,然后保存在前面正在使用的弱引用的 HashMap 中,LruCache 的原理是使用一个 LinkedHashMap 对请求过的图片进行保存。
两种情况下获取到的资源不为空时都会调用 SingleRequest.onResourceReady() 通知主线程更新UI。
首先来看 loadFromActiveResources(),这里是从正在使用的图片资源中获取,如果我们请求的图片之前请求过并且正在使用中,那么这个方法就可以拿到这个图片资源:
private EngineResource<?> loadFromActiveResources(Key key, boolean isMemoryCacheable) {
if (!isMemoryCacheable) {
return null;
}
EngineResource<?> active = activeResources.get(key);
if (active != null) {
active.acquire();
}
return active;
}
第一步先判断 isMemeoryCacheable,由我们设置 RequestOptions 的 skipMemoryCache(true) 决定,为 true 表示不使用内存缓存 isMemeoryCacheable 就为 false,直接返回 null;为 false 表示使用内存缓存,Glide 默认是使用内存缓存的,此时 isMemeoryCacheable 为 true,接着调用 ActiveResources.get() 方法通过 key 获取图片资源:
final Map<Key, ResourceWeakReference> activeEngineResources = new HashMap<>();
EngineResource<?> get(Key key) {
ResourceWeakReference activeRef = activeEngineResources.get(key);
if (activeRef == null) {
return null;
}
EngineResource<?> active = activeRef.get();
if (active == null) {
cleanupActiveReference(activeRef);
}
return active;
}
拿到图片的 key 从 activeEngineResources 中获取一个弱引用的对象,activeEngineResources 是一个 HashMap,保存的是我们正在使用的图片资源,由于是弱引用的对象,只要系统发起 GC 操作,这些对象都会被回收掉。这就是弱引用缓存,一方面提高了请求效率,另一方面也避免了应用由于图片资源过大导致的内存溢出问题。
如果弱引用缓存中没有我们想要请求的图片,接着就调用 loadFromCache() 去内存缓存 LruCache 中查找,也就是说需要请求的这个图片是否之前使用过但现在没有使用了,Glide 便将其保存在 LruCache 中:
private EngineResource<?> loadFromCache(Key key, boolean isMemoryCacheable) {
if (!isMemoryCacheable) {
return null;
}
EngineResource<?> cached = getEngineResourceFromCache(key);
if (cached != null) {
cached.acquire();
activeResources.activate(key, cached); //缓存正在使用的图片
}
return cached;
}
private EngineResource<?> getEngineResourceFromCache(Key key) {
Resource<?> cached = cache.remove(key); //移除并返回 LreCache 中的图片资源
final EngineResource<?> result;
if (cached == null) {
result = null;
} else if (cached instanceof EngineResource) {
result = (EngineResource<?>) cached;
} else {
result = new EngineResource<>(cached, true /*isMemoryCacheable*/, true /*isRecyclable*/);
}
return result;
}
重点看 cache.remove(key),cache 就是 LruCache,调用 remove() 获取缓存并移除,拿到资源后如果不为空,由于应用此刻正好需要使用这个资源,首先先将它保存在正在使用的资源中,调用 activeResources.activate(key, cached) 将这个资源保存在我们前面提到的 HashMap 中,最后再返回给 Engine.load();
由此可见,通常情况下 Glide 是有两级缓存的,弱引用缓存和内存缓存,正在使用的图片保存在弱引用的 HashMap 中,使用过但现在不使用的图片保存在 LruCache 的 LinkedHashMap 中,两者之间存在交互的,如果现在请求的图片存在于 LreCache 中,Glide 会将这张图片从 LruCache 中移除并保存在弱引用缓存 activeResources 中,如果正在使用的图片现在不使用了(图片的引用计数为0)Glide 又会将这张图片从 activeResources 中移除并存入 LruCache 中。
Glide 获取一张图片时,首先会从弱引用缓存中获取,没有则从内存缓存 LruCache 中获取,如果有磁盘缓存,接着去磁盘中获取,最后才是通过网络获取。
1.2 缓存写入
前面我们了解了 Glide 在请求一个图片时缓存的获取原理,而缓存又是在什么时候写入的呢?前一章了解到,当我们完成一次图片的请求时,通过 Handler 发送消息给主线程,最终会调用到 EngineJob 的 handleResultOnMainThread() 方法,进去看看都做了什么:
void handleResultOnMainThread() {
...
engineResource = engineResourceFactory.build(resource, isCacheable);
hasResource = true;
engineResource.acquire(); //关注点1
listener.onEngineJobComplete(this, key, engineResource); //关注点2
for (int i = 0, size = cbs.size(); i < size; i++) {
ResourceCallback cb = cbs.get(i);
if (!isInIgnoredCallbacks(cb)) {
engineResource.acquire();
cb.onResourceReady(engineResource, dataSource);
}
}
// Our request is complete, so we can release the resource.
engineResource.release(); //关注点3
release(false /*isRemovedFromQueue*/);
}
先来看关注点2,这里又回到了 Engine 的 onEngineJobComplete() 方法中:
public void onEngineJobComplete(EngineJob<?> engineJob, Key key, EngineResource<?> resource) {
Util.assertMainThread();
if (resource != null) {
resource.setResourceListener(key, this);
if (resource.isCacheable()) {
activeResources.activate(key, resource);
}
}
jobs.removeIfCurrent(key, engineJob);
}
void activate(Key key, EngineResource<?> resource) {
ResourceWeakReference toPut =
new ResourceWeakReference(
key,
resource,
getReferenceQueue(),
isActiveResourceRetentionAllowed);
ResourceWeakReference removed = activeEngineResources.put(key, toPut); //传入弱引用缓存
if (removed != null) {
removed.reset();
}
}
很明显,如果 resource 不为空调用 activeResources.activate(),这个方法就是将我们这里的 resource 存入了弱引用缓存中。
那么内存缓存又是什么时候写入的呢?
回到 handleResultOnMainThread() 里面,关注点1和关注点3分别调用了 engineResource 的 acquire() 和 release() 方法,acquire() 每调用一次引用计数 acquired 加1,release() 方法每调用一次 acquired 减1:
void acquire() {
...
++acquired;
}
void release() {
...
if (--acquired == 0) {
listener.onResourceReleased(key, this);
}
}
引用计数 acquired 表示当前正在使用资源的使用者数,大于0表示资源正在使用中,值为0表示没有使用者使用此刻就需要将它写入内存缓存中,release() 中调用 onResourceReleased() 将没有使用的资源写入内存缓存,仍然又回到了 Engine 中的 onResourceReleased() 方法:
private final MemoryCache cache;
public void onResourceReleased(Key cacheKey, EngineResource<?> resource) {
Util.assertMainThread();
activeResources.deactivate(cacheKey); //先从弱引用缓存中移除
if (resource.isCacheable()) {
cache.put(cacheKey, resource); //写入内存缓存
} else {
resourceRecycler.recycle(resource);
}
}
由于这个图片当前已没有使用者,调用 activeResources.deactivate() 先把它从弱引用缓存中清除,然后就是将数据写入内存缓存,cache 是 MemoryCache 类型的,MemoryCache 是一个接口类它的实现者就是 LruCache。
由此可见,正在使用的图片使用 activeResources 以弱引用的方式保存起来,Glide 给图片设置了一个引用计数变量 acquired 用于统计图片当前的引用数,acquired 为0即为图片没有使用者,就将图片从弱引用缓存中移除然后保存到 LruCache 中。
2. 磁盘缓存
2.1 磁盘缓存读取
回到之前讲解请求图片流程,DecodeJob 的 run() 方法调用了 runWrapped(),之前只考虑了第一次请求图片的情况,如果是第二次请求图片,进入 runWrapper() 看看是怎么处理的:
private void runWrapped() {
switch (runReason) {
case INITIALIZE:
stage = getNextStage(Stage.INITIALIZE); //关注点1
currentGenerator = getNextGenerator(); //关注点2
runGenerators(); //关注点3
break;
case SWITCH_TO_SOURCE_SERVICE:
runGenerators();
break;
case DECODE_DATA:
decodeFromRetrievedData();
break;
default:
throw new IllegalStateException("Unrecognized run reason: " + runReason);
}
}
还是 INITIALIZE,首先来看关注点1,调用 getNextStage 获取下一步的操作标记:
private Stage getNextStage(Stage current) {
switch (current) {
case INITIALIZE:
return diskCacheStrategy.decodeCachedResource()
? Stage.RESOURCE_CACHE : getNextStage(Stage.RESOURCE_CACHE);
case RESOURCE_CACHE: //磁盘缓存的修改后的图片
return diskCacheStrategy.decodeCachedData()
? Stage.DATA_CACHE : getNextStage(Stage.DATA_CACHE);
case DATA_CACHE: //磁盘缓存的原始图片
return onlyRetrieveFromCache ? Stage.FINISHED : Stage.SOURCE;
case SOURCE:
case FINISHED:
return Stage.FINISHED;
default:
throw new IllegalArgumentException("Unrecognized stage: " + current);
}
}
当前阶段为:INITIALIZE,首先判断是否需要解码磁盘缓存中经过压缩或者转换的图片,需要则返回 RESOURCE_CACHE,否则判断是否要解码磁盘缓存中原始的图片,即未经过压缩和转换的图片,需要则返回 DATA_CACHE,如果两种都不需要则返回 SOURCE 表示直接请求原始图片,这里具体是使用哪种缓存取决于我们在设置 RequestOptions 的diskCacheStrategy(DiskCacheStrategy.ALL) 时决定;
接着来看关注点2,假设我们通过 getNextStage() 拿到的 stage 为 RESOURCE_CACHE,那么进入 getNextGenerator() 中就会返回一个 ResourceCacheGenerator 对象:
private DataFetcherGenerator getNextGenerator() {
switch (stage) {
case RESOURCE_CACHE:
return new ResourceCacheGenerator(decodeHelper, this); //磁盘缓存中获取修改后的图片
case DATA_CACHE:
return new DataCacheGenerator(decodeHelper, this); //磁盘缓存中获取原始图片
case SOURCE:
return new SourceGenerator(decodeHelper, this); //直接请求图片
case FINISHED:
return null;
default:
throw new IllegalStateException("Unrecognized stage: " + stage);
}
}
ResourceCacheGenerator 对象有什么作用呢?继续来看关注点3,调用 runGenerators(),紧接着调用 currentGenerator.startNext(),这里就是执行这个 ResourceCacheGenerator 的地方,进入 ResourceCacheGenerator 的 startNext():
public boolean startNext() {
...
currentKey =
new ResourceCacheKey
helper.getArrayPool(),
sourceId,
helper.getSignature(),
helper.getWidth(),
helper.getHeight(),
transformation,
resourceClass,
helper.getOptions()); //创建图片资源的key
cacheFile = helper.getDiskCache().get(currentKey); //获取磁盘缓存
... //没有磁盘缓存,
return started;
}
首先为当前图片创建一个用于标识图片唯一性的 key,接着很明显了,调用 helper.getDiskCache().get(currentKey) 就是从我们的磁盘缓存中获取图片资源,由于这里是 ResourceCacheGenerator 的 startNext(),所以获取到的资源是经过压缩或者转换后的图片。
要获取未经过压缩及转换的图片的话如何获取呢?首先需要我们在设置 RequestOptions 的 diskCacheStrategy() 时设置的 DiskCacheStrategy 类型是可以缓存原始图片的(ALL、DATA、AUTOMATIC 都可以缓存原始图片),接着 Glide 内部通过调用 DataCacheGenerator 的 startNext() 方法就能获取到原始的图片。
总结下来,当原始图片缓存和修改后的图片缓存都存在时,首先会获取经过压缩或转换后的图片,然后才去获取原始图片,毕竟原始图片一般比经过处理后的图片大且占据更多内存空间,在使用的时候应避免缓存以及从缓存中获取原始的图片。
2.2 磁盘缓存写入
-
缓存原始图片
磁盘缓存的写入是在请求图片的时候写入的,在磁盘获取的时候 getNextGenerator() 如果返回的是 SourceGenerator 时,表明需要去请求图片,进入 SourceGenerator 的 startNext() 方法:
public boolean startNext() {
if (dataToCache != null) {
Object data = dataToCache;
dataToCache = null;
cacheData(data); //关键点
}
...
}
在图片请求之前调用了 cacheData() 方法:
private void cacheData(Object dataToCache) {
long startTime = LogTime.getLogTime();
try {
Encoder<Object> encoder = helper.getSourceEncoder(dataToCache);
DataCacheWriter<Object> writer =
new DataCacheWriter<>(encoder, dataToCache, helper.getOptions());
originalKey = new DataCacheKey(loadData.sourceKey, helper.getSignature());
helper.getDiskCache().put(originalKey, writer); // 写入磁盘缓存
...
} finally {
loadData.fetcher.cleanup();
}
sourceCacheGenerator =
new DataCacheGenerator(Collections.singletonList(loadData.sourceKey), helper, this);
}
调用 helper.getDiskCache().put(originalKey, writer) 将数据写入了磁盘缓存,但这个方法是在 dataToCache 不为空时调用的,dataToCache 是在哪里存值的呢?还是 startNext() 方法里,接着调用loadData.fetcher.loadData(),由于我们使用的是从网络获取图片,所以fetcher 就是 HttpUrlFetcher,也就是调用了 HttpUrlFetcher 的 loadData() :
public void loadData(@NonNull Priority priority,
@NonNull DataCallback<? super InputStream> callback) {
long startTime = LogTime.getLogTime();
try {
InputStream result = loadDataWithRedirects(glideUrl.toURL(), 0, null, glideUrl.getHeaders()); //进行网络请求
callback.onDataReady(result); //调用 SourceGenerator的onDataReady()方法
}
...
}
从网络请求图片资源,请求完成后将请求结果传给了 SourceGenerator 的 onDataReday():
public void onDataReady(Object data) {
DiskCacheStrategy diskCacheStrategy = helper.getDiskCacheStrategy();
if (data != null && diskCacheStrategy.isDataCacheable(loadData.fetcher.getDataSource())) {
dataToCache = data; //关注点1
cb.reschedule();
} else {
cb.onDataFetcherReady(loadData.sourceKey, data, loadData.fetcher,
loadData.fetcher.getDataSource(), originalKey); //关注点2
}
}
看关注点1,如果请求返回的数据 data 不为空且需要缓存原始数据,就将 data 赋值给我们刚才提到的 dataToCache,接着调用 cb.reschedule() 会再一次进入到 SourceGenerator 的 startNext() 方法,这个时候 dataToCache 已经不为空就可以写入磁盘缓存了,注意这里是缓存的原始未经过任何修改的图片,如果不需要缓存原始数据,直接调用 DecodeJob.onDataFetcherReady()。
-
缓存编码(压缩或转换)后的图片
什么时候缓存压缩或转换后的图片呢?cacheData() 方法里面原始图片写入磁盘缓存完成后新建了一个 DataCacheGenerator,然后有如下流程:DataCacheGenerator.startNext() -> HttpUrlFetcher.loadData() -> DataCacheGenerator.onDataReady() -> DecodeJob.onDataFetcherReady() -> decodeFromRetrievedData() -> notifyEncodeAndRelease() -> DeferredEncodeManager.encode(),进入 encode() 方法看看:
void encode(DiskCacheProvider diskCacheProvider, Options options) {
GlideTrace.beginSection("DecodeJob.encode");
try {
diskCacheProvider.getDiskCache().put(key,
new DataCacheWriter<>(encoder, toEncode, options)); //写入磁盘缓存
} finally {
toEncode.unlock();
GlideTrace.endSection();
}
}
代码写的很清楚了,这里就把编码后的数据写入了磁盘缓存中。
3. 总结
到此为此就介绍完了 Glide 的两种缓存的读取和写入原理,需要注意的是内存缓存不仅是 LruCache 还提供了一种弱引用缓存,用于缓存正在使用的图片资源,由于是弱引用的缓存当系统发起 GC 时就会被回收掉,有效避免了内存较低时系统开启 GC 回收部分内存时可能发生的内存泄漏问题,以及由于图片过大导致内存不足时可能引发的内存溢出问题。
假设我们在一开始设置了 Glide 支持磁盘缓存,且原图和编码后(即压缩或转换)的图片都要缓存;
-
缓存读取顺序:
弱引用缓存 -> LruCache -> DiskLruCache
-
缓存写入顺序:
DiskLruCache 缓存原图 -> 弱引用缓存 -> LruCache -> DiskLruCache 缓存编码后的图片
-
注意:
弱引用缓存和 LruCache 之间存在缓存的转换关系,图片从正在使用状态转为不使用状态,Glide 将图片从弱引用缓存移除然后缓存到 LruCache 中,假如 LruCache 中的某张图片现在需要使用,则图片从 LruCache 中移除缓存到弱引用缓存中,弱引用缓存中保存的是正在使用的图片。
网友评论