目前图片框架,基本就是 Glide 一统江山了,除了极其简单的链式调用,里面丰富的 API 也让人爱不释手。
那么,这样一个好用的框架,里面的缓存机制是怎么样的呢?
我们知道,一般图片框架,加载图片,都是通过内存缓存 LruCache ,DiskLruCache 硬盘缓存中去拿,那 Glide 又是怎么样的呢?这里,我们一起来探讨一下;
这里的 Glide 版本为 4.9.0
Glide 的缓存可以分为两种,一种内存缓存,一种是硬盘缓存;其中内存缓存又包含 弱引用 和 LruCache ;而硬盘缓存就是 DiskLruCache。
流程图,可以参考这个,再去跟比较好:
一. 内存缓存
首先,Glide 的图片加载在 Engine 中的 load 方法中,如下:
public synchronized <R> LoadStatus load(
// 获取资源的 key ,参数有8个
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;
}
// 通过 LruCache
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;
}
// 如果都获取不到,则网络加载
....
1.1 获取缓存key
可以看到,首先通过:
EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations,
resourceClass, transcodeClass, options);
来获取 缓存的key,它的参数有8个,所以,当同一图片的,它的大小不一样时,也会生成一个新的缓存。
然后 Glide 默认是开启内存缓存的,如果你想关掉,可以使用:
//关闭内存缓存
skipMemoryCache(false)
1.2 弱引用
接着,会从弱引用中是否拿到资源,通过:
EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);
里面的实现为:
@Nullable
private EngineResource<?> loadFromActiveResources(Key key, boolean isMemoryCacheable) {
if (!isMemoryCacheable) {
return null;
}
EngineResource<?> active = activeResources.get(key);
//如果能拿到资源,则计数器 +1
if (active != null) {
active.acquire();
}
return active;
}
#ActiveResources#get()
@Nullable
synchronized 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;
}
#EngineResource#acquire()
synchronized void acquire() {
if (isRecycled) {
throw new IllegalStateException("Cannot acquire a recycled resource");
}
++acquired;
}
可以看到,activeEngineResources 为实现了弱引用的 hasmap,通过 key 拿到弱引用的对象,如果获取不到,则可能GC,对象被回收,则从 map 中移除;如果拿到对象,则引用计数 acquired +1, 计数器后面再讲;
1.3 LruCache
如果获取不到,则通过
// 从 lrucache 获取对象
EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);
#Engine#loadFromCache()
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;
}
我们看看 getEngineResourceFromCache 方法:
发现,它从 LruCache 那大对象,如果对象不为空,则通过 activeResources.activate(key, cached); 把它加入弱引用中,且从 LruCache 删除。且 调用 acquire() 让计数器 +1.
所以,我们知道了,Glide 的内存缓存的流程是这样的,先从弱引用中取对象,如果存在,引用计数+1,如果不存在,从 LruCache 取,如果存在,则引用计数+1,并把它存到弱引用中,且自身从 LruCache 移除。
上面,我们讲到的是取,那如果存呢?如果要对一个对象进行存储,那肯定在图片加载的时候去存。
回调 Engine 类的load 方法,其中通过加载的代码如下:
public synchronized <R> LoadStatus load(
..
// 获取 key
EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations,
resourceClass, transcodeClass, options);
// 从 弱引用中获取对象
EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);
...
// 从 LruCache 获取对象
EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);
...
EngineJob<R> engineJob =
engineJobFactory.build(
key,
isMemoryCacheable,
useUnlimitedSourceExecutorPool,
useAnimationPool,
onlyRetrieveFromCache);
DecodeJob<R> decodeJob =
decodeJobFactory.build(
glideContext,
model,
key,
signature,
width,
height,
resourceClass,
transcodeClass,
priority,
diskCacheStrategy,
transformations,
isTransformationRequired,
isScaleOnlyOrNoTransform,
onlyRetrieveFromCache,
options,
engineJob);
jobs.put(key, engineJob);
engineJob.addCallback(cb, callbackExecutor);
engineJob.start(decodeJob);
...
这里两个关键的对象,一个是 EngineJob ,它是一个线程池,维护着编码、资源解析、网络下载等工作;一个是 DecodeJob ,它继承 Runnable ,相当于于 EngineJob 的一个任务;
engineJob.start(decodeJob); 可以知道,调用的是 DecodeJob 里面的 run 方法,具体细节,等硬盘缓存的时候,我们再跟;最后会回调EngineJob 的 onResourceReady 方法:
@Override
public void onResourceReady(Resource<R> resource, DataSource dataSource) {
synchronized (this) {
this.resource = resource;
this.dataSource = dataSource;
}
notifyCallbacksOfResult();
}
#EngineJob #onResourceReady ()
@Synthetic
void notifyCallbacksOfResult() {
ResourceCallbacksAndExecutors copy;
Key localKey;
EngineResource<?> localResource;
synchronized (this) {
stateVerifier.throwIfRecycled();
//是否被取消
if (isCancelled) {
// TODO: Seems like we might as well put this in the memory cache instead of just recycling
// it since we've gotten this far...
resource.recycle();
release();
return;
} else if (cbs.isEmpty()) {
throw new IllegalStateException("Received a resource without any callbacks to notify");
} else if (hasResource) {
throw new IllegalStateException("Already have resource");
}
engineResource = engineResourceFactory.build(resource, isCacheable);
// Hold on to resource for duration of our callbacks below so we don't recycle it in the
// middle of notifying if it synchronously released by one of the callbacks. Acquire it under
// a lock here so that any newly added callback that executes before the next locked section
// below can't recycle the resource before we call the callbacks.
hasResource = true;
copy = cbs.copy();
// 引用计数 +1
incrementPendingCallbacks(copy.size() + 1);
localKey = key;
localResource = engineResource;
}
// 把对象put到弱引用上
listener.onEngineJobComplete(this, localKey, localResource);
// 遍历所有图片
for (final ResourceCallbackAndExecutor entry : copy) {
// 把资源加载到 imageview 中,引用计数 +1
entry.executor.execute(new CallResourceReady(entry.cb));
}
//引用计数 -1
decrementPendingCallbacks();
}
从上面看,notifyCallbacksOfResult() 方法做了以下事情
- 图片的引用计数 +1
- 通过 listener.onEngineJobComplete() ,它的回调为 Engine#onEngineJobComplete(),把资源 put 到 弱引用上,实现如下:
- 遍历加载的图片,如果加载成功,则引用计数+1,且通过 cb.onResourceReady(engineResource, dataSource) 回调给 target (imageview) 去加载
- 通过 decrementPendingCallbacks() 释放资源,引用计数 -1
synchronized void decrementPendingCallbacks() {
....
if (decremented == 0) {
if (engineResource != null) {
// 释放资源
engineResource.release();
}
release();
}
}
#ResourceEnginer#release()
void release() {
// To avoid deadlock, always acquire the listener lock before our lock so that the locking
// scheme is consistent (Engine -> EngineResource). Violating this order leads to deadlock
// (b/123646037).
synchronized (listener) {
synchronized (this) {
if (acquired <= 0) {
throw new IllegalStateException("Cannot release a recycled or not yet acquired resource");
}
if (--acquired == 0) {
listener.onResourceReleased(key, this);
}
}
}
}
当引用计数减到 0 时,即图片已经没有使用时,就会调用 onResourceReleased() 接口,它的实现如下:
@Override
public synchronized void onResourceReleased(Key cacheKey, EngineResource<?> resource) {
// 从弱引用中移除
activeResources.deactivate(cacheKey);
if (resource.isCacheable()) {
//添加到 LruCache 中
cache.put(cacheKey, resource);
} else {
// 回收资源
resourceRecycler.recycle(resource);
}
}
这样,我们就知道了真个流程了,这里,我们再对流程梳理一下:
首先,从弱引用去缓存,如果有,则引用计数+1,没有则从 LruCache 取,如果有,则引用计数+1,且该缓存从 LruCache移除,存到 弱引用中。反过来,当该资源不再被引用时,就会从弱引用移除,存存到 LruCache 中。
而这个 引用计数是啥呢?acquired 这个变量是用来记录图片被引用的次数的,当从 loadFromActiveResources()、loadFromCache()、incrementPendingCallbacks,CallResourceReady#run 获取图片时,都会调用 acquire() 方法,让 acquired +1,当暂停请求或加载完毕,或清除资源都会调用 release() 方法,让 acquired -1
可以看到这个图:
二. 硬盘缓存
上面已经解释了 Glide 如何从内存缓存中拿到图片,但如果还是拿不到图片,则此时 Glide 会从以下两个方法来检查:
- 资源类型(Resource) - 该图片是否之前曾被解码、转换并写入过磁盘缓存?
- 数据来源 (Data) - 构建这个图片的资源是否之前曾被写入过文件缓存?
即我们的硬盘缓存;Glide 的硬盘策略可以分为如下几种:
- DiskCacheStrategy.RESOURCE :只缓存解码过的图片
- DiskCacheStrategy.DATA :只缓存原始图片
- DiskCacheStrategy.ALL : 即缓存原始图片,也缓存解码过的图片啊, 对于远程图片,缓存 DATA 和 RESOURCE;对本地使用 只缓存 RESOURCE。
- DiskCacheStrategy.NONE :不使用硬盘缓存
- DiskCacheStrategy.AUTOMATIC :默认策略,会对本地和和远程图片使用最佳的策略;对下载网络图片,使用 DATA,对于本地图片,使用 RESOURCE
**这里以下载一个远程图片为例子,且缓存策略为 DiskCacheStrategy.ALL **
从上面我们知道,一个图片的加载在 DecodeJob 这个类中,这个任务由 EngineJob 这个线程池去执行的。去到 run 方法,可以看到有个 runWrapped() 方法:
刚开始 runReason 初始化为 INITIALIZE , 所以它会走第一个 case,getNextStage 其实,就是对当前的缓存策略进行判断,由于我们的策略为DiskCacheStrategy.ALL ,所以 diskCacheStrategy.decodeCachedResource() 为true,即会解析解码的流程,所以 State 被赋值为 Stage.RESOURCE_CACHE,如下:
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:
// Skip loading from source if the user opted to only retrieve the resource from cache.
return onlyRetrieveFromCache ? Stage.FINISHED : Stage.SOURCE;
case SOURCE:
case FINISHED:
return Stage.FINISHED;
default:
throw new IllegalArgumentException("Unrecognized stage: " + current);
}
}
接着,调用 currentGenerator = getNextGenerator() 拿到当前的解码器为 ResourceCacheGenerator;然后 调用 runGenerators() 方法,它才是关键;它里面维护着一个 while 循环,即不断通过 startNext() 去解析不同的缓存策略,当 stage == Stage.SOURCE 的时候,才会退出。
接着会调用 ResourceCacheGenerator 的 startNext() 方法,它会从生成缓存key,从 DiskLruCache 拿缓存,如下:
@Override
public boolean startNext() {
List<Key> sourceIds = helper.getCacheKeys();
...
while (modelLoaders == null || !hasNextModelLoader()) {
resourceClassIndex++;
if (resourceClassIndex >= resourceClasses.size()) {
sourceIdIndex++;
// 由于没有缓存,最后会在这里退出这个循环
if (sourceIdIndex >= sourceIds.size()) {
return false;
}
resourceClassIndex = 0;
}
Key sourceId = sourceIds.get(sourceIdIndex);
//获取缓存 key
currentKey =
new ResourceCacheKey(// NOPMD AvoidInstantiatingObjectsInLoops
helper.getArrayPool(),
sourceId,
helper.getSignature(),
helper.getWidth(),
helper.getHeight(),
transformation,
resourceClass,
helper.getOptions());
// 尝试从 DiskLruCache 拿数据
cacheFile = helper.getDiskCache().get(currentKey);
if (cacheFile != null) {
sourceKey = sourceId;
modelLoaders = helper.getModelLoaders(cacheFile);
modelLoaderIndex = 0;
}
}
loadData = null;
boolean started = false;
while (!started && hasNextModelLoader()) {
ModelLoader<File, ?> modelLoader = modelLoaders.get(modelLoaderIndex++);
loadData = modelLoader.buildLoadData(cacheFile,
helper.getWidth(), helper.getHeight(), helper.getOptions());
if (loadData != null && helper.hasLoadPath(loadData.fetcher.getDataClass())) {
started = true;
loadData.fetcher.loadData(helper.getPriority(), this);
}
}
return started;
}
由于第一次肯定是拿不到缓存的,所以 while (modelLoaders == null || !hasNextModelLoader()) 循环会一直运行,直到返回 false。
if (sourceIdIndex >= sourceIds.size()) {
return false;
}
同理,接着来则是 DataCacheGenerator 也是同样,最后,当 stage == Stage.SOURCE 的时候,才会退出,并调用reschedule() 方法。
而在 reschedule() 方法中,会把 runReason 的状态改成RunReason.SWITCH_TO_SOURCE_SERVICE ,并重新回调 run 方法
@Override
public void reschedule() {
runReason = RunReason.SWITCH_TO_SOURCE_SERVICE;
callback.reschedule(this);
}
所以,它又会调用 runWrapped() 方法,但此时的 runReason 已经变成了 SWITCH_TO_SOURCE_SERVICE,所以它会执行 runGenerators() 方法
而在啊runGennerator() 方法中,它里面也是个 while 循环:
private void runGenerators() {
currentThread = Thread.currentThread();
startFetchTime = LogTime.getLogTime();
boolean isStarted = false;
while (!isCancelled && currentGenerator != null
&& !(isStarted = currentGenerator.startNext())) {
stage = getNextStage(stage);
currentGenerator = getNextGenerator();
if (stage == Stage.SOURCE) {
reschedule();
return;
}
}
...
}
但此时的 currentGenerator 为 SourceGennerator ,已经不为null,所以,去到 SourceGennerator 的 startNext() 方法:
@Override
public boolean startNext() {
if (dataToCache != null) {
Object data = dataToCache;
dataToCache = null;
// 存储到 DiskLruCache
cacheData(data);
}
if (sourceCacheGenerator != null && sourceCacheGenerator.startNext()) {
return true;
}
sourceCacheGenerator = null;
loadData = null;
boolean started = false;
while (!started && hasNextModelLoader()) {
loadData = helper.getLoadData().get(loadDataListIndex++);
if (loadData != null
&& (helper.getDiskCacheStrategy().isDataCacheable(loadData.fetcher.getDataSource())
|| helper.hasLoadPath(loadData.fetcher.getDataClass()))) {
started = true;
//加载数据
loadData.fetcher.loadData(helper.getPriority(), this);
}
}
return started;
}
首先它会判断 dataToCache 是否为 null,第一次肯定会 null,所以,可以先不管;这里看 loadData.fetcher.loadData(); 这个方法,loadData() 是个接口,它有很多个实现方法,由于我们这里假设是网络下载,所以去到 HttpUrlFetcher#loadData() 中:
可以看到,它拿到 inputStream 后,通过 onDataReady() 方法回调回去,在 SourceGenerator#onDataReady() 中,对 dataToCache 进行赋值;然后又调用 cb.reschedule(); 方法
@Override
public void onDataReady(Object data) {
DiskCacheStrategy diskCacheStrategy = helper.getDiskCacheStrategy();
if (data != null && diskCacheStrategy.isDataCacheable(loadData.fetcher.getDataSource())) {
dataToCache = data;
// We might be being called back on someone else's thread. Before doing anything, we should
// reschedule to get back onto Glide's thread.
cb.reschedule();
} else {
cb.onDataFetcherReady(loadData.sourceKey, data, loadData.fetcher,
loadData.fetcher.getDataSource(), originalKey);
}
}
可以看到,绕了一圈,又调用 cb.reschedule() 方法,所以,它还是会走 DecodeJob 的run方法,且执行 runWrapped();是不是看得很恶心?恩,我写得也是。
此时的 runReason 还是为 SWITCH_TO_SOURCE_SERVICE,currentGenerator 为 SourceGenerator ;所以,它还是会执行 SourceGenerator startNext() 方法,只不过此时 dataToCache 已经不为空,所以会执行 cacheData() 方法:
可以看到,对这个已经解析完的数据,通过 helper.getDiskCache().put() 方法,存到到 DiskLruCache 硬盘缓存中。并通过 loadData.fetcher.clearup() 清除任务,赋值 sourceCacheGenerator 为 DataCacheGenerator。
此时 sourceCacheGenerator 不为 null,所以会走 DataCacheGenerator 的startNext() 方法;
由于此时已经能从 DiskLruCache 拿到数据了,所以会跳出循环,走下一步:
然后则会调用 loadData.fetcher.loadData() 方法:
该方法会进入 MultiModeLoader#loadData 方法,里面才是重点;由于这种网络的,所以loadData () 方法中的 fetchers.get(currentIndex).loadData(),调用的是 ByteBufferFileLoader 方法:
@Override
public void loadData(
@NonNull Priority priority, @NonNull DataCallback<? super Data> callback) {
this.priority = priority;
this.callback = callback;
exceptions = throwableListPool.acquire();
fetchers.get(currentIndex).loadData(priority, this);
...
}
#ByteBufferFileLoader
@Override
public void loadData(@NonNull Priority priority,
@NonNull DataCallback<? super ByteBuffer> callback) {
ByteBuffer result;
try {
result = ByteBufferUtil.fromFile(file);
} catch (IOException e) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Failed to obtain ByteBuffer for file", e);
}
callback.onLoadFailed(e);
return;
}
// 回调 onDataReady 方法
callback.onDataReady(result);
}
#DataCacheGenerator#onDataReady
@Override
public void onDataReady(Object data) {
cb.onDataFetcherReady(sourceKey, data, loadData.fetcher, DataSource.DATA_DISK_CACHE, sourceKey);
}
可以看到,最后回调了DataCacheGenerator#onDataReady() 方法,接下来则是回到 EngineJob 的onDataFetcherReady() 方法了。
从调试可以看到,最后走了 decodeFromRetrievedData方法,然后走的方法为 EngineJob#notifyComplete() - EngineJob#onResourceReady()
可以看到,最后终于调用 EngineJob 的 onResourceReady() 方法了。
这个方法在内存缓存中已经分析,它会把资源存到 弱引用且加载图片等操作。
此致,Glide 的缓存机制我们就分析完了。
是不是记不住?记不住就对了,看下面的总结和流程图把。
总结:
硬盘缓存时通过在 EngineJob 中的 DecodeJob 中完成的,先通过ResourcesCacheGenerator、DataCacheGenerator 看是否能从 DiskLruCache 拿到数据,如果不能,从SourceGenerator去解析数据,并把数据存储到 DiskLruCache 中,后面通过 DataCacheGenerator 的 startNext() 去分发 fetcher 。
最后会回调 EngineJob 的 onResourceReady() 方法了,该方法会加载图片,并把数据存到弱引用中
流程图:
三. 为啥要用弱引用
我们知道,glide 是用弱引用缓存当前的活跃资源的;为啥不直接从 LruCache 取呢?原因猜测如下:
- 这样可以保护当前使用的资源不会被 LruCache 算法回收
- 使用弱引用,即可以缓存正在使用的强引用资源,又不阻碍系统需要回收的无引用资源。