分析Glide緩存策略识颊,我們還得從之前分析的Engine#load方法入手,這個方法中奕坟,展示了緩存讀取的一些策略祥款,我們繼續(xù)貼上這塊代碼。
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);
}
涉及到的緩存類型如下:
內(nèi)存和磁盤各自的兩種緩存
- ActiveResources緩存和MemoryCache月杉,MemoryCache我們很好理解刃跛,就是Resouce在內(nèi)存中的緩存,ActiveResources是什么意思呢沙合,其實我們可以這樣理解,類似多級緩存的概念跌帐,當然這里不是特別的適合首懈,ActiveResources緩存和MemoryCache是同時存在的。ActiveResources緩存存放的是所有未被clear的Request請求到的Resource谨敛,這部分Resource會存放至ActiveResources緩存中究履,當Request被clear的時候,會把這部分在ActiveResources緩存中的Resource移動至MemoryCache中去脸狸,只有MemoryCache中能夠命中最仑,則這部分resource又會從MemoryCache移至ActiveResources緩存中去,到這里炊甲,相信大家能夠明白ActiveResources了泥彤,其實相當于是對內(nèi)存緩存再次做了一層,能夠有效的提高訪問速度卿啡,避免過多的操作MemoryCache吟吝,因為我們知道,MemoryCache中存放的緩存可能很多颈娜,這樣的話剑逃,直接在上面做一層ActiveResources緩存顯得就很有必要了浙宜。
- DiskCache,磁盤緩存比較簡單蛹磺,其中也分為ResourceCacheKey與DataCacheKey粟瞬,一個是已經(jīng)decode過的可以之間供Target給到View去渲染的,另一個是還未decode過的萤捆,緩存的是源數(shù)據(jù)裙品。磁盤緩存的保存是在第一次請求網(wǎng)絡成功時候,會刷新磁盤緩存鳖轰,此時處理的是源數(shù)據(jù)清酥,至于是否會緩存decode過后的數(shù)據(jù),取決于DiskCacheStrategy的策略蕴侣。
結(jié)合前面所有文章焰轻,這里我再次簡要梳理下資源加載的過程。
簡要資源加載全過程
- 檢查ActiveResources緩存中能否命中昆雀,若命中辱志,則請求完成,通知Target渲染對應的View狞膘。若未命中揩懒,則進入Step2。
- 檢查MemoryCache緩存能否命中挽封,若命中已球,則請求完成,通知Target渲染對應的View辅愿。若未命中智亮,則進入Step3。
- 構(gòu)造或復用已有的EngineJob與DecodeJob点待,開始資源的加載阔蛉,加載過程是ResourceCacheGenerator -> DataCacheGenerator -> SourceGenerator優(yōu)先級順序,不管哪種方式取到了數(shù)據(jù)癞埠,最終都會回調(diào)至DecodeJob中處理状原,區(qū)別在于SourceGenerator會更新磁盤緩存,此時的是DataCacheKey類型的緩存苗踪。進入步驟4颠区。
4. DecodeJob回調(diào)中,一方面通過decodeFromData從DataFetcher中decode取到的原數(shù)據(jù)通铲,轉(zhuǎn)換為View能夠展示的Resource瓦呼,比如Drawable或Bitmap等,同時根據(jù)緩存策略,取決是否會構(gòu)建ResourceCacheKey類型的緩存央串。decode這一步就已經(jīng)結(jié)束磨澡,接下來會進行線程切換,最終切換到EngineJob的handleResultOnMainThread方法中质和,在這個方法中稳摄,會根據(jù)resource資源,構(gòu)建一個非常重要的角色EngineResource饲宿,它是用來存放至ActiveResources緩存和MemoryCache中的厦酬,這里往ActiveResources緩存中put資源就是在此時回調(diào)至Engine的onEngineJobComplete中完成的。接下來就是回調(diào)至SingleRequest中的onResourceReady中去更新Target中View的渲染資源了瘫想。至此仗阅,全過程就已經(jīng)結(jié)束。
內(nèi)存緩存的要點
相信到這里国夜,有同學已經(jīng)意識到减噪,這里并沒有更新MemoryCache呢,難道此時不正是應該更新到內(nèi)存緩存中去嗎车吹?這里什么時候一個資源才會put至MemoryCache呢筹裕,回到ActiveResources緩存中存放的EngineResource,它內(nèi)部維護了一個計數(shù)窄驹,當計數(shù)減為0的時候朝卒,會觸發(fā)一個callback,它里面的實現(xiàn)就是將EngineResource從ActiveResources緩存移動至MemoryCache乐埠,也就是put到MemoryCache的時機抗斤,為什么是這樣呢?通過我仔細的細節(jié)分析丈咐,每一個加載的SingleRequest中有一個對應的EngineResource的引用瑞眼,SingleRequest是與生命周期綁定的,當所屬的請求上下文被onDestroy是扯罐,會通過其對應的RequestManager取消其所有的Request對象负拟,而在Request的clear中則會調(diào)用Resource的recycle方法烦衣。此時就是EngineResource的recycle方法歹河,因此,當生命周期onDestory被觸發(fā)時花吟,對應EngineResource計數(shù)會減為0秸歧,也就觸發(fā)將EngineResource從ActiveResources緩存移動至MemoryCache。此時ActiveResources緩存會失效衅澈,同時我們可以看到MemoryCache命中時键菱,恰恰會進行一個反向的操作,將EngineResource從MemoryCache重新移動至ActiveResources緩存今布。這里相信大家更明白了经备,為什么這里做了一個類似內(nèi)存的二級緩存拭抬,也是Glide處于一種優(yōu)化的考慮吧。下面我們再來分析下磁盤緩存DataCacheKey命中的情況侵蒙。
磁盤緩存的命中
在Glide源碼分析(五)造虎,EngineJob與DecodeJob代碼詳細加載過程一文中,我們看到資源加載成功緩存到磁盤上是在SourceGenerator#cacheData方法中進行的纷闺,我們來看其具體實現(xiàn)算凿。
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);
}
這段代碼邏輯相關比較好理解,根據(jù)loadData中的sourceKey以及簽名信息犁功,構(gòu)造一個DataChcheKey類型的對象氓轰,而后將其put至磁盤緩存中,其中sourceKey就是我們加載資源的GlideUrl對象(https://p.upyun.com/docs/cloud/demo.jpg)浸卦。
磁盤緩存的具體實現(xiàn)我們已經(jīng)了解署鸡,默認是由DiskLruCacheWrapper實現(xiàn),具體功能就是將數(shù)據(jù)寫入預先設置的緩存目錄的文件下镐躲,以文件的方式存放储玫。在分析D加載資源的詳細過程中,我們知道Engine#load會先在內(nèi)存中查找是否有緩存命中萤皂,否則會啟動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;
}
我們假定內(nèi)存緩存以及在激活的資源池中均沒有命中,則此時會根據(jù)GlideUrl[https://p.upyun.com/docs/cloud/demo.jpg] 以它和簽名組成的DataCacheKey入录,從DiskCache中去尋找這個緩存文件蛤奥,DiskLruCacheWrapper#get方法實現(xiàn)如下:
@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;
}
可以看到,真正去根據(jù)key獲取文件信息實際上是由getDiskCache().get方法去實現(xiàn)的僚稿,這里我們需要分析getDiskCache()的實現(xiàn)凡桥,也就是操作磁盤文件的類了。
private synchronized DiskLruCache getDiskCache() throws IOException {
if (diskLruCache == null) {
diskLruCache = DiskLruCache.open(directory, APP_VERSION, VALUE_COUNT, maxSize);
}
return diskLruCache;
}
getDiskCache的實現(xiàn)也很明確蚀同,就是調(diào)用DiskLruCache的靜態(tài)open方法缅刽,創(chuàng)建一個diskLruCache單例對象,方法入?yún)irectory表示緩存目錄蠢络,maxSize緩存最大大小衰猛。open的實現(xiàn)如下:
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文件是存在的啡省,因此,接下來調(diào)用readJournal根據(jù)緩存key將索引信息讀入lruEntries中,每一個緩存key對應有一個Entry信息卦睹。Entry中保存緩存文件索引的是cleanFiles畦戒,cleanFiles雖然是一個File數(shù)組,但是目前glide對于這個數(shù)據(jù)的size是恒為1的结序,也就是緩存key,Entry,文件是一個一一對應的關系兢交,這里glide用數(shù)組提供了將來一種可擴展性的預留實現(xiàn)。這樣磁盤緩存索引也就建立完成笼痹。下面繼續(xù)看DiskLruCache#get的實現(xiàn)
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索引中根據(jù)key信息查找,而后將結(jié)果返個DiskLruCacheWrapper凳干,這里我們看到有entry.cleanFiles晴裹,。
entry.cleanFiles也就是對應在DataCacheGenerator中cacheFile的實例救赐。因此整個在磁盤cache中查找文件的過程也就比較清楚了涧团。再次看DataCacheGenerator中的startNext,此時cacheFile能夠命中经磅,因此會觸發(fā)對應的modelLoader去從緩存中加載數(shù)據(jù)泌绣。
總結(jié)
這里我們介紹了內(nèi)存緩存,ActiveResources與MemoryCache的命中情況分析预厌,以及DiskCache的DataCacheKey的命中分析阿迈,DiskCach還有一個關于ResourceCacheKey的情況,相應的代碼在ResourceCacheGenerator中轧叽,我們這里不再研究苗沧,也是一樣的思路。這里再強調(diào)幾點炭晒,DataCacheKey中緩存的是DataFetcher拉取的源數(shù)據(jù)待逞,也就是原始的數(shù)據(jù),ResourceCacheKey則是基于原始數(shù)據(jù)网严,做的一層更精細的緩存识樱,從它們的構(gòu)造方法中我們可以看到。
key =
new ResourceCacheKey(
decodeHelper.getArrayPool(),
currentSourceKey,
signature,
width,
height,
appliedTransformation,
resourceSubClass,
options);
// DataCacheKey
key = new DataCacheKey(currentSourceKey, signature);
正如我們簡單的例子震束,這里DataCacheKey只有網(wǎng)絡的url決定怜庸,也即是一個數(shù)據(jù)流對象,不同的decode可以來擴展它驴一,ResourceCacheKey就是這樣一種緩存休雌。至此灶壶,對于Glide的緩存架構(gòu)我們就分析完了肝断,整個系列差不多也接近尾聲了,后面文章中,我會整理一些大綱的總線胸懈,供大家自己研讀担扑。