緩存模塊
我在分析 Glide 源碼前將 Glide 的項(xiàng)目 clone 到了本地第煮,閱讀時(shí)添加了很多注釋以及自己的理解等等害晦,現(xiàn)在已經(jīng)推到了 Github 上稻轨,有興趣的同學(xué)可以看看:
https://github.com/0xZhangKe/Glide-note
緩存模塊涉及到的東西比較多,比較重要李丰,所以需要單獨(dú)用一章節(jié)來(lái)講。
關(guān)于緩存的獲取、數(shù)據(jù)加載相關(guān)的邏輯在 Engine#load 方法中。
先來(lái)看看緩存流程床佳,流程如下圖:
全部的緩存流程大致如上圖所示。
Glide 實(shí)例化時(shí)會(huì)實(shí)例化三個(gè)緩存相關(guān)的類(lèi)以及一個(gè)計(jì)算緩存大小的類(lèi):
//根據(jù)當(dāng)前機(jī)器參數(shù)計(jì)算需要設(shè)置的緩存大小
MemorySizeCalculator calculator = new MemorySizeCalculator(context);
//創(chuàng)建 Bitmap 池
if (bitmapPool == null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
int size = calculator.getBitmapPoolSize();
bitmapPool = new LruBitmapPool(size);
} else {
bitmapPool = new BitmapPoolAdapter();
}
}
//創(chuàng)建內(nèi)存緩存
if (memoryCache == null) {
memoryCache = new LruResourceCache(calculator.getMemoryCacheSize());
}
//創(chuàng)建磁盤(pán)緩存
if (diskCacheFactory == null) {
diskCacheFactory = new InternalCacheDiskCacheFactory(context);
}
除此之外 Engine 中還有一個(gè) ActiveResources 作為第一級(jí)緩存浪感。下面分別來(lái)介紹一下昔头。
ActiveResources
ActiveResources 是第一級(jí)緩存,表示當(dāng)前正在活動(dòng)中的資源影兽。
類(lèi)路徑:
com.bumptech.glide.load.engine.ActiveResources
Engine#load 方法中構(gòu)建好 Key 之后第一件事就是去這個(gè)緩存中獲取資源揭斧,獲取到則直接返回,獲取不到才繼續(xù)從其他緩存中尋找峻堰。
當(dāng)資源加載成功讹开,或者通過(guò)緩存中命中資源后都會(huì)將其放入 ActivityResources 中,資源被釋放時(shí)移除出 ActivityResources 捐名。
由于其中的生命周期較短旦万,所以沒(méi)有大小限制。
ActiveResources 中通過(guò)一個(gè) Map 來(lái)存儲(chǔ)數(shù)據(jù)镶蹋,數(shù)據(jù)保存在一個(gè)虛引用(WeakReference)中成艘。
剛剛說(shuō)的 activeResource 使用一個(gè) Map<Key, WeakReference<EngineResource<?>>> 來(lái)存儲(chǔ)的赏半,此外還有一個(gè)引用隊(duì)列:
ReferenceQueue<EngineResource<?>> resourceReferenceQueue;
每當(dāng)向 activeResource 中添加一個(gè) WeakReference 對(duì)象時(shí)都會(huì)將 resourceReferenceQueue 和這個(gè) WeakReference 關(guān)聯(lián)起來(lái),用來(lái)跟蹤這個(gè) WeakReference 的 gc狰腌,一旦這個(gè)弱引用被 gc 掉除破,就會(huì)將它從 activeResource 中移除,ReferenceQueue 的具體作用可以自行谷歌琼腔,大概就是用來(lái)跟蹤弱引用(或者軟引用瑰枫、虛引用)是否被 gc 的。
那么 ReferenceQueue 具體是在何時(shí)去判斷 WeakReference 是否被 gc 了呢丹莲,Handler 機(jī)制大家應(yīng)該都知道光坝,但不知道大家有沒(méi)有用過(guò) MessageQueue.IdleHandler 這個(gè)東東,可以調(diào)用 MessageQueue#addIdleHandler 添加一個(gè) MessageQueue.IdleHandler 對(duì)象甥材,Handler 會(huì)在線程空閑時(shí)調(diào)用這個(gè)方法盯另。resourceReferenceQueue 在創(chuàng)建時(shí)會(huì)創(chuàng)建一個(gè) Engine#RefQueueIdleHandler 對(duì)象并將其添加到當(dāng)前線程的 MessageQueue 中,ReferenceQueue 會(huì)在 IdleHandler 回調(diào)的方法中去判斷 activeResource 中的 WeakReference 是不是被 gc 了洲赵,如果是鸳惯,則將引用從 activeResource 中移除,代碼如下:
//MessageQueue 中的消息暫時(shí)處理完回調(diào)
@Override
public boolean queueIdle() {
ResourceWeakReference ref = (ResourceWeakReference) queue.poll();
if (ref != null) {
activeResources.remove(ref.key);
}
//返回 true叠萍,表示下次處理完仍然繼續(xù)回調(diào)
return true;
}
MemorySizeCalculator
這個(gè)類(lèi)是用來(lái)計(jì)算 BitmapPool 芝发、ArrayPool 以及 MemoryCache 大小的。
計(jì)算方式如下:
//默認(rèn)為 4MB苛谷,如果是低內(nèi)存設(shè)備則在此基礎(chǔ)上除以二
arrayPoolSize =
isLowMemoryDevice(builder.activityManager)
? builder.arrayPoolSizeBytes / LOW_MEMORY_BYTE_ARRAY_POOL_DIVISOR
: builder.arrayPoolSizeBytes;
//其中會(huì)先獲取當(dāng)前進(jìn)程可使用內(nèi)存大小辅鲸,
//然后通過(guò)判斷是否是否為低內(nèi)存設(shè)備乘以相應(yīng)的系數(shù),
//普通設(shè)備是乘以 0.4腹殿,低內(nèi)存為 0.33独悴,這樣得到的是 Glide 可使用的最大內(nèi)存閾值 maxSize
int maxSize =
getMaxSize(
builder.activityManager, builder.maxSizeMultiplier, builder.lowMemoryMaxSizeMultiplier);
?
int widthPixels = builder.screenDimensions.getWidthPixels();
int heightPixels = builder.screenDimensions.getHeightPixels();
//計(jì)算一張格式為 ARGB_8888 ,大小為屏幕大小的圖片的占用內(nèi)存大小
//BYTES_PER_ARGB_8888_PIXEL 值為 4
int screenSize = widthPixels * heightPixels * BYTES_PER_ARGB_8888_PIXEL;
?
int targetBitmapPoolSize = Math.round(screenSize * builder.bitmapPoolScreens);
?
int targetMemoryCacheSize = Math.round(screenSize * builder.memoryCacheScreens);
//去掉 ArrayPool 占用的內(nèi)存后還剩余的內(nèi)存
int availableSize = maxSize - arrayPoolSize;
?
if (targetMemoryCacheSize + targetBitmapPoolSize <= availableSize) {
//未超出內(nèi)存限制
memoryCacheSize = targetMemoryCacheSize;
bitmapPoolSize = targetBitmapPoolSize;
} else {
//超出內(nèi)存限制
float part = availableSize / (builder.bitmapPoolScreens + builder.memoryCacheScreens);
memoryCacheSize = Math.round(part * builder.memoryCacheScreens);
bitmapPoolSize = Math.round(part * builder.bitmapPoolScreens);
}
直接看上面的注釋即可锣尉。
BitmapPool
Bitmap 是用來(lái)復(fù)用 Bitmap 從而避免重復(fù)創(chuàng)建 Bitmap 而帶來(lái)的內(nèi)存浪費(fèi)刻炒,Glide 通過(guò) SDK 版本不同創(chuàng)建不同的 BitmapPool 實(shí)例,版本低于 Build.VERSION_CODES.HONEYCOMB(11) 實(shí)例為 BitmapPoolAdapter悟耘,其中的方法體幾乎都是空的落蝙,也就是是個(gè)實(shí)例不做任何緩存。
否則實(shí)例為 LruBitmapPool暂幼,先來(lái)看這個(gè)類(lèi)筏勒。
LruBitmapPool
LruBitmapPool 中沒(méi)有做太多的事,主要任務(wù)都交給了 LruPoolStrategy旺嬉,這里只是做一些緩存大小管理管行、封裝、日志記錄等等操作邪媳。
每次調(diào)用 put 緩存數(shù)據(jù)時(shí)都會(huì)調(diào)用 trimToSize 方法判斷已緩存內(nèi)容是否大于設(shè)定的最大內(nèi)存捐顷,如果大于則使用 LruPoolStrategy#removeLast 方法逐步移除荡陷,直到內(nèi)存小于設(shè)定的最大內(nèi)存為止。
LruPoolStrategy 有兩個(gè)實(shí)現(xiàn)類(lèi):SizeConfigStrategy 以及 AttributeStrategy迅涮,根據(jù)系統(tǒng)版本創(chuàng)建不同的實(shí)例废赞,這兩個(gè)差異不大,KITKAT 之后使用的都是 SizeConfigStrategy叮姑,這個(gè)比較重要唉地。
SizeConfigStrategy
SizeConfigStrategy 顧名思義,是通過(guò) Bitmap 的 size 與 Config 來(lái)當(dāng)做 key 緩存 Bitmap传透,Key 也會(huì)通過(guò) KeyPool 來(lái)緩存在一個(gè)隊(duì)列(Queue)中耘沼。
與 AttributeStrategy 相同的是,其中都使用 Glide 內(nèi)部自定義的數(shù)據(jù)結(jié)構(gòu):GroupedLinkedMap 來(lái)存儲(chǔ) Bitmap朱盐。
當(dāng)調(diào)用 put 方法緩存一個(gè) Bitmap 時(shí)會(huì)先通過(guò) Bitmap 的大小以及 Bitmap.Config 創(chuàng)建(從 KeyPool 中獲热亨汀)Key,然后將這個(gè) Key 與 Bitmap 按照鍵值對(duì)的方式存入 GroupedLinkedMap 中兵琳。
此外其中還包含一個(gè) sortedSizes狂秘,這是一個(gè) HashMap,Key 對(duì)應(yīng) put 進(jìn)來(lái)的 Bitmap.Config躯肌,value 對(duì)應(yīng)一個(gè) TreeMap赃绊,TreeMap 中記錄著每一個(gè) size 的 Bitmap 在當(dāng)前緩存中的個(gè)數(shù),即 put 時(shí)加一羡榴,get 時(shí)減一。
TreeMap 是有序的數(shù)據(jù)結(jié)構(gòu)运敢,當(dāng)需要通過(guò) Bitmap 的 size 與 Config 從緩存中獲取一個(gè) Biamp 時(shí)未必會(huì)一定要獲取到 size 完全相同的 Bitmap校仑,由于 TreeMap 的特性,調(diào)用其 ceilingKey 可以獲取到一個(gè)相等或大于當(dāng)前 size 的一個(gè)最小值传惠,用這個(gè) Key 去獲取 Bitmap迄沫,然后重置一下大小即可。
重點(diǎn)看一下 GroupedLinkedMap卦方,這是 Glide 為了 實(shí)現(xiàn) LRU 算法自定義的一個(gè)數(shù)據(jù)結(jié)構(gòu)羊瘩,看名字是已分組的鏈表 Map?看一下下面的圖就明白了:
其中包含三種數(shù)據(jù)結(jié)構(gòu):哈希表(HashMap)盼砍、循環(huán)鏈表以及列表(ArrayList)靶累。
這個(gè)結(jié)構(gòu)其實(shí)類(lèi)似 Java 里提供的 LinkedHashMap 類(lèi)岭妖。
循環(huán)鏈表是通過(guò)內(nèi)部類(lèi) GroupedLinkedMap$LinkedEntry 實(shí)現(xiàn)的,其中除了定義了鏈表結(jié)構(gòu)需要的上下兩個(gè)節(jié)點(diǎn)信息之外還包含著一個(gè) Key 與一個(gè) Values,定義如下:
private static class LinkedEntry<K, V> {
private final K key;
private List<V> values;
LinkedEntry<K, V> next;
LinkedEntry<K, V> prev;
...
}
其實(shí)就是將 HashMap 的 Values 使用鏈表串了起來(lái)勃教,每個(gè) Value 中又存了個(gè) List。
調(diào)用 put 方法時(shí)會(huì)先根據(jù) Key 去這個(gè) Map 中獲取 LinkedEntry熙尉,獲取不到則創(chuàng)建一個(gè),并且加入到鏈表的尾部臀晃,然后將 value (也就是 Bitmap)存入 LinkedEntry 中的 List 中。
所以這里說(shuō)的分組指的是通過(guò) Key 來(lái)對(duì) Bitmap 進(jìn)行分組介劫,對(duì)于同一個(gè) Key(size 與 config 都相同)的 Bitmap 都會(huì)存入同一個(gè) LinkedEntry 中徽惋。
調(diào)用 get 方法獲取 Bitmap 時(shí)會(huì)先通過(guò) Key 去 keyToEntry 中獲取 LinkedEntry 對(duì)象,獲取不到則創(chuàng)建一個(gè)座韵,然后將其加入到鏈表頭部险绘,此時(shí)已經(jīng)有了 LinkedEntry 對(duì)象,調(diào)用 LinkedEntry#removeLast 方法返回并刪除 List 中的最后一個(gè)元素回右。
通過(guò)上面兩步可以看到之所以使用鏈表是為了支持 LRU 算法隆圆,最近使用的 Bitmap 都會(huì)移動(dòng)到鏈表的前端,使用次數(shù)越少就越靠后翔烁,當(dāng)調(diào)用 removeLast 方法時(shí)就直接調(diào)用鏈表最后一個(gè)元素的 removeLast 方法移除元素渺氧。
好了 BitmapPool 大概就這么多內(nèi)容,總結(jié)一下:
- BitmapPool 大小通過(guò) MemorySizeCalculator 設(shè)置蹬屹;
- 使用 LRU 算法維護(hù) BitmapPool 侣背;
- Glide 會(huì)根據(jù) Bitmap 的大小與 Config 生成一個(gè) Key;
- Key 也有自己對(duì)應(yīng)的對(duì)象池慨默,使用 Queue 實(shí)現(xiàn)贩耐;
- 數(shù)據(jù)最終存儲(chǔ)在 GroupedLinkedMap 中;
- GroupedLinkedMap 使用哈希表厦取、循環(huán)鏈表潮太、List 來(lái)存儲(chǔ)數(shù)據(jù)。
MemoryCache
相比較而言內(nèi)存緩存就簡(jiǎn)單多了虾攻,如果從上面說(shuō)的 ActiveResources 中沒(méi)獲取到資源則開(kāi)始從這里尋找铡买。
內(nèi)存緩存同樣使用 LRU 算法,實(shí)現(xiàn)類(lèi)為 LruResourceCache霎箍,這個(gè)類(lèi)沒(méi)幾行代碼奇钞,繼承了 LruCache ,所以著重看一下 LruCache 好了漂坏。
其實(shí) Java 集合里面提供了一個(gè)很好的用來(lái)實(shí)現(xiàn) LRU 算法的數(shù)據(jù)結(jié)構(gòu)景埃,即上面提到過(guò)的 LinkedHashMap。其基于 HashMap 實(shí)現(xiàn)顶别,同時(shí)又將 HashMap 中的 Entity 串成了一個(gè)雙向鏈表谷徙。
LruCache 中就是使用這個(gè)集合來(lái)緩存數(shù)據(jù),其中代碼量也不多筋夏,主要就是在 LinkedHashMap 的基礎(chǔ)上又提供了對(duì)內(nèi)存的管理的幾個(gè)操作蒂胞。
特別地,LruResourceCache 中提供了一個(gè) ResourceRemovedListener 接口条篷,當(dāng)有資源從 MemoryCache 中被移除時(shí)會(huì)回調(diào)其中的方法骗随,Engine 中接收到這個(gè)消息后就會(huì)進(jìn)行 Bitmap 的回收操作蛤织。
磁盤(pán)緩存
緩存路徑默認(rèn)為 Context#getCacheDir() 下面的 image_manager_disk_cache 文件夾,默認(rèn)緩存大小為 250MB鸿染。
磁盤(pán)緩存實(shí)現(xiàn)類(lèi)由 InternalCacheDiskCacheFactory 創(chuàng)建指蚜,最終會(huì)通過(guò)緩存路徑及緩存文件夾最大值創(chuàng)建一個(gè) DiskLruCacheWrapper 對(duì)象。
DiskLruCacheWrapper 實(shí)現(xiàn)了 DiskCache 接口涨椒,接口主要的代碼如下:
File get(Key key);
void put(Key key, Writer writer);
void delete(Key key);
void clear();
可以看到其中提供了作為一個(gè)緩存類(lèi)必須的幾個(gè)方法摊鸡,并且文件以 Key 的形式操作。
SafeKeyGenerator 類(lèi)用來(lái)將 Key 對(duì)象轉(zhuǎn)換為字符串蚕冬,Key 不同的實(shí)現(xiàn)類(lèi)生成 Key 的方式也不同免猾,一般來(lái)說(shuō)會(huì)通過(guò)圖片寬高、加密解碼器囤热、引擎等等生成一個(gè) byte[] 然后再轉(zhuǎn)為字符串猎提,以此來(lái)保證圖片資源的唯一性。
另外旁蔼,在向磁盤(pán)寫(xiě)入文件時(shí)(put 方法)會(huì)使用重入鎖來(lái)同步代碼锨苏,也就是 DiskCacheWriteLocker 類(lèi),其中主要是對(duì) ReentrantLock 的包裝棺聊。
DiskLruCacheWrapper 顧名思義也是一個(gè)包裝類(lèi)伞租,包裝的是 DiskLruCache,那再來(lái)看看這個(gè)類(lèi)限佩。
DiskLruCache
這里考慮一個(gè)問(wèn)題葵诈,磁盤(pán)緩存同樣使用的是 LRU 算法,但文件是存在磁盤(pán)中的祟同,如何在 APP 啟動(dòng)之后準(zhǔn)確的按照使用次數(shù)排序讀取緩存文件呢驯击?
Glide 是使用一個(gè)日志清單文件來(lái)保存這種順序,DiskLruCache 在 APP 第一次安裝時(shí)會(huì)在緩存文件夾下創(chuàng)建一個(gè) journal 日志文件來(lái)記錄圖片的添加耐亏、刪除、讀取等等操作沪斟,后面每次打開(kāi) APP 都會(huì)讀取這個(gè)文件广辰,把其中記錄下來(lái)的緩存文件名讀取到 LinkedHashMap 中,后面每次對(duì)圖片的操作不僅是操作這個(gè) LinkedHashMap 還要記錄在 journal 文件中.
journal 文件內(nèi)容如下圖:
開(kāi)頭的 libcore.io.DiskLruCache 是魔數(shù)主之,用來(lái)標(biāo)識(shí)文件择吊,后面的三個(gè) 1 是版本號(hào) valueCount 等等,再往下就是圖片的操作日志了槽奕。
DIRTY几睛、CLEAN 代表操作類(lèi)型,除了這兩個(gè)還有 REMOVE 以及 READ粤攒,緊接著的一長(zhǎng)串字符串是文件的 Key所森,由上文提到的 SafeKeyGenerator 類(lèi)生成囱持,是由圖片的寬、高焕济、加密解碼器等等生成的 SHA-256 散列碼 后面的數(shù)字是圖片大小纷妆。
根據(jù)這個(gè)字符串就可以在同目錄下找到對(duì)應(yīng)的圖片緩存文件,那么打開(kāi)緩存文件夾即可看到上面日志中記錄的文件:
可以看到日志文件中記錄的緩存文件就在這個(gè)文件夾下面晴弃。
由于涉及到磁盤(pán)緩存的外部排序問(wèn)題掩幢,所以相對(duì)而言磁盤(pán)緩存比較復(fù)雜。
那么 Glide 的緩存模塊至此就結(jié)束了上鞠,主要是 BitmapPool 中的數(shù)據(jù)結(jié)構(gòu)以及磁盤(pán)緩存比較復(fù)雜际邻,其他的倒也不是很復(fù)雜。
關(guān)于 Glide 其它模塊的源碼解析可以看我的上一篇博客:
http://www.reibang.com/p/9bb50924d42a
另外芍阎,我在分析 Glide 源碼前將 Glide 的項(xiàng)目 clone 到了本地世曾,閱讀時(shí)添加了很多注釋以及自己的理解等等,現(xiàn)在已經(jīng)推到了 Github 上能曾,有興趣的同學(xué)可以看看:
https://github.com/0xZhangKe/Glide-note