前言
最近有個(gè)想法——就是把 Android 主流開源框架進(jìn)行深入分析,然后寫成一系列文章更振,包括該框架的詳細(xì)使用與源碼解析宙帝。目的是通過鑒賞大神的源碼來(lái)了解框架底層的原理,也就是做到不僅要知其然捌锭,還要知其所以然俘陷。
這里我說下自己閱讀源碼的經(jīng)驗(yàn),我一般都是按照平時(shí)使用某個(gè)框架或者某個(gè)系統(tǒng)源碼的使用流程入手的观谦,首先要知道怎么使用拉盾,然后再去深究每一步底層做了什么,用了哪些好的設(shè)計(jì)模式豁状,為什么要這么設(shè)計(jì)捉偏。
系列文章:
- Android 主流開源框架(一)OkHttp 鋪墊-HttpClient 與 HttpURLConnection 使用詳解
- Android 主流開源框架(二)OkHttp 使用詳解
- Android 主流開源框架(三)OkHttp 源碼解析
- Android 主流開源框架(四)Retrofit 使用詳解
- Android 主流開源框架(五)Retrofit 源碼解析
- Android 主流開源框架(六)Glide 的執(zhí)行流程源碼解析
- Android 主流開源框架(七)Glide 的緩存機(jī)制
- 更多框架持續(xù)更新中...
更多干貨請(qǐng)關(guān)注 AndroidNotes
上一篇主要講了 Glide 的執(zhí)行流程,當(dāng)時(shí)是禁用了內(nèi)存與磁盤緩存的泻红,所以涉及到緩存相關(guān)的流程都省略了夭禽,還沒看上篇的建議先去看一遍,因?yàn)檫@篇講的緩存機(jī)制很多都是要與上篇聯(lián)系起來(lái)的谊路。
一讹躯、Glide 中的緩存
默認(rèn)情況下,Glide 在加載圖片之前會(huì)依次檢查是否有以下緩存:
- 活動(dòng)資源 (Active Resources):正在使用的圖片
- 內(nèi)存緩存 (Memory cache):內(nèi)存緩存中的圖片
- 資源類型(Resource):磁盤緩存中轉(zhuǎn)換過后的圖片
- 數(shù)據(jù)來(lái)源 (Data):磁盤緩存中的原始圖片
也就是說 Glide 中實(shí)際有四級(jí)緩存缠劝,前兩個(gè)屬于內(nèi)存緩存潮梯,后兩個(gè)屬于磁盤緩存。以上每步是按順序檢查的惨恭,檢查到哪一步有緩存就直接返回圖片秉馏,否則繼續(xù)檢查下一步。如果都沒有緩存脱羡,則 Glide 會(huì)從原始資源(File萝究、Uri、遠(yuǎn)程圖片 url 等)中加載圖片轻黑。
二糊肤、緩存 Key
緩存功能必然要有一個(gè)唯一的緩存 Key 用來(lái)存儲(chǔ)和查找對(duì)應(yīng)的緩存數(shù)據(jù)。那么下面我們就看下 Glide 的緩存 Key 是怎么生成的氓鄙。
其實(shí)上一篇文章中已經(jīng)瞄過一眼了馆揉,是在 Engine 類的 load() 方法中生成的:
/*Engine*/
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,
Executor callbackExecutor) {
EngineKey key =
keyFactory.buildKey(
model,
signature,
width,
height,
transformations,
resourceClass,
transcodeClass,
options);
...
}
繼續(xù)跟進(jìn):
/*EngineKeyFactory*/
EngineKey buildKey(
Object model,
Key signature,
int width,
int height,
Map<Class<?>, Transformation<?>> transformations,
Class<?> resourceClass,
Class<?> transcodeClass,
Options options) {
return new EngineKey(
model, signature, width, height, transformations, resourceClass, transcodeClass, options);
}
class EngineKey implements Key {
...
@Override
public boolean equals(Object o) {
if (o instanceof EngineKey) {
EngineKey other = (EngineKey) o;
return model.equals(other.model)
&& signature.equals(other.signature)
&& height == other.height
&& width == other.width
&& transformations.equals(other.transformations)
&& resourceClass.equals(other.resourceClass)
&& transcodeClass.equals(other.transcodeClass)
&& options.equals(other.options);
}
return false;
}
@Override
public int hashCode() {
if (hashCode == 0) {
hashCode = model.hashCode();
hashCode = 31 * hashCode + signature.hashCode();
hashCode = 31 * hashCode + width;
hashCode = 31 * hashCode + height;
hashCode = 31 * hashCode + transformations.hashCode();
hashCode = 31 * hashCode + resourceClass.hashCode();
hashCode = 31 * hashCode + transcodeClass.hashCode();
hashCode = 31 * hashCode + options.hashCode();
}
return hashCode;
}
...
}
可以看到,這里傳入了 model(File抖拦、Uri升酣、遠(yuǎn)程圖片 url 等)舷暮、簽名、寬高(這里的寬高是指顯示圖片的 View 的寬高噩茄,不是圖片的寬高)等參數(shù)下面,然后通過 EngineKeyFactory 構(gòu)建了一個(gè) EngineKey 對(duì)象(即緩存 Key),然后 EngineKey 通過重寫 equals() 與 hashCode() 方法來(lái)保證緩存 Key 的唯一性绩聘。
雖然決定緩存 Key 的參數(shù)很多沥割,但是加載圖片的代碼寫好后這些參數(shù)都是不會(huì)變的。很多人遇到的 “服務(wù)器返回的圖片變了凿菩,但是前端顯示的還是以前的圖片” 的問題就是這個(gè)原因机杜,因?yàn)殡m然服務(wù)器返回的圖片變了,但是圖片 url 還是以前那個(gè)衅谷,其他決定緩存 Key 的參數(shù)也不會(huì)變铸本,Glide 就認(rèn)為有該緩存腹泌,就會(huì)直接從緩存中獲取,而不是重新下載,所以顯示的還是以前的圖片掰烟。
對(duì)于這個(gè)問題臀规,有幾種方法可以解決姥卢,分別如下:
(1)圖片 url 不要固定
也就是說如果某個(gè)圖片改變了摔蓝,那么該圖片的 url 也要跟著改變。
(2)使用 signature() 更改緩存 Key
我們剛剛知道了決定緩存 Key 的參數(shù)包括 signature预茄,剛好 Glide 提供了 signature() 方法來(lái)更改該參數(shù)兴溜。具體如下:
Glide.with(this).load(url).signature(new ObjectKey(timeModified)).into(imageView);
其中 timeModified 可以是任意數(shù)據(jù),這里用圖片的更改時(shí)間耻陕。例如圖片改變了拙徽,那么服務(wù)器應(yīng)該改變?cè)撟侄蔚闹担缓箅S圖片 url 一起返回給前端诗宣,這樣前端加載的時(shí)候就知道圖片改變了膘怕,需要重新下載。
(3)禁用緩存
前端加載圖片的時(shí)候設(shè)置禁用內(nèi)存與磁盤緩存召庞,這樣每次加載都會(huì)重新下載最新的岛心。
Glide.with(this)
.load(url)
.skipMemoryCache(true) // 禁用內(nèi)存緩存
.diskCacheStrategy(DiskCacheStrategy.NONE) // 禁用磁盤緩存
.into(imageView);
以上 3 種方法都可以解決問題,但是推薦使用第一種篮灼,這樣設(shè)計(jì)是比較規(guī)范的忘古,后臺(tái)人員就應(yīng)該這么設(shè)計(jì)。第二種方法也可以诅诱,但是這樣無(wú)疑是給后端髓堪、前端人員都增加了麻煩。第三種是最不推薦的,相當(dāng)于舍棄了緩存功能干旁,每次都要從服務(wù)器重新下載圖片驶沼,不僅浪費(fèi)用戶流量,而且每次加載需要等待也影響用戶體驗(yàn)争群。
三回怜、緩存策略
在講 Glide 中的內(nèi)存緩存與磁盤緩存之前,我們先了解下緩存策略换薄。例如加載一張圖片顯示到設(shè)備上的緩存策略應(yīng)該這樣設(shè)計(jì):
當(dāng)程序第一次從網(wǎng)絡(luò)上加載圖片后玉雾,就將它緩存到設(shè)備磁盤中,下次使用這張圖片的時(shí)候就不用再?gòu)木W(wǎng)絡(luò)上加載了专控。為了提升用戶體驗(yàn)抹凳,往往還會(huì)在內(nèi)存中緩存一份,因?yàn)閺膬?nèi)存中加載圖片比從磁盤中加載要快伦腐。程序下次加載這張圖片的時(shí)候首先從內(nèi)存中查找,如果沒有就去磁盤中查找失都,都沒有才從網(wǎng)絡(luò)上加載柏蘑。
這里的緩存策略涉及到緩存的添加、獲取和刪除操作粹庞,什么時(shí)候進(jìn)行這些操作等邏輯就構(gòu)成了一種緩存算法咳焚。目前常用的一種緩存算法是 LRU(Least Recently Used),即最近最少使用算法庞溜。它的核心思想是當(dāng)緩存滿時(shí)革半,會(huì)優(yōu)先淘汰那些最近最少使用的緩存對(duì)象。采用 LRU 算法的緩存有兩種:LruCache 和 DiskLruCache流码,LruCache 用于實(shí)現(xiàn)內(nèi)存緩存又官,DiskLruCache 則用于實(shí)現(xiàn)磁盤緩存,兩者結(jié)合使用就可以實(shí)現(xiàn)上面的緩存策略漫试。
LruCache 和 DiskLruCache 的內(nèi)部算法原理是采用一個(gè) LinkedHashMap 以強(qiáng)引用的方式存儲(chǔ)外界的緩存對(duì)象六敬,其提供了 get() 和 put() 方法來(lái)完成緩存的獲取和添加的操作。當(dāng)緩存滿時(shí)驾荣,會(huì)移除較早使用的緩存對(duì)象外构,然后再添加新的緩存對(duì)象〔ブ溃可以用如下流程圖表示:
下面要講的 Glide 中的內(nèi)存緩存與磁盤緩存也是用的 LruCache 和 DiskLruCache审编,只不過 LruCache 用的不是 SDK 中的,而是自己寫的歧匈,但是看了原理其實(shí)是一樣的垒酬。而 DiskLruCache 用的是 JakeWharton 封裝的 DiskLruCache。
四、內(nèi)存緩存
Glide 默認(rèn)是配置了內(nèi)存緩存的伤溉,當(dāng)然 Glide 也提供了 API 給我們開啟和禁用般码,如下:
// 開啟內(nèi)存緩存
Glide.with(this).load(url).skipMemoryCache(false).into(imageView);
// 禁用內(nèi)存緩存
Glide.with(this).load(url).skipMemoryCache(true).into(imageView);
文章開頭說了,Glide 在加載圖片之前會(huì)依次檢查四級(jí)緩存÷夜耍現(xiàn)在緩存 Key 也拿到了板祝,那么我們先看看前兩級(jí)中的內(nèi)存緩存是怎么獲取的(下面分析的時(shí)候需要用默認(rèn)加載語(yǔ)句或者手動(dòng)開啟內(nèi)存緩存)。從上一篇文章知道走净,獲取內(nèi)存緩存的代碼也是在 Engine 類的 load() 方法中券时,我們進(jìn)去看看:
/*Engine*/
public <R> LoadStatus load(...) {
// 構(gòu)建緩存 Key
EngineKey key =
keyFactory.buildKey(
model,
signature,
width,
height,
transformations,
resourceClass,
transcodeClass,
options);
EngineResource<?> memoryResource;
synchronized (this) {
// 從內(nèi)存中加載緩存數(shù)據(jù)
memoryResource = loadFromMemory(key, isMemoryCacheable, startTime);
...
}
// 加載完成回調(diào)
cb.onResourceReady(memoryResource, DataSource.MEMORY_CACHE);
return null;
}
繼續(xù)點(diǎn)擊 loadFromMemory() 方法進(jìn)去看看:
/*Engine*/
private EngineResource<?> loadFromMemory(
EngineKey key, boolean isMemoryCacheable, long startTime) {
if (!isMemoryCacheable) {
return null;
}
//(1)
EngineResource<?> active = loadFromActiveResources(key);
if (active != null) {
if (VERBOSE_IS_LOGGABLE) {
logWithTimeAndKey("Loaded resource from active resources", startTime, key);
}
return active;
}
//(2)
EngineResource<?> cached = loadFromCache(key);
if (cached != null) {
if (VERBOSE_IS_LOGGABLE) {
logWithTimeAndKey("Loaded resource from cache", startTime, key);
}
return cached;
}
return null;
}
這里我標(biāo)記了兩個(gè)關(guān)注點(diǎn),分別如下:
- (1):表示從 ActiveResources 中加載緩存數(shù)據(jù)伏伯。
- (2):表示從內(nèi)存緩存中加載緩存數(shù)據(jù)橘洞。
是的,這就是 Glide 四級(jí)緩存中的前兩級(jí)说搅。ActiveResources 里面主要包含了一個(gè) HashMap 的相關(guān)操作炸枣,然后 HashMap 中保存的值又是弱引用來(lái)引用的,也就是說這里是采用一個(gè)弱引用的 HashMap 來(lái)緩存活動(dòng)資源弄唧。下面我們分析下這兩個(gè)關(guān)注點(diǎn):
-
Engine#loadFromMemory() 中的關(guān)注點(diǎn)(1)
我們點(diǎn)擊關(guān)注點(diǎn)(1)看看:
/*Engine*/
private EngineResource<?> loadFromActiveResources(Key key) {
EngineResource<?> active = activeResources.get(key);
if (active != null) {
active.acquire();
}
return active;
}
繼續(xù)看 get() 方法:
/*ActiveResources*/
synchronized EngineResource<?> get(Key key) {
// 從 HashMap 中獲取 ResourceWeakReference
ResourceWeakReference activeRef = activeEngineResources.get(key);
if (activeRef == null) {
return null;
}
// 從弱引用中獲取活動(dòng)資源
EngineResource<?> active = activeRef.get();
if (active == null) {
cleanupActiveReference(activeRef);
}
return active;
}
可以看到适肠,這里首先從 HashMap 中獲取 ResourceWeakReference(繼承了弱引用),然后從弱引用中獲取了活動(dòng)資源(獲取活動(dòng)資源)
候引,即正在使用的圖片侯养。也就是說正在使用的圖片實(shí)際是通過弱引用維護(hù),然后保存在 HashMap 中的澄干。
繼續(xù)看 acquire() 方法:
/*EngineResource*/
synchronized void acquire() {
if (isRecycled) {
throw new IllegalStateException("Cannot acquire a recycled resource");
}
++acquired;
}
發(fā)現(xiàn)這里是將 acquired 變量 +1逛揩,這個(gè)變量用來(lái)記錄圖片被引用的次數(shù)。該變量除了 acquire() 方法中做了 +1 操作麸俘,還在 release() 方法中做了 -1 的操作辩稽,如下:
/*EngineResource*/
void release() {
boolean release = false;
synchronized (this) {
if (acquired <= 0) {
throw new IllegalStateException("Cannot release a recycled or not yet acquired resource");
}
if (--acquired == 0) {
release = true;
}
}
if (release) {
listener.onResourceReleased(key, this);
}
}
/*Engine*/
@Override
public void onResourceReleased(Key cacheKey, EngineResource<?> resource) {
activeResources.deactivate(cacheKey);
if (resource.isMemoryCacheable()) {
cache.put(cacheKey, resource);
} else {
resourceRecycler.recycle(resource, /*forceNextFrame=*/ false);
}
}
/*ActiveResources*/
synchronized void deactivate(Key key) {
ResourceWeakReference removed = activeEngineResources.remove(key);
if (removed != null) {
removed.reset();
}
}
可以看到,當(dāng) acquired 減到 0 的時(shí)候疾掰,又回調(diào)了 Engine#onResourceReleased()搂誉。在 onResourceReleased() 方法中首先將活動(dòng)資源從弱引用的 HashMap 中移除(清理活動(dòng)資源)
,然后將它緩存到內(nèi)存緩存中(存儲(chǔ)內(nèi)存緩存)
静檬。
也就是說炭懊,release() 方法主要是釋放資源。當(dāng)我們從一屏滑動(dòng)到下一屏的時(shí)候拂檩,上一屏的圖片就會(huì)看不到侮腹,這個(gè)時(shí)候就會(huì)調(diào)用該方法。還有我們關(guān)閉當(dāng)前顯示圖片的頁(yè)面時(shí)會(huì)調(diào)用 onDestroy() 方法稻励,最終也會(huì)調(diào)用該方法父阻。這兩種情況很明顯是不需要用到該圖片了愈涩,那么理所當(dāng)然的會(huì)調(diào)用 release() 方法來(lái)釋放弱引用的 HashMap 中緩存的活動(dòng)資源。
這樣也就實(shí)現(xiàn)了正在使用中的圖片使用弱引用來(lái)進(jìn)行緩存加矛,不在使用中的圖片使用 LruCache 來(lái)進(jìn)行緩存的功能履婉。
-
Engine#loadFromMemory() 中的關(guān)注點(diǎn)(2)
我們點(diǎn)擊關(guān)注點(diǎn)(2)看看:
/*Engine*/
private EngineResource<?> loadFromCache(Key key) {
//(2.1)
EngineResource<?> cached = getEngineResourceFromCache(key);
if (cached != null) {
//(2.2)
cached.acquire();
//(2.3)
activeResources.activate(key, cached);
}
return cached;
}
這里我標(biāo)注了 3 個(gè)關(guān)注點(diǎn),分別如下:
- (2.1):這里是獲取內(nèi)存緩存斟览。點(diǎn)進(jìn)去看看:
/*Engine*/
private EngineResource<?> getEngineResourceFromCache(Key key) {
Resource<?> cached = cache.remove(key);
final EngineResource<?> result;
if (cached == null) {
result = null;
} else if (cached instanceof EngineResource) {
// Save an object allocation if we've cached an EngineResource (the typical case).
result = (EngineResource<?>) cached;
} else {
result =
new EngineResource<>(
cached, /*isMemoryCacheable=*/ true, /*isRecyclable=*/ true, key, /*listener=*/ this);
}
return result;
}
可以看到毁腿,這里的 cache 就是 LruResourceCache,remove() 操作就是移除緩存的同時(shí)獲取該緩存(獲取內(nèi)存緩存)
苛茂。LruResourceCache 繼承了 LruCache已烤,雖然不是 SDK 中的 LruCache,但是看了原理其實(shí)是一樣的妓羊,也就是說內(nèi)存緩存使用的是 LRU 算法實(shí)現(xiàn)的胯究。
- (2.2):與關(guān)注點(diǎn)(1)中的獲取活動(dòng)資源一樣,也是將 acquired 變量 +1躁绸,然后用來(lái)記錄圖片被引用的次數(shù)裕循。
- (2.3):將內(nèi)存中獲取的緩存數(shù)據(jù)緩存到弱引用的 HashMap 中。
再回去看我標(biāo)記了高亮的文字涨颜,發(fā)現(xiàn)這 2 個(gè)關(guān)注點(diǎn)主要做了獲取活動(dòng)資源费韭、清理活動(dòng)資源、獲取內(nèi)存緩存庭瑰、存儲(chǔ)內(nèi)存緩存。其中清理內(nèi)存緩存的操作 LRU 算法已經(jīng)自動(dòng)幫我們實(shí)現(xiàn)了抢埋,那是不是發(fā)現(xiàn)少了存儲(chǔ)活動(dòng)資源的步驟弹灭?
活動(dòng)資源是哪里來(lái)的呢?其實(shí)就是我們從網(wǎng)絡(luò)請(qǐng)求中返回的數(shù)據(jù)揪垄。從上一篇文章(可以回去搜索 onEngineJobComplete() 回憶下上下文)可以知道網(wǎng)絡(luò)請(qǐng)求回來(lái)后先進(jìn)行解碼穷吮,然后在 Engine#onEngineJobComplete() 方法中進(jìn)行了活動(dòng)資源的存儲(chǔ),我再貼下代碼:
/*Engine*/
@Override
public synchronized void onEngineJobComplete(
EngineJob<?> engineJob, Key key, EngineResource<?> resource) {
// A null resource indicates that the load failed, usually due to an exception.
if (resource != null && resource.isMemoryCacheable()) {
activeResources.activate(key, resource);
}
jobs.removeIfCurrent(key, engineJob);
}
/*ActiveResources*/
synchronized void activate(Key key, EngineResource<?> resource) {
ResourceWeakReference toPut =
new ResourceWeakReference(
key, resource, resourceReferenceQueue, isActiveResourceRetentionAllowed);
// 存儲(chǔ)活動(dòng)資源
ResourceWeakReference removed = activeEngineResources.put(key, toPut);
if (removed != null) {
removed.reset();
}
}
以上就是 Glide 內(nèi)存緩存的原理饥努,但是我們發(fā)現(xiàn)除了利用 LruCache 實(shí)現(xiàn)的內(nèi)存緩存捡鱼,還有一個(gè)是利用弱引用的 HashMap 實(shí)現(xiàn)的。一般如果讓我們?cè)O(shè)計(jì)酷愧,可能就只會(huì)想到用 LruCache 實(shí)現(xiàn)內(nèi)存緩存驾诈。那這里設(shè)計(jì)多一個(gè)弱引用的 HashMap 的好處是什么呢?
郭霖的深入探究Glide的緩存機(jī)制中是這樣描述的使用activeResources來(lái)緩存正在使用中的圖片溶浴,可以保護(hù)這些圖片不會(huì)被LruCache算法回收掉
乍迄。我覺得這樣解釋不太合理,我看完源碼并沒有覺得這個(gè)弱引用的 HashMap 起到了 “保護(hù)圖片不被 LRU 算法回收” 的作用士败。我覺得有如下作用(如有不對(duì)請(qǐng)指出):
(1)提高訪問效率
因?yàn)?ActiveResources 用的是 HashMap闯两,而 LruCache 用的是 LinkedHashMap,并且在實(shí)例化 LinkedHashMap 時(shí)是設(shè)置了訪問順序的(如下設(shè)置),所以 HashMap 的訪問速度是要比 LinkedHashMap 快的漾狼。
// accessOrder 設(shè)置為 true重慢,表示是訪問順序模式
Map<T, Y> cache = new LinkedHashMap<>(100, 0.75f, true);
(2)防止內(nèi)存泄漏
ActiveResources 中的 HashMap 是弱引用維護(hù)的,而 LruCache 中的 LinkedHashMap 用的是強(qiáng)引用逊躁。因?yàn)槿跻脤?duì)象會(huì)隨時(shí)被 gc 回收似踱,所以可以防止內(nèi)存泄漏。這里列舉下各種引用的區(qū)別:
- 強(qiáng)引用:直接的對(duì)象引用志衣。
- 軟引用:當(dāng)一個(gè)對(duì)象只有軟引用存在時(shí)屯援,系統(tǒng)內(nèi)存不足時(shí)此對(duì)象會(huì)被 gc 回收。
- 弱引用:當(dāng)一個(gè)對(duì)象只有弱引用存在時(shí), 此對(duì)象會(huì)隨時(shí)被 gc 回收念脯。
五狞洋、磁盤緩存
5.1 磁盤緩存策略
前面說了禁用緩存只需要如下設(shè)置即可:
Glide.with(this).load(url).diskCacheStrategy(DiskCacheStrategy.NONE).into(imageView);
上面的 DiskCacheStrategy 封裝的是磁盤緩存策略,一共有如下幾種策略:
- ALL:既緩存原始圖片绿店,也緩存轉(zhuǎn)換過后的圖片吉懊。
- NONE:不緩存任何內(nèi)容。
- DATA:只緩存原始圖片假勿。
- RESOURCE:只緩存轉(zhuǎn)換過后的圖片借嗽。
- AUTOMATIC:默認(rèn)策略,它會(huì)嘗試對(duì)本地和遠(yuǎn)程圖片使用最佳的策略转培。如果是遠(yuǎn)程圖片恶导,則只緩存原始圖片;如果是本地圖片浸须,那么只緩存轉(zhuǎn)換過后的圖片惨寿。
其實(shí) 5 種策略總結(jié)起來(lái)對(duì)應(yīng)的就是文章開頭說的后兩級(jí)緩存,即資源類型(Resource)與 數(shù)據(jù)來(lái)源(Data)删窒,下面通過源碼來(lái)分析下它們是在哪里獲取裂垦、存儲(chǔ)和清理緩存的。
5.2 資源類型(Resource)
該級(jí)緩存只緩存轉(zhuǎn)換過后的圖片肌索,那么我們需要先配置如下策略:
Glide.with(this).load(url).diskCacheStrategy(DiskCacheStrategy.RESOURCE).into(imageView);
通過上一篇文章可知蕉拢,當(dāng)我們從主線程切換到子線程去執(zhí)行請(qǐng)求的時(shí)候用到了磁盤緩存策略,那么我們這里直接從 DecodeJob 任務(wù)的 run() 方法開始分析:
/*DecodeJob*/
@Override
public void run() {
...
try {
// 執(zhí)行
runWrapped();
} catch (CallbackException e) {
throw e;
}
...
}
繼續(xù) runWrapped() 方法:
/*DecodeJob*/
private void runWrapped() {
switch (runReason) {
case INITIALIZE:
// 1. 獲取資源狀態(tài)
stage = getNextStage(Stage.INITIALIZE);
// 2. 根據(jù)資源狀態(tài)獲取資源執(zhí)行器
currentGenerator = getNextGenerator();
// 3. 執(zhí)行
runGenerators();
break;
case SWITCH_TO_SOURCE_SERVICE:
runGenerators();
break;
case DECODE_DATA:
decodeFromRetrievedData();
break;
default:
throw new IllegalStateException("Unrecognized run reason: " + runReason);
}
}
/*DecodeJob*/
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);
}
}
/*DecodeJob*/
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);
}
}
這里會(huì)根據(jù)緩存策略獲取到資源狀態(tài)诚亚,然后再根據(jù)資源狀態(tài)獲取資源執(zhí)行器晕换,最后調(diào)用 runGenerators() 方法:
/*DecodeJob*/
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;
}
}
}
可以看到,該方法中會(huì)調(diào)用當(dāng)前執(zhí)行器的 startNext() 方法亡电,因?yàn)槲覀兣渲玫木彺娌呗允?RESOURCE届巩,所以這里直接看 ResourceCacheGenerator 的 startNext() 方法:
/*ResourceCacheGenerator*/
@Override
public boolean startNext() {
...
while (modelLoaders == null || !hasNextModelLoader()) {
...
//(1)
currentKey =
new ResourceCacheKey(
helper.getArrayPool(),
sourceId,
helper.getSignature(),
helper.getWidth(),
helper.getHeight(),
transformation,
resourceClass,
helper.getOptions());
//(2)
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;
//(3)
loadData.fetcher.loadData(helper.getPriority(), this);
}
}
return started;
}
可以看到,根據(jù)我標(biāo)記的關(guān)注點(diǎn)這里首先構(gòu)建緩存 Key份乒,然后根據(jù)緩存 Key 去獲取緩存文件(獲取轉(zhuǎn)換后的圖片)
恕汇,最后將緩存文件加載成需要的數(shù)據(jù)腕唧。其中 helper.getDiskCache() 為 DiskLruCacheWrapper,內(nèi)部是通過 DiskLruCache 操作的瘾英,也就是說這一級(jí)的磁盤緩存使用的是 LRU 算法實(shí)現(xiàn)的枣接。
因?yàn)楂@取的是緩存文件,所以這里的 loadData.fetcher 實(shí)際為 ByteBufferFileLoader缺谴,繼續(xù)看 ByteBufferFileLoader#(loadData):
/*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;
}
callback.onDataReady(result);
}
這里主要是將緩存文件轉(zhuǎn)換成 ByteBuffer但惶,然后通過 onDataReady() 方法回調(diào)出去,最終回調(diào)到 DecodeJob 的 onDataFetcherReady() 方法中湿蛔,后面的流程就跟上一篇文章差不多了膀曾。
上面是獲取緩存的流程,那么是哪里存儲(chǔ)緩存的呢阳啥?我們可以用反推的方法添谊,剛剛獲取緩存 Key 的時(shí)候用的是 ResourceCacheKey,那么存儲(chǔ)緩存與獲取緩存肯定都是用的 ResourceCacheKey察迟,經(jīng)過查找發(fā)現(xiàn)除了 ResourceCacheGenerator斩狱,只有在 DecodeJob 的 onResourceDecoded() 方法中使用到:
/*DecodeJob*/
<Z> Resource<Z> onResourceDecoded(DataSource dataSource, @NonNull Resource<Z> decoded) {
...
boolean isFromAlternateCacheKey = !decodeHelper.isSourceKey(currentSourceKey);
if (diskCacheStrategy.isResourceCacheable(
isFromAlternateCacheKey, dataSource, encodeStrategy)) {
if (encoder == null) {
throw new Registry.NoResultEncoderAvailableException(transformed.get().getClass());
}
final Key key;
//(1)
switch (encodeStrategy) {
case SOURCE:
key = new DataCacheKey(currentSourceKey, signature);
break;
case TRANSFORMED:
key =
new ResourceCacheKey(
decodeHelper.getArrayPool(),
currentSourceKey,
signature,
width,
height,
appliedTransformation,
resourceSubClass,
options);
break;
default:
throw new IllegalArgumentException("Unknown strategy: " + encodeStrategy);
}
LockedResource<Z> lockedResult = LockedResource.obtain(transformed);
//(2)
deferredEncodeManager.init(key, encoder, lockedResult);
result = lockedResult;
}
return result;
}
內(nèi)部又調(diào)用了 init() 方法:
private static class DeferredEncodeManager<Z> {
private Key key;
private ResourceEncoder<Z> encoder;
private LockedResource<Z> toEncode;
<X> void init(Key key, ResourceEncoder<X> encoder, LockedResource<X> toEncode) {
this.key = key;
this.encoder = (ResourceEncoder<Z>) encoder;
this.toEncode = (LockedResource<Z>) toEncode;
}
void encode(DiskCacheProvider diskCacheProvider, Options options) {
GlideTrace.beginSection("DecodeJob.encode");
try {
//(3)
diskCacheProvider
.getDiskCache()
.put(key, new DataCacheWriter<>(encoder, toEncode, options));
} finally {
toEncode.unlock();
GlideTrace.endSection();
}
}
}
可以看到,根據(jù)我標(biāo)記的關(guān)注點(diǎn)這里首先根據(jù)緩存策略構(gòu)建不同的緩存 Key扎瓶,然后調(diào)用 DeferredEncodeManager 的 init() 方法給變量 key 賦值所踊,然后 key 又在 encode() 方法中使用了,該方法中就做了存儲(chǔ)緩存的操作(存儲(chǔ)轉(zhuǎn)換后的圖片)
概荷。
那么我們現(xiàn)在看看 encode() 方法在哪里被調(diào)用了唄秕岛,點(diǎn)擊發(fā)現(xiàn)只在 DecodeJob 的 notifyEncodeAndRelease() 方法中被調(diào)用了:
/*DecodeJob */
private void notifyEncodeAndRelease(Resource<R> resource, DataSource dataSource) {
if (resource instanceof Initializable) {
((Initializable) resource).initialize();
}
Resource<R> result = resource;
LockedResource<R> lockedResource = null;
if (deferredEncodeManager.hasResourceToEncode()) {
lockedResource = LockedResource.obtain(resource);
result = lockedResource;
}
notifyComplete(result, dataSource);
stage = Stage.ENCODE;
try {
if (deferredEncodeManager.hasResourceToEncode()) {
// 將資源緩存到磁盤
deferredEncodeManager.encode(diskCacheProvider, options);
}
} finally {
if (lockedResource != null) {
lockedResource.unlock();
}
}
// Call onEncodeComplete outside the finally block so that it's not called if the encode process
// throws.
onEncodeComplete();
}
notifyEncodeAndRelease() 方法是我們上一篇文章中講的解碼完成了通知下去的步驟,也就是說第一次加載的時(shí)候在 SourceGenerator#startNext() 中請(qǐng)求到數(shù)據(jù)误证,然后解碼完成瓣蛀,最后再存儲(chǔ)緩存。
上面已經(jīng)實(shí)現(xiàn)了轉(zhuǎn)換后的圖片的獲取雷厂、存儲(chǔ),剩下的清理操作 LRU 算法已經(jīng)自動(dòng)幫我們實(shí)現(xiàn)了叠殷。接下來(lái)繼續(xù)看下原始圖片是怎么獲取改鲫、存儲(chǔ)與清理的。
5.3 數(shù)據(jù)來(lái)源(Data)
該級(jí)緩存只緩存原始圖片林束,那么我們需要先配置如下策略:
Glide.with(this).load(url).diskCacheStrategy(DiskCacheStrategy.DATA).into(imageView);
與資源類型一樣像棘,只不過這里緩存策略換成了 DATA,所以前面就不講了壶冒,我們直接看 DataCacheGenerator 的 startNext() 方法:
/*DataCacheGenerator*/
@Override
public boolean startNext() {
while (modelLoaders == null || !hasNextModelLoader()) {
...
//(1)
Key originalKey = new DataCacheKey(sourceId, helper.getSignature());
//(2)
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;
//(3)
loadData.fetcher.loadData(helper.getPriority(), this);
}
}
return started;
}
可以看到缕题,根據(jù)我標(biāo)記的關(guān)注點(diǎn)這里首先構(gòu)建緩存 Key,然后根據(jù)緩存 Key 去獲取緩存文件(獲取原始圖片)
胖腾,最后將緩存文件加載成需要的數(shù)據(jù)烟零。與資源類型一樣瘪松,這里的 helper.getDiskCache() 也為 DiskLruCacheWrapper,所以這一級(jí)的磁盤緩存使用的也是 LRU 算法實(shí)現(xiàn)的锨阿。
這里獲取的同樣是緩存文件宵睦,所以這里的 loadData.fetcher 也為 ByteBufferFileLoader,最終也是回調(diào)到 DecodeJob 的 onDataFetcherReady() 方法中墅诡。
那么是哪里存儲(chǔ)緩存的呢壳嚎?同樣用反推的方法,但是發(fā)現(xiàn)除了 DataCacheGenerator 還有兩個(gè)地方用到了末早。
第一個(gè)與資源類型一樣是在 DecodeJob#onResourceDecoded():
/*DecodeJob*/
<Z> Resource<Z> onResourceDecoded(DataSource dataSource, @NonNull Resource<Z> decoded) {
...
boolean isFromAlternateCacheKey = !decodeHelper.isSourceKey(currentSourceKey);
//(1)
if (diskCacheStrategy.isResourceCacheable(
isFromAlternateCacheKey, dataSource, encodeStrategy)) {
if (encoder == null) {
throw new Registry.NoResultEncoderAvailableException(transformed.get().getClass());
}
final Key key;
switch (encodeStrategy) {
case SOURCE:
key = new DataCacheKey(currentSourceKey, signature);
break;
case TRANSFORMED:
key =
new ResourceCacheKey(
decodeHelper.getArrayPool(),
currentSourceKey,
signature,
width,
height,
appliedTransformation,
resourceSubClass,
options);
break;
default:
throw new IllegalArgumentException("Unknown strategy: " + encodeStrategy);
}
LockedResource<Z> lockedResult = LockedResource.obtain(transformed);
deferredEncodeManager.init(key, encoder, lockedResult);
result = lockedResult;
}
return result;
}
這里的關(guān)注點(diǎn)(1)做了一個(gè)緩存策略的判斷烟馅,因?yàn)榍懊媾渲玫木彺娌呗允?DATA,所以這里調(diào)用的是 DATA 中的 isResourceCacheable() 方法:
/*DiskCacheStrategy*/
public static final DiskCacheStrategy DATA =
new DiskCacheStrategy() {
@Override
public boolean isDataCacheable(DataSource dataSource) {
return dataSource != DataSource.DATA_DISK_CACHE && dataSource != DataSource.MEMORY_CACHE;
}
// 調(diào)用的是該方法
@Override
public boolean isResourceCacheable(
boolean isFromAlternateCacheKey, DataSource dataSource, EncodeStrategy encodeStrategy) {
return false;
}
@Override
public boolean decodeCachedResource() {
return false;
}
@Override
public boolean decodeCachedData() {
return true;
}
};
可以看到然磷,isResourceCacheable() 方法始終返回 false郑趁,所以上面關(guān)注點(diǎn)(1)是進(jìn)不去的,可以排除样屠。
那我們繼續(xù)看下另一個(gè)地方用到的:
/*SourceGenerator*/
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());
//(1)
originalKey = new DataCacheKey(loadData.sourceKey, helper.getSignature());
//(2)
helper.getDiskCache().put(originalKey, writer);
...
} finally {
loadData.fetcher.cleanup();
}
sourceCacheGenerator =
new DataCacheGenerator(Collections.singletonList(loadData.sourceKey), helper, this);
}
這里首先構(gòu)建緩存 Key穿撮,然后存儲(chǔ)緩存(存儲(chǔ)原始圖片)
。而該方法是在 SourceGenerator#startNext() 中調(diào)用的:
/*SourceGenerator*/
@Override
public boolean startNext() {
//(1)
if (dataToCache != null) {
Object data = dataToCache;
dataToCache = null;
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;
startNextLoad(loadData);
}
}
return started;
}
可以看到痪欲,關(guān)注點(diǎn)(1)中首先判斷緩存不為空才進(jìn)行緩存數(shù)據(jù)的操作悦穿,那我們看下 dataToCache 是哪里被賦值了唄,查找發(fā)現(xiàn)只有在 SourceGenerator#onDataReadyInternal() 中賦值過:
/*SourceGenerator*/
void onDataReadyInternal(LoadData<?> loadData, 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.
// 回調(diào)
cb.reschedule();
} else {
cb.onDataFetcherReady(
loadData.sourceKey,
data,
loadData.fetcher,
loadData.fetcher.getDataSource(),
originalKey);
}
}
可以看到业踢,onDataReadyInternal() 方法又是我們熟悉的栗柒,也就是上一篇文章中加載完數(shù)據(jù)后調(diào)用的。上一篇文章是因?yàn)榻昧司彺嬷伲宰叩氖?else瞬沦。這里配置的緩存策略是 DATA,所以自然走的是 if雇锡。
那么賦值完成逛钻,下一步肯定要用到,我們繼續(xù)跟這里的回調(diào)方法锰提,發(fā)現(xiàn)調(diào)用的是 EngineJob 的 reschedule() 方法:
/*EngineJob*/
@Override
public void reschedule(DecodeJob<?> job) {
getActiveSourceExecutor().execute(job);
}
這里又用線程池執(zhí)行了 DecodeJob曙痘,所以最后又回到了 SourceGenerator 的 startNext() 方法,這時(shí)候 dataToCache 就不是空了立肘,所以就將數(shù)據(jù)緩存起來(lái)了边坤。其實(shí) cacheData() 方法中存儲(chǔ)緩存的時(shí)候還構(gòu)建了一個(gè) DataCacheGenerator,然后存儲(chǔ)完成又執(zhí)行了 DataCacheGenerator#startNext()谅年,這里再?gòu)拇疟P獲取緩存后才將圖片顯示到控件上茧痒,也就是說網(wǎng)絡(luò)請(qǐng)求拿到數(shù)據(jù)后是先緩存數(shù)據(jù),然后再?gòu)拇疟P獲取緩存才顯示到控件上融蹂。
同理旺订,原始圖片的清理操作也是 LRU 算法自動(dòng)幫我們實(shí)現(xiàn)了弄企。
可以看到,這里資源類型(Resource)與數(shù)據(jù)來(lái)源(Data)中存儲(chǔ)緩存的步驟我都是利用緩存 Key 去反推得出數(shù)據(jù)是哪里緩存的耸峭。有時(shí)候適當(dāng)?shù)睦?strong>反推是挺方便的桩蓉,如果你不習(xí)慣利用反推,可以跟著程序走劳闹,也就是網(wǎng)絡(luò)請(qǐng)求到數(shù)據(jù)后再一步步跟著去看是哪里緩存的院究。
六、總結(jié)
通過分析 Glide 的緩存機(jī)制本涕,發(fā)現(xiàn)設(shè)計(jì)的確實(shí)精妙业汰。利用四級(jí)緩存大大提高了圖片的加載效率,磁盤緩存策略也提升了框架的靈活性菩颖,如果讓我們?cè)O(shè)計(jì)一個(gè)圖片加載框架样漆,完全可以將 Glide 中的這些優(yōu)點(diǎn)用上。
參考資料: