目前圖片框架,基本就是 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 算法回收
- 使用弱引用,即可以緩存正在使用的強引用資源,又不阻礙系統需要回收的無引用資源。