Glide原始碼分析(六),快取架構、存取命中分析
分析Glide快取策略,我們還得從之前分析的Engine#load方法入手,這個方法中,展示了快取讀取的一些策略,我們繼續貼上這塊程式碼。
Engine#load
public <R> LoadStatus load(
GlideContext glideContext,
Object model,
Key signature,
int width,
int height,
Class<?> resourceClass,
Class<R> transcodeClass,
Priority priority,
DiskCacheStrategy diskCacheStrategy,
Map<Class<?>, Transformation<?>> transformations,
boolean isTransformationRequired,
boolean isScaleOnlyOrNoTransform,
Options options,
boolean isMemoryCacheable,
boolean useUnlimitedSourceExecutorPool,
boolean useAnimationPool,
boolean onlyRetrieveFromCache,
ResourceCallback cb) {
Util.assertMainThread();
long startTime = VERBOSE_IS_LOGGABLE ? LogTime.getLogTime() : 0;
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;
}
EngineJob<?> current = jobs.get(key, onlyRetrieveFromCache);
if (current != null) {
current.addCallback(cb);
if (VERBOSE_IS_LOGGABLE) {
logWithTimeAndKey("Added to existing load", startTime, key);
}
return new LoadStatus(cb, current);
}
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);
engineJob.start(decodeJob);
if (VERBOSE_IS_LOGGABLE) {
logWithTimeAndKey("Started new load", startTime, key);
}
return new LoadStatus(cb, engineJob);
}
涉及到的快取型別如下:
記憶體和磁碟各自的兩種快取
- ActiveResources快取和MemoryCache,MemoryCache我們很好理解,就是Resouce在記憶體中的快取,ActiveResources是什麼意思呢,其實我們可以這樣理解,類似多級快取的概念,當然這裡不是特別的適合,ActiveResources快取和MemoryCache是同時存在的。ActiveResources快取存放的是所有未被clear的Request請求到的Resource,這部分Resource會存放至ActiveResources快取中,當Request被clear的時候,會把這部分在ActiveResources快取中的Resource移動至MemoryCache中去,只有MemoryCache中能夠命中,則這部分resource又會從MemoryCache移至ActiveResources快取中去,到這裡,相信大家能夠明白ActiveResources了,其實相當於是對記憶體快取再次做了一層,能夠有效的提高訪問速度,避免過多的操作MemoryCache,因為我們知道,MemoryCache中存放的快取可能很多,這樣的話,直接在上面做一層ActiveResources快取顯得就很有必要了。
- DiskCache,磁碟快取比較簡單,其中也分為ResourceCacheKey與DataCacheKey,一個是已經decode過的可以之間供Target給到View去渲染的,另一個是還未decode過的,快取的是源資料。磁碟快取的儲存是在第一次請求網路成功時候,會重新整理磁碟快取,此時處理的是源資料,至於是否會快取decode過後的資料,取決於DiskCacheStrategy的策略。
結合前面所有文章,這裡我再次簡要梳理下資源載入的過程。
簡要資源載入全過程
- 檢查ActiveResources快取中能否命中,若命中,則請求完成,通知Target渲染對應的View。若未命中,則進入Step2。
- 檢查MemoryCache快取能否命中,若命中,則請求完成,通知Target渲染對應的View。若未命中,則進入Step3。
- 構造或複用已有的EngineJob與DecodeJob,開始資源的載入,載入過程是ResourceCacheGenerator -> DataCacheGenerator -> SourceGenerator優先順序順序,不管哪種方式取到了資料,最終都會回撥至DecodeJob中處理,區別在於SourceGenerator會更新磁碟快取,此時的是DataCacheKey型別的快取。進入步驟4。
4. DecodeJob回撥中,一方面通過decodeFromData從DataFetcher中decode取到的原資料,轉換為View能夠展示的Resource,比如Drawable或Bitmap等,同時根據快取策略,取決是否會構建ResourceCacheKey型別的快取。decode這一步就已經結束,接下來會進行執行緒切換,最終切換到EngineJob的handleResultOnMainThread方法中,在這個方法中,會根據resource資源,構建一個非常重要的角色EngineResource,它是用來存放至ActiveResources快取和MemoryCache中的,這裡往ActiveResources快取中put資源就是在此時回撥至Engine的onEngineJobComplete中完成的。接下來就是回撥至SingleRequest中的onResourceReady中去更新Target中View的渲染資源了。至此,全過程就已經結束。
記憶體快取的要點
相信到這裡,有同學已經意識到,這裡並沒有更新MemoryCache呢,難道此時不正是應該更新到記憶體快取中去嗎?這裡什麼時候一個資源才會put至MemoryCache呢,回到ActiveResources快取中存放的EngineResource,它內部維護了一個計數,當計數減為0的時候,會觸發一個callback,它裡面的實現就是將EngineResource從ActiveResources快取移動至MemoryCache,也就是put到MemoryCache的時機,為什麼是這樣呢?通過我仔細的細節分析,每一個載入的SingleRequest中有一個對應的EngineResource的引用,SingleRequest是與生命週期繫結的,當所屬的請求上下文被onDestroy是,會通過其對應的RequestManager取消其所有的Request物件,而在Request的clear中則會呼叫Resource的recycle方法。此時就是EngineResource的recycle方法,因此,當生命週期onDestory被觸發時,對應EngineResource計數會減為0,也就觸發將EngineResource從ActiveResources快取移動至MemoryCache。此時ActiveResources快取會失效,同時我們可以看到MemoryCache命中時,恰恰會進行一個反向的操作,將EngineResource從MemoryCache重新移動至ActiveResources快取。這裡相信大家更明白了,為什麼這裡做了一個類似記憶體的二級快取,也是Glide處於一種優化的考慮吧。下面我們再來分析下磁碟快取DataCacheKey命中的情況。
磁碟快取的命中
在Glide原始碼分析(五),EngineJob與DecodeJob程式碼詳細載入過程一文中,我們看到資源載入成功快取到磁碟上是在SourceGenerator#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);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Finished encoding source to cache"
+ ", key: " + originalKey
+ ", data: " + dataToCache
+ ", encoder: " + encoder
+ ", duration: " + LogTime.getElapsedMillis(startTime));
}
} finally {
loadData.fetcher.cleanup();
}
sourceCacheGenerator =
new DataCacheGenerator(Collections.singletonList(loadData.sourceKey), helper, this);
}
這段程式碼邏輯相關比較好理解,根據loadData中的sourceKey以及簽名信息,構造一個DataChcheKey型別的物件,而後將其put至磁碟快取中,其中sourceKey就是我們載入資源的GlideUrl物件(https://p.upyun.com/docs/cloud/demo.jpg)。
磁碟快取的具體實現我們已經瞭解,預設是由DiskLruCacheWrapper實現,具體功能就是將資料寫入預先設定的快取目錄的檔案下,以檔案的方式存放。在分析D載入資源的詳細過程中,我們知道Engine#load會先在記憶體中查詢是否有快取命中,否則會啟動DecodeJob,在它中總共有三個DataFetchGenerator,這裡和磁碟快取相關的就是DataCacheGenerator,具體邏輯是在其DataCacheGenerator#startNext方法中。
@Override
public boolean startNext() {
while (modelLoaders == null || !hasNextModelLoader()) {
sourceIdIndex++;
if (sourceIdIndex >= cacheKeys.size()) {
return false;
}
Key sourceId = cacheKeys.get(sourceIdIndex);
// PMD.AvoidInstantiatingObjectsInLoops The loop iterates a limited number of times
// and the actions it performs are much more expensive than a single allocation.
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
Key originalKey = new DataCacheKey(sourceId, helper.getSignature());
cacheFile = helper.getDiskCache().get(originalKey);
if (cacheFile != null) {
this.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;
}
我們假定記憶體快取以及在啟用的資源池中均沒有命中,則此時會根據GlideUrl[https://p.upyun.com/docs/cloud/demo.jpg] 以它和簽名組成的DataCacheKey,從DiskCache中去尋找這個快取檔案,DiskLruCacheWrapper#get方法實現如下:
@Override
public File get(Key key) {
String safeKey = safeKeyGenerator.getSafeKey(key);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Get: Obtained: " + safeKey + " for for Key: " + key);
}
File result = null;
try {
// It is possible that the there will be a put in between these two gets. If so that shouldn't
// be a problem because we will always put the same value at the same key so our input streams
// will still represent the same data.
final DiskLruCache.Value value = getDiskCache().get(safeKey);
if (value != null) {
result = value.getFile(0);
}
} catch (IOException e) {
if (Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "Unable to get from disk cache", e);
}
}
return result;
}
可以看到,真正去根據key獲取檔案資訊實際上是由getDiskCache().get方法去實現的,這裡我們需要分析getDiskCache()的實現,也就是操作磁碟檔案的類了。
private synchronized DiskLruCache getDiskCache() throws IOException {
if (diskLruCache == null) {
diskLruCache = DiskLruCache.open(directory, APP_VERSION, VALUE_COUNT, maxSize);
}
return diskLruCache;
}
getDiskCache的實現也很明確,就是呼叫DiskLruCache的靜態open方法,建立一個diskLruCache單例物件,方法入參directory表示快取目錄,maxSize快取最大大小。open的實現如下:
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
throws IOException {
...
// Prefer to pick up where we left off.
DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
if (cache.journalFile.exists()) {
try {
cache.readJournal();
cache.processJournal();
return cache;
} catch (IOException journalIsCorrupt) {
System.out
.println("DiskLruCache "
+ directory
+ " is corrupt: "
+ journalIsCorrupt.getMessage()
+ ", removing");
cache.delete();
}
}
// Create a new empty cache.
directory.mkdirs();
cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
cache.rebuildJournal();
return cache;
}
我們分析最簡單的情況,如果在磁碟中有快取檔案了,顯然此時if語句journalFile檔案是存在的,因此,接下來呼叫readJournal根據快取key將索引資訊讀入lruEntries中,每一個快取key對應有一個Entry資訊。Entry中儲存快取檔案索引的是cleanFiles,cleanFiles雖然是一個File陣列,但是目前glide對於這個資料的size是恆為1的,也就是快取key,Entry,檔案是一個一一對應的關係,這裡glide用陣列提供了將來一種可擴充套件性的預留實現。這樣磁碟快取索引也就建立完成。下面繼續看DiskLruCache#get的實現
public synchronized Value get(String key) throws IOException {
checkNotClosed();
Entry entry = lruEntries.get(key);
if (entry == null) {
return null;
}
if (!entry.readable) {
return null;
}
...
return new Value(key, entry.sequenceNumber, entry.cleanFiles, entry.lengths);
}
還是分析簡單的情況,這裡就是在Entry索引中根據key資訊查詢,而後將結果返個DiskLruCacheWrapper,這裡我們看到有entry.cleanFiles,。
entry.cleanFiles也就是對應在DataCacheGenerator中cacheFile的例項。因此整個在磁碟cache中查詢檔案的過程也就比較清楚了。再次看DataCacheGenerator中的startNext,此時cacheFile能夠命中,因此會觸發對應的modelLoader去從快取中載入資料。
總結
這裡我們介紹了記憶體快取,ActiveResources與MemoryCache的命中情況分析,以及DiskCache的DataCacheKey的命中分析,DiskCach還有一個關於ResourceCacheKey的情況,相應的程式碼在ResourceCacheGenerator中,我們這裡不再研究,也是一樣的思路。這裡再強調幾點,DataCacheKey中快取的是DataFetcher拉取的源資料,也就是原始的資料,ResourceCacheKey則是基於原始資料,做的一層更精細的快取,從它們的構造方法中我們可以看到。
key =
new ResourceCacheKey(
decodeHelper.getArrayPool(),
currentSourceKey,
signature,
width,
height,
appliedTransformation,
resourceSubClass,
options);
// DataCacheKey
key = new DataCacheKey(currentSourceKey, signature);
正如我們簡單的例子,這裡DataCacheKey只有網路的url決定,也即是一個數據流物件,不同的decode可以來擴充套件它,ResourceCacheKey就是這樣一種快取。至此,對於Glide的快取架構我們就分析完了,整個系列差不多也接近尾聲了,後面文章中,我會整理一些大綱的匯流排,供大家自己研讀。