說一下Glide緩存的大概流程
Glide會在開始一個新的圖片請求之前檢查以下多級緩存:
Active Resources: this image displayed in another View right now(該圖片正在被其它view展示)
Memory Cache: this image recently loaded and still in memory(該圖片最近被加載到內(nèi)存中并且還未釋放)
Resource: this image been decoded, transformed, and written to the disk cache before(該圖片已經(jīng)被解碼,轉(zhuǎn)換過,并且寫到了磁盤緩存中)
Data: the data this image was obtained from written to the disk cache before(該圖片的原始數(shù)據(jù)已經(jīng)被寫入磁盤緩存中)
ActiveResources
ActiveResources是第一級緩存。當(dāng)資源加載成功或者通過其它緩存命中,都會加到ActiveResources 中,當(dāng)資源釋放時再移除。ActiveResources 用一個 Map 來保存資源的 WeakReference:
final Map<Key, ResourceWeakReference> activeEngineResources = new HashMap<>();
使用 WeakReference 的原因是防止內(nèi)存泄漏。但由于是 WeakReference,因此隨時有可能被系統(tǒng)GC,因此ActiveResources還有一個 ReferenceQueue 來跟蹤資源被 GC 的情況:
private final ReferenceQueue<EngineResource<?>> resourceReferenceQueue = new ReferenceQueue<>();
每當(dāng) activeEngineResources 中添加一個 WeakReference 都會把它和這個 ReferenceQueue 關(guān)聯(lián)起來,這個體現(xiàn)在 ActiveResources#activate 方法中:
synchronized void activate(Key key, EngineResource<?> resource) {
ResourceWeakReference toPut =
new ResourceWeakReference(
key, resource, resourceReferenceQueue, isActiveResourceRetentionAllowed);
ResourceWeakReference removed = activeEngineResources.put(key, toPut);
if (removed != null) {
removed.reset();
}
}
相關(guān)問題:
1. 具體說一下 ActiveResources 是怎樣利用 ReferenceQueue 去跟蹤 WeakReference 是否被GC 的?
先說一下為什么ReferenceQueue 可以跟蹤 WeakReference 的GC情況。
WeakReference 有一個包含2個參數(shù)的構(gòu)造方法:
/**
* Creates a new weak reference that refers to the given object and is
* registered with the given queue.
*
* @param referent object the new weak reference will refer to
* @param q the queue with which the reference is to be registered,
* or <tt>null</tt> if registration is not required
*/
public WeakReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
其中一個就是 ReferenceQueue。這個 ReferenceQueue 的作用在 WeakReference 這個類頂部的注釋中有描述:
/**
* Weak reference objects, which do not prevent their referents from being
* made finalizable, finalized, and then reclaimed. Weak references are most
* often used to implement canonicalizing mappings.
*
* <p> Suppose that the garbage collector determines at a certain point in time
* that an object is <a href="package-summary.html#reachability">weakly
* reachable</a>. At that time it will atomically clear all weak references to
* that object and all weak references to any other weakly-reachable objects
* from which that object is reachable through a chain of strong and soft
* references. At the same time it will declare all of the formerly
* weakly-reachable objects to be finalizable. At the same time or at some
* later time it will enqueue those newly-cleared weak references that are
* registered with reference queues.
*
* @author Mark Reinhold
* @since 1.2
*/
第二段中提到,在發(fā)生GC的時候,garbage collector 會把該對象的所有 weak reference 清除,而這些被清除的 WeakReference 對象在GC的同時或者稍晚一點的時間點被加入到他們注冊的ReferenceQueue 中,也就是上面那個構(gòu)造函數(shù)中傳入的第二個參數(shù)。我們只需要遍歷 ReferenceQueue 就可以知道哪些WeakReference 已經(jīng)被GC了,然后從 activeEngineResources 中移除這些資源。
那么 ActiveResources 是在什么時機(jī)去檢查ReferenceQueue 的呢?實際上在ActiveResources 對象被創(chuàng)建的時候,會起一個線程專門去檢測是否有資源被GC:
ActiveResources(
boolean isActiveResourceRetentionAllowed, Executor monitorClearedResourcesExecutor) {
this.isActiveResourceRetentionAllowed = isActiveResourceRetentionAllowed;
this.monitorClearedResourcesExecutor = monitorClearedResourcesExecutor;
monitorClearedResourcesExecutor.execute(
new Runnable() {
@Override
public void run() {
cleanReferenceQueue();
}
});
}
@Synthetic
void cleanReferenceQueue() {
while (!isShutdown) {
try {
ResourceWeakReference ref = (ResourceWeakReference) resourceReferenceQueue.remove();
cleanupActiveReference(ref);
// This section for testing only.
DequeuedResourceCallback current = cb;
if (current != null) {
current.onResourceDequeued();
}
// End for testing only.
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
@Synthetic
void cleanupActiveReference(@NonNull ResourceWeakReference ref) {
// Fixes a deadlock where we normally acquire the Engine lock and then the ActiveResources lock
// but reverse that order in this one particular test. This is definitely a bit of a hack...
synchronized (listener) {
synchronized (this) {
activeEngineResources.remove(ref.key);
if (!ref.isCacheable || ref.resource == null) {
return;
}
EngineResource<?> newResource =
new EngineResource<>(ref.resource, /*isCacheable=*/ true, /*isRecyclable=*/ false);
newResource.setResourceListener(ref.key, listener);
listener.onResourceReleased(ref.key, newResource);
}
}
}
這個線程中會調(diào)用 cleanReferenceQueue 方法,在這個方法中又調(diào)用了 cleanupActiveReference,就是在這個方法中,activeEngineResources 刪除已經(jīng)被GC 的資源。
2. Reference 的種類有哪些?各自的使用場景是什么?
LruBitmapPool
/**
* An {@link com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool} implementation that uses an
* {@link com.bumptech.glide.load.engine.bitmap_recycle.LruPoolStrategy} to bucket {@link Bitmap}s
* and then uses an LRU eviction policy to evict {@link android.graphics.Bitmap}s from the least
* recently used bucket in order to keep the pool below a given maximum size limit.
*/
Glide 自己定義了一個 LruBitmapPool 對象來管理緩存的 Bitmap 對象。但是具體實現(xiàn)LRU算法的是 SizeConfigStrategy,它實現(xiàn)了 LruPoolStrategy 接口。
SizeConfigStrategy
/**
* Keys {@link android.graphics.Bitmap Bitmaps} using both
* {@link android.graphics.Bitmap#getAllocationByteCount()} and the
* {@link android.graphics.Bitmap.Config} returned from
* {@link android.graphics.Bitmap#getConfig()}.
*
* <p> Using both the config and the byte size allows us to safely re-use a greater variety of
* {@link android.graphics.Bitmap Bitmaps}, which increases the hit rate of the pool and therefore
* the performance of applications. This class works around #301 by only allowing re-use of
* {@link android.graphics.Bitmap Bitmaps} with a matching number of bytes per pixel. </p>
*/
SizeConfigStrategy 利用一個 GroupedLinkedMap 來存儲 bitmap,這是一個Glide 自定義的類似 LinkedHashMap 的數(shù)據(jù)結(jié)構(gòu):
/**
* Similar to {@link java.util.LinkedHashMap} when access ordered except that it is access ordered
* on groups of bitmaps rather than individual objects. The idea is to be able to find the LRU
* bitmap size, rather than the LRU bitmap object. We can then remove bitmaps from the least
* recently used size of bitmap when we need to reduce our cache size.
*
* For the purposes of the LRU, we count gets for a particular size of bitmap as an access, even if
* no bitmaps of that size are present. We do not count addition or removal of bitmaps as an
* access.
*/
private final GroupedLinkedMap<Key, Bitmap> groupedMap = new GroupedLinkedMap<>();
groupMap 的key的類型是 Key,是通過 Bitmap 的 size 和 config 組合生成的,這也是 SizeConfigStrategy 這個類名的由來。
GroupedLinkedMap 具體實現(xiàn)如下圖:
內(nèi)部實現(xiàn)主要包括3中數(shù)據(jù)結(jié)構(gòu):
- List,用于存儲每個LinkedEntry中的數(shù)據(jù)
- LinkedEntry,本質(zhì)是循環(huán)雙鏈表,用于記錄數(shù)據(jù)的順序
- HashMap,用于快速找到對應(yīng)的 Entry
總結(jié)一下,BitmapPool 需要注意的有以下幾點:
- BitmapPool 大小通過 MemorySizeCalculator 設(shè)置;
- 使用 LRU 算法維護(hù) BitmapPool ;
- Glide 會根據(jù) Bitmap 的 size 和 Config 生成一個 Key;
- Key 也有自己對應(yīng)的對象池,使用 Queue 實現(xiàn);
- 數(shù)據(jù)最終存儲在 GroupedLinkedMap 中;
- GroupedLinkedMap 使用哈希表、循環(huán)鏈表、List 來存儲數(shù)據(jù)。
MemoryCache
在最上面的 Glide 緩存層級中提到過,MemoryCache 中存儲的是最近被加載到內(nèi)存中并且還未被釋放的資源。
MemoryCache 接口的實現(xiàn)類是 LruResourceCache,和 ActiveResources 的緩存策略一樣,也是采用LRU cache。具體實現(xiàn)就不贅述了。
磁盤緩存
磁盤緩存同樣采用LRU策略,它的實現(xiàn)類是 DiskLruCacheWrapper,看名字就知道這是一個包裝類,包裝的是 DiskLruCache。
DiskLruCache
首先想一個問題,磁盤緩存同樣采用LRU,那么Glide怎樣在APP啟動的時候知道資源的使用頻率并給資源排序呢?
其實Glide 的做法是創(chuàng)建一個日志清單文件來保存這個順序。DiskLruCache 在 APP 第一次安裝時會在緩存文件夾下創(chuàng)建一個 journal 日志文件來記錄圖片的添加、刪除、讀取等等操作,后面每次打開 APP 都會讀取這個文件,把其中記錄下來的緩存文件名讀取到 LinkedHashMap 中,后面每次對圖片的操作不僅是操作這個 LinkedHashMap 還要記錄在 journal 文件中。
DiskLruCache 里有很多IO操作,就是因為它除了管理緩存之外還要把對資源的操作記錄到日志清單文件中。
磁盤緩存的策略
Glide中定義的磁盤緩存策略有下面五種:
- DiskCacheStrategy.ALL:原始圖片和轉(zhuǎn)換過的圖片都緩存
- DiskCacheStrategy.RESOURCE:只緩存原始圖片
- DiskCacheStrategy.NONE:不緩存
- DiskCacheStrategy.DATA:只緩存使用過的圖片
- DiskCacheStrategy.AUTOMATIC: 默認(rèn)策略,嘗試自動為本地和遠(yuǎn)程圖片使用最佳的緩存策略。當(dāng)你加載遠(yuǎn)程數(shù)據(jù)(比如,從URL下載)時,AUTOMATIC 策略僅會存儲未被你的加載過程修改過(比如,變換,裁剪–譯者注)的原始數(shù)據(jù),因為下載遠(yuǎn)程數(shù)據(jù)相比調(diào)整磁盤上已經(jīng)存在的數(shù)據(jù)要昂貴得多。對于本地數(shù)據(jù),AUTOMATIC 策略則會僅存儲變換過的縮略圖,因為即使你需要再次生成另一個尺寸或類型的圖片,取回原始數(shù)據(jù)也很容易。