轉(zhuǎn)載請注明出處:
源碼分析glide中三層存儲機制并與常規(guī)三層緩存機制對比
地址:http://www.itdecent.cn/p/dc8fcf7e69bc
目錄

常規(guī)三層緩存機制
三級緩存的流程
強引用->軟引用->硬盤緩存
當我們的APP中想要加載某張圖片時,先去LruCache中尋找圖片,如果LruCache中有,則直接取出來使用,如果LruCache中沒有,則去SoftReference中尋找(軟引用適合當cache,當內(nèi)存吃緊的時候才會被回收。而weakReference在每次system.gc()就會被回收)(當LruCache存儲緊張時,會把最近最少使用的數(shù)據(jù)放到SoftReference中),如果SoftReference中有,則從SoftReference中取出圖片使用,同時將圖片重新放回到LruCache中,如果SoftReference中也沒有圖片,則去硬盤緩存中中尋找,如果有則取出來使用,同時將圖片添加到LruCache中,如果沒有,則連接網(wǎng)絡(luò)從網(wǎng)上下載圖片。圖片下載完成后,將圖片保存到硬盤緩存中,然后放到LruCache中。(參考這里)
強引用
強引用不能是隨便的一個map數(shù)組,因為還要處理最近不用的數(shù)據(jù)。一般用的是LruCache(這是內(nèi)存存儲,以前一直以為這是disk硬盤存儲,囧,那個叫DiskLruCache),里面持有一個LinkedHashMap,需要將持有的對象進行按時間排序,這樣才能實現(xiàn)Lru算法(Least Recently Used),也就是當達到最大的存儲限制時,把最近最少使用的移除。使用Lrucache需要實現(xiàn)的最主要的方法(參考這里):
- entryRemoved(boolean evicted, String key, T oldValue, T newValue)這個作用是,當存儲滿了以后,需要移除一個最近不使用的。也就是這個oldValue,我們可以把這個oldValue存到本地或者變成弱引用?;蛘咧苯觨ldValue==null(因為LruCache只是把最近不使用的那個移出map,并沒有置為null即沒有移除內(nèi)存,這個如果需要的話得自己處理)
- create(String key)這個就是當用戶從內(nèi)存中獲取一個value的時候,沒有找到,可以在這里生成一個,之后會放cache中。不過我們一般會用put進行放置,所以這里不實現(xiàn)也ok。
- int sizeOf(K key, V value)//這個方法要特別注意,跟我們實例化 LruCache 的 maxSize 要呼應,怎么做到呼應呢,比如 maxSize 的大小為緩存的個數(shù),這里就是 return 1就 ok,如果是內(nèi)存的大小,如果5M,這個就不能是個數(shù) 了,這是應該是每個緩存 value 的 size 大小,如果是 Bitmap,這應該是 bitmap.getByteCount();
軟引用
當圖片被LruCache移除的時候,我們需要手動將這張圖片添加到軟引用map(SoftReference)中,也就是在剛才提到的entryRemoved()中做這件事。我們需要在項目中維護一個由SoftReference組成的集合,其中存儲被LruCache移除出來的圖片。軟引用的一個好處是當系統(tǒng)空間緊張的時候,軟引用可以隨時銷毀(當然前提是這個內(nèi)存對象除了軟引用之外沒有其他引用),因此軟引用是不會影響系統(tǒng)運行的,換句話說,如果系統(tǒng)因為某個原因OOM了,那么這個原因肯定不是軟引用引起的。例如:
private Map<String, SoftReference<Bitmap>> cacheMap=new HashMap<>();
@Override // 當有圖片從LruCache中移除時,將其放進軟引用集合中
protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
if (oldValue != null) {
SoftReference<Bitmap> softReference = new SoftReference<Bitmap>(oldValue);
cacheMap.put(key, softReference);
}
glide中三層緩存機制
glide作為一個優(yōu)秀的圖片加載開源庫,使用的就是三級存儲機制。下面就詳細說下(稍微和一般的三層緩存機制不一樣):
三層存儲的機制在Engine中實現(xiàn)的。先說下Engine是什么?Engine這一層負責加載時做管理內(nèi)存緩存的邏輯。持有MemoryCache、Map<Key, WeakReference<EngineResource<?>>>。通過load()來加載圖片,加載前后會做內(nèi)存存儲的邏輯。如果內(nèi)存緩存中沒有,那么才會使用EngineJob這一層來進行異步獲取硬盤資源或網(wǎng)絡(luò)資源。EngineJob類似一個異步線程或observable。Engine是一個全局唯一的,通過Glide.getEngine()來獲取。
那么
在Engine類的load()方法前面有這么一段注釋:
<p>
* The flow for any request is as follows:
* <ul>
* <li>Check the memory cache and provide the cached resource if present</li>
* <li>Check the current set of actively used resources and return the active resource if present</li>
* <li>Check the current set of in progress loads and add the cb to the in progress load if present</li>
* <li>Start a new load</li>
* </ul>
* </p>
*
Active resources are those that have been provided to at least one request and have not yet been released.
* Once all consumers of a resource have released that resource, the resource then goes to cache. If the
* resource is ever returned to a new consumer from cache, it is re-added to the active resources. If the
* resource is evicted from the cache, its resources are recycled and re-used if possible and the resource is
* discarded. There is no strict requirement that consumers release their resources so active resources are
* held weakly.
active resources存在的形式是弱引用:
private final Map<Key, WeakReference<EngineResource<?>>> activeResources;
MemoryCache是強引用的內(nèi)存緩存,其實現(xiàn)類:
public class LruResourceCache extends LruCache<Key, Resource<?>> implements MemoryCache
說到這里,我們具體說下Engine類的load()方法:
final String id = fetcher.getId();
EngineKey key = keyFactory.buildKey(id, signature, width, height, loadProvider.getCacheDecoder(),
loadProvider.getSourceDecoder(), transformation, loadProvider.getEncoder(),
transcoder, loadProvider.getSourceEncoder());
這兩句話是獲取key,獲取key會使用width,height,transform等,注意這里還會使用fetcher.getId()。這個和圖片的url有關(guān),可以自定義,所以可以進行動態(tài)url的存儲。詳細可以看這里.
接下來就是加載內(nèi)存緩存了:
EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);
if (cached != null) {
cb.onResourceReady(cached);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Loaded resource from cache", startTime, key);
}
return null;
}
EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);
if (active != null) {
cb.onResourceReady(active);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Loaded resource from active resources", startTime, key);
}
return null;
}
private EngineResource<?> loadFromCache(Key key, boolean isMemoryCacheable) {
if (!isMemoryCacheable) {
return null;
}
EngineResource<?> cached = getEngineResourceFromCache(key);
if (cached != null) {
cached.acquire();
activeResources.put(key, new ResourceWeakReference(key, cached, getReferenceQueue()));
}
return cached;
}
@SuppressWarnings("unchecked")
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, true /*isCacheable*/);
}
return result;
}
大致獲取緩存圖片的流程是:如果Lruche中有,那么根據(jù)key把對應的EngineResource取出,并從Lruche中刪除。當EngineResource從Lruche刪除時,會馬上回調(diào)一個onResourceRemoved()接口方法,這個方法在Engine中實現(xiàn):
@Override
public void onResourceRemoved(final Resource<?> resource) {
Util.assertMainThread();
resourceRecycler.recycle(resource);
}
在resourceRecycler.recycle(resource);中使用handler將回收的任務給到了主線程,即在主線程對資源進行回收。當然在資源回收時,需要進行判斷,看是不是還有其他地方對資源進行了引用,如果有,那么就不進行回收;沒有沒有,那么再進行回收。看下EngineResource:
@Override
public void recycle() {
if (acquired > 0) {//是否還有引用
throw new IllegalStateException("Cannot recycle a resource while it is still acquired");
}
if (isRecycled) {
throw new IllegalStateException("Cannot recycle a resource that has already been recycled");
}
isRecycled = true;
resource.recycle();
}
從前面的loadFromCache()代碼可以看出,當把資源從Lruche中取出以后,會執(zhí)行:
if (cached != null) {
cached.acquire();
activeResources.put(key, new ResourceWeakReference(key, cached, getReferenceQueue()));
}
資源引用會加1(因為現(xiàn)在的場景是獲取的緩存圖片會被使用,所以資源引用加1),并且把資源放到弱引用map中。所以這種情況下,前面的資源回收并不會執(zhí)行。如果當需求清楚資源時(比如頁面關(guān)閉,請求cancle),那么會調(diào)用EngineResource的release()方法將資源引用減1。如果引用變成0后,會調(diào)用一個onResourceReleased()接口方法,其在Engine中實現(xiàn):
@Override
public void onResourceReleased(Key cacheKey, EngineResource resource) {
Util.assertMainThread();
activeResources.remove(cacheKey);
if (resource.isCacheable()) {
cache.put(cacheKey, resource);
} else {
resourceRecycler.recycle(resource);
}
}
會把資源從activeResources弱引用數(shù)組中清除,然后把資源放到Lruche中,然后將資源進行回收。
當資源異步加載成功后(網(wǎng)絡(luò)/diskLrucache),除了會放到diskLrucache中(網(wǎng)絡(luò)請求),資源還會放到哪里呢?
@SuppressWarnings("unchecked")
@Override
public void onEngineJobComplete(Key key, EngineResource<?> resource) {
Util.assertMainThread();
// A null resource indicates that the load failed, usually due to an exception.
if (resource != null) {
resource.setResourceListener(key, this);
if (resource.isCacheable()) {
activeResources.put(key, new ResourceWeakReference(key, resource, getReferenceQueue()));
}
}
// TODO: should this check that the engine job is still current?
jobs.remove(key);
}
可以看到資源是放到activeResources中了。
我們總結(jié)一下:需要一個圖片資源,假設(shè)Lruche中相應的資源圖片,那么就就返回相應資源,同時從Lruche中清除,放到activeResources中。activeResources map是盛放正在使用的資源,以弱引用的形式存在。同時資源內(nèi)部有被引用的記錄。如果資源沒有引用記錄了,那么再放回Lruche中,同時從activeResources中清除。需要一個圖片資源如果Lruche中沒有,就從activeResources中找,找到后相應資源的引用加1。如果Lruche和activeResources中沒有,那么進行資源異步請求(網(wǎng)絡(luò)/diskLrucache),請求成功后,資源放到diskLrucache和activeResources中。
核心思想就是:使用一個弱引用map activeResources來盛放項目中正在使用的資源。Lruche中不含有正在使用的資源。資源內(nèi)部有個計數(shù)器來顯示自己是不是還有被引用的情況(當然這里說的被項目中使用/引用不包括被Lruche/activeResources引用)。把正在使用的資源和沒有被使用的資源分開有什么好處呢?因為當Lruche需要移除一個緩存時,會調(diào)用resource.recycle()方法。注意到該方法上面注釋寫著只有沒有任何consumer引用該資源的時候才可以調(diào)用這個方法。這也是為什么需要保證Lruche中的緩存資源都不能有外界的引用。那么為什么調(diào)用resource.recycle()方法需要保證該資源沒有任何consumer引用呢?一開始我不是很理解,因為像bitmap.recycle(),即使bitmap還有資源引用,調(diào)用recycle()也沒關(guān)系啊,大不了就是不會被gc罷了。后來發(fā)現(xiàn)glide中resource定義的recycle()和bitmap中的recycle()并不是一回事。glide中resource定義的recycle()要做的事情是把這個不用的資源(假設(shè)是bitmap或drawable)放到bitmapPool中。我們知道bitmapPool是一個bitmap回收再利用的庫,在做transform的時候會從這個bitmapPool中拿一個bitmap進行再利用。這樣就避免了重新創(chuàng)建bitmap,減少了內(nèi)存的開支。而既然bitmapPool中的bitmap會被重復利用,那么肯定要保證回收該資源的時候(即調(diào)用資源的recycle()時),要保證該資源真的沒有外界引用了。這也是為什么glide花費那么多邏輯來保證Lruche中的資源沒有外界引用的原因。