上篇我們以加載一張網(wǎng)絡(luò)圖片為例嗽上,講解了Glide加載一張圖片的整體流程。為了更連貫的理解流程我們略過了一些細節(jié)伊约,包括緩存功能姚淆,本篇我們來講解Glide的二級緩存機制。
緩存流程是穿插在Glide整體加載流程中的屡律,所以建議讀這篇為文章之前先了解上篇文章《Glide 系列(三) Glide源碼整體流程梳理》腌逢。
由于Glide的代碼復雜,我們先回顧下Glide緩存相關(guān)的用法超埋,和整體上回顧下Glide的加載流程的主要類搏讶。
Glide緩存功能相關(guān)用法
設(shè)置內(nèi)存緩存開關(guān):
skipMemoryCache(true)
設(shè)置磁盤緩存模式:
diskCacheStrategy(DiskCacheStrategy.NONE)
可以設(shè)置4種模式:
- DiskCacheStrategy.NONE:表示不緩存任何內(nèi)容佳鳖。
- DiskCacheStrategy.SOURCE:表示只緩存原始圖片。
- DiskCacheStrategy.RESULT:表示只緩存轉(zhuǎn)換過后的圖片(默認選項)媒惕。
- DiskCacheStrategy.ALL :表示既緩存原始圖片系吩,也緩存轉(zhuǎn)換過后的圖片。
Glide加載圖片主要類
這里只簡單介紹主干流程吓笙,方便介紹緩存功能是定位源碼淑玫,詳細流程請移步上篇文章。
首先我們想下一個圖片框架面睛,應該包含哪幾個模塊:
- 對外接口:封裝該框架的功能接口絮蒿,一般為單例模式。
- 獲取圖片Request:為每個圖片加載創(chuàng)建一個Request叁鉴,用來準備數(shù)據(jù)土涝、請求圖片資源。
- 異步處理:不管從網(wǎng)絡(luò)還是從本地讀取圖片都是耗時操作幌墓,需要在子線程中完成但壮。
- 網(wǎng)絡(luò)連接 :網(wǎng)絡(luò)獲取圖片的必備模塊。
- 解碼 :獲得圖片流后要解碼得到圖片對象常侣。
上面幾部分是圖片框架所必備的模塊蜡饵,Glide也不例外,接下來看看Glide各模塊對應的主要類:
- Glide:Glide除了是接口封裝類胳施,還負責創(chuàng)建全局使用的工具和組件溯祸。
- GenericRequest:為每個圖片加載創(chuàng)建一個Request,初始化這張圖片的轉(zhuǎn)碼器舞肆、圖片變換器焦辅、圖片展示器target等,當然這個過程實在GenericRequestBuilder的實現(xiàn)類里完成的椿胯。
- Engine:異步處理總調(diào)度器筷登。EnginJob負責線程管理,EngineRunnable是一個異步處理線程哩盲。DecodeJob是真正線程里獲取和處理圖片的地方前方。
- HttpUrlFetcher :獲取網(wǎng)絡(luò)流,使用的是HttpURLConnection种冬。
- Decoder :讀取網(wǎng)絡(luò)流后镣丑,解碼得到Bitmap或者gifResource。因為加載圖片類型不同娱两,這快分支較多,學習Glide初級階段金吗,這塊可以先不再詳細分析十兢。
接下來我們進入主題趣竣,緩存的代碼在上面流程圖里的什么位置?內(nèi)存緩存的操作應該是在異步處理之前旱物,磁盤緩存是耗時操作應該是在異步處理中完成遥缕。
Glide內(nèi)存緩存源碼分析
內(nèi)存存緩存的 讀存都在Engine類中完成。
Glide內(nèi)存緩存的特點
內(nèi)存緩存使用弱引用和LruCache結(jié)合完成的,弱引用來緩存的是正在使用中的圖片宵呛。圖片封裝類Resources內(nèi)部有個計數(shù)器判斷是該圖片否正在使用单匣。
Glide內(nèi)存緩存的流程
- 讀:是先從lruCache取,取不到再從弱引用中缺λ搿户秤;
- 存:內(nèi)存緩存取不到,從網(wǎng)絡(luò)拉取回來先放在弱引用里逮矛,渲染圖片鸡号,圖片對象Resources使用計數(shù)加一;
- 渲染完圖片须鼎,圖片對象Resources使用計數(shù)減一鲸伴,如果計數(shù)為0,圖片緩存從弱引用中刪除晋控,放入lruCache緩存汞窗。
具體看源碼:
上篇提到,Engine在加載流程的中的入口方法是load方法:
public class Engine implements EngineJobListener,
MemoryCache.ResourceRemovedListener,
EngineResource.ResourceListener {
...
public <T, Z, R> LoadStatus load(Key signature, int width, int height, DataFetcher<T> fetcher,
DataLoadProvider<T, Z> loadProvider, Transformation<Z> transformation, ResourceTranscoder<Z, R> transcoder,
Priority priority, boolean isMemoryCacheable, DiskCacheStrategy diskCacheStrategy, ResourceCallback cb) {
Util.assertMainThread();
long startTime = LogTime.getLogTime();
final String id = fetcher.getId();
//生成緩存的key
EngineKey key = keyFactory.buildKey(id, signature, width, height, loadProvider.getCacheDecoder(),
loadProvider.getSourceDecoder(), transformation, loadProvider.getEncoder(),
transcoder, loadProvider.getSourceEncoder());
//從LruCache獲取緩存圖片
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;
}
EngineJob current = jobs.get(key);
if (current != null) {
current.addCallback(cb);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Added to existing load", startTime, key);
}
return new LoadStatus(cb, current);
}
EngineJob engineJob = engineJobFactory.build(key, isMemoryCacheable);
DecodeJob<T, Z, R> decodeJob = new DecodeJob<T, Z, R>(key, width, height, fetcher, loadProvider, transformation,
transcoder, diskCacheProvider, diskCacheStrategy, priority);
EngineRunnable runnable = new EngineRunnable(engineJob, decodeJob, priority);
jobs.put(key, engineJob);
engineJob.addCallback(cb);
engineJob.start(runnable);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Started new load", startTime, key);
}
return new LoadStatus(cb, engineJob);
}
...
}
上面是從內(nèi)存緩存中讀取圖片的主流程:
- 生成緩存的key赡译。
- 從LruCache獲取緩存圖片仲吏。
- LruCache沒取到,從弱引用獲取圖片捶朵。
- 內(nèi)存緩存取不到蜘矢,進入異步處理。
我們具體看取圖片的兩個方法loadFromCache()和loadFromActiveResources()综看。loadFromCache使用的就是LruCache算法品腹,loadFromActiveResources使用的就是弱引用。我們來看一下它們的源碼:
public class Engine implements EngineJobListener,
MemoryCache.ResourceRemovedListener,
EngineResource.ResourceListener {
private final MemoryCache cache;
private final Map<Key, WeakReference<EngineResource<?>>> activeResources;
...
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;
}
private EngineResource<?> getEngineResourceFromCache(Key key) {
Resource<?> cached = cache.remove(key);
final EngineResource result;
if (cached == null) {
result = null;
} else if (cached instanceof EngineResource) {
result = (EngineResource) cached;
} else {
result = new EngineResource(cached, true /*isCacheable*/);
}
return result;
}
private EngineResource<?> loadFromActiveResources(Key key, boolean isMemoryCacheable) {
if (!isMemoryCacheable) {
return null;
}
EngineResource<?> active = null;
WeakReference<EngineResource<?>> activeRef = activeResources.get(key);
if (activeRef != null) {
active = activeRef.get();
if (active != null) {
active.acquire();
} else {
activeResources.remove(key);
}
}
return active;
}
...
}
loadFromCache()方法:
- 首先就判斷isMemoryCacheable是不是false红碑,如果是false的話就直接返回null舞吭。這就是skipMemoryCache()方法設(shè)置的是否內(nèi)存緩存已被禁用。
- 然后調(diào)用getEngineResourceFromCache()方法來獲取緩存析珊。在這個方法中羡鸥,會從中獲取圖片緩存LruResourceCache,LruResourceCache其實使用的就是LruCache算法實現(xiàn)的緩存忠寻。
- 當我們從LruResourceCache中獲取到緩存圖片之后會將它從緩存中移除惧浴,將緩存圖片存儲到activeResources當中。activeResources就是弱引用的HashMap奕剃,用來緩存正在使用中的圖片衷旅。
loadFromActiveResources()方法:
- 就是從activeResources這個activeResources當中取值的捐腿。使用activeResources來緩存正在使用中的圖片,用來保護正在使用中的圖片不會被LruCache算法回收掉柿顶。
這樣我們把從內(nèi)存讀取圖片緩存的流程搞清了茄袖,那是什么時候存儲的呢。想想什么時候合適嘁锯?是不是應該在異步處理獲取到圖片后宪祥,再緩存到內(nèi)存?
參考上一篇家乘,EngineJob 獲取到圖片后 會回調(diào)Engine的onEngineJobComplete()蝗羊。我們來看下做了什么:
public class Engine implements EngineJobListener,
MemoryCache.ResourceRemovedListener,
EngineResource.ResourceListener {
...
@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()));
}
}
jobs.remove(key);
}
...
}
在onEngineJobComplete()方法里將正在加載的圖片放到弱引用緩存。那什么時候放在LruCache里呢烤低?當然是在使用完肘交,那什么時候使用完呢?
那我們來看EngineResource這個類是怎么標記自己是否在被使用的扑馁。EngineResource是用一個acquired變量用來記錄圖片被引用的次數(shù)涯呻,調(diào)用acquire()方法會讓變量加1,調(diào)用release()方法會讓變量減1腻要,代碼如下所示:
class EngineResource<Z> implements Resource<Z> {
private int acquired;
...
void acquire() {
if (isRecycled) {
throw new IllegalStateException("Cannot acquire a recycled resource");
}
if (!Looper.getMainLooper().equals(Looper.myLooper())) {
throw new IllegalThreadStateException("Must call acquire on the main thread");
}
++acquired;
}
void release() {
if (acquired <= 0) {
throw new IllegalStateException("Cannot release a recycled or not yet acquired resource");
}
if (!Looper.getMainLooper().equals(Looper.myLooper())) {
throw new IllegalThreadStateException("Must call release on the main thread");
}
if (--acquired == 0) {
listener.onResourceReleased(key, this);
}
}
}
可以看出當引用計數(shù)acquired變量為0复罐,就是沒有在使用了,然后調(diào)用了 listener.onResourceReleased(key, this);
這個listener就是Engine對象雄家,我們來看下它的onResourceReleased()方法:
public class Engine implements EngineJobListener,
MemoryCache.ResourceRemovedListener,
EngineResource.ResourceListener {
private final MemoryCache cache;
private final Map<Key, WeakReference<EngineResource<?>>> activeResources;
...
@Override
public void onResourceReleased(Key cacheKey, EngineResource resource) {
Util.assertMainThread();
activeResources.remove(cacheKey);
if (resource.isCacheable()) {
cache.put(cacheKey, resource);
} else {
resourceRecycler.recycle(resource);
}
}
...
}
做了三件事:
- 從弱引用刪除圖片緩存
- 是否支持緩存效诅,緩存到LruCache緩存
- 不支持緩存直接調(diào)用垃圾回收,回收圖片
到這里內(nèi)存緩存的讀和存的流程就介紹完了趟济,根據(jù)源碼回頭看看我們之前列的Glide內(nèi)存緩存流程乱投,就清晰很多了。
Glide磁盤緩存源碼分析
Glide磁盤緩存流程
先列下主流程顷编,再具體看代碼
- 讀:先找處理后(result)的圖片戚炫,沒有的話再找原圖。
- 存:先存原圖媳纬,再存處理后的圖双肤。
注意一點:diskCacheStrategy設(shè)置的的緩存模式即影響讀取,也影響存儲钮惠。
具體看源碼:
源碼入口位置是在EngineRunnable的run()方法茅糜,run()方法中調(diào)用到decode()方法,decode()方法的源碼:
private Resource<?> decode() throws Exception {
if (isDecodingFromCache()) {
//從磁盤緩存讀取圖片
return decodeFromCache();
} else {
//從原始位置讀取圖片
return decodeFromSource();
}
}
來看一下decodeFromCache()方法的源碼素挽,如下所示:
private Resource<?> decodeFromCache() throws Exception {
Resource<?> result = null;
try {
//先嘗試讀取處理后的緩存圖
result = decodeJob.decodeResultFromCache();
} catch (Exception e) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Exception decoding result from cache: " + e);
}
}
if (result == null) {
//再嘗試讀取原圖的緩存圖
result = decodeJob.decodeSourceFromCache();
}
return result;
}
處理后的緩存圖和原圖緩存圖對應的是DiskCacheStrategy.RESULT和DiskCacheStrategy.SOURCE這兩個緩存模式蔑赘。
到DecodeJob具體看下這兩個讀取磁盤緩存的方法,decodeResultFromCache()和decodeSourceFromCache():
public Resource<Z> decodeResultFromCache() throws Exception {
if (!diskCacheStrategy.cacheResult()) {
return null;
}
long startTime = LogTime.getLogTime();
Resource<T> transformed = loadFromCache(resultKey);
startTime = LogTime.getLogTime();
Resource<Z> result = transcode(transformed);
return result;
}
public Resource<Z> decodeSourceFromCache() throws Exception {
if (!diskCacheStrategy.cacheSource()) {
return null;
}
long startTime = LogTime.getLogTime();
Resource<T> decoded = loadFromCache(resultKey.getOriginalKey());
return transformEncodeAndTranscode(decoded);
}
這里兩個方法都先判斷了是否是對應的緩存模式,不是則讀取失敗米死。這里我們不關(guān)注transform 和transcode的相關(guān)功能锌历,只分析緩存功能贮庞。兩個緩存方法都調(diào)用到了loadFromCache()方法峦筒,只是傳入的key不同。一個是處理后圖片的key窗慎,一個是原始圖片的key物喷。
繼續(xù)看loadFromCache()方法的源碼:
private Resource<T> loadFromCache(Key key) throws IOException {
File cacheFile = diskCacheProvider.getDiskCache().get(key);
if (cacheFile == null) {
return null;
}
Resource<T> result = null;
try {
result = loadProvider.getCacheDecoder().decode(cacheFile, width, height);
} finally {
if (result == null) {
diskCacheProvider.getDiskCache().delete(key);
}
}
return result;
}
源碼中可以看到我們是從diskCacheProvider.getDiskCache()中讀取的緩存,diskCacheProvider.getDiskCache()獲得的是DiskLruCache工具類的實例遮斥,然后從DiskLruCache獲取緩存峦失。之后的decode不是本篇的關(guān)注點,先不分析术吗。
到這里我們把從磁盤緩存讀取緩存的流程講完了尉辑,那什么時候存入的呢?肯定是在從原始位置獲取圖片后较屿,我們回到decodeFromSource()方法隧魄,一步步看進去:
public Resource<Z> decodeFromSource() throws Exception {
Resource<T> decoded = decodeSource();
return transformEncodeAndTranscode(decoded);
}
decodeSource()顧名思義是用來解析原圖片的,而transformEncodeAndTranscode()則是用來對圖片進行轉(zhuǎn)換和轉(zhuǎn)碼的隘蝎。我們先來看decodeSource()方法:
private Resource<T> decodeSource() throws Exception {
Resource<T> decoded = null;
try {
long startTime = LogTime.getLogTime();
//從網(wǎng)絡(luò)獲取圖片
final A data = fetcher.loadData(priority);
if (isCancelled) {
return null;
}
decoded = decodeFromSourceData(data);
} finally {
fetcher.cleanup();
}
return decoded;
}
private Resource<T> decodeFromSourceData(A data) throws IOException {
final Resource<T> decoded;
//判斷設(shè)置了是否緩存原圖
if (diskCacheStrategy.cacheSource()) {
decoded = cacheAndDecodeSourceData(data);
} else {
long startTime = LogTime.getLogTime();
decoded = loadProvider.getSourceDecoder().decode(data, width, height);
}
return decoded;
}
private Resource<T> cacheAndDecodeSourceData(A data) throws IOException {
long startTime = LogTime.getLogTime();
SourceWriter<A> writer = new SourceWriter<A>(loadProvider.getSourceEncoder(), data);
diskCacheProvider.getDiskCache().put(resultKey.getOriginalKey(), writer);
startTime = LogTime.getLogTime();
Resource<T> result = loadFromCache(resultKey.getOriginalKey());
return result;
}
decodeSource()方法中獲取圖片后购啄,調(diào)用到decodeFromSourceData()方法,然后判斷是否緩存原圖嘱么,是的話就調(diào)用到cacheAndDecodeSourceData(A data)方法狮含。看進去曼振,還是調(diào)用了 diskCacheProvider.getDiskCache()獲取DiskLruCache工具類的實例几迄。然后調(diào)用put方法緩存了原圖。
到此我們緩存了原圖冰评,處理后的圖片是什么時候緩存的映胁?肯定是在圖片處理之后,在transformEncodeAndTranscode()方法中:
private Resource<Z> transformEncodeAndTranscode(Resource<T> decoded) {
long startTime = LogTime.getLogTime();
Resource<T> transformed = transform(decoded);
writeTransformedToCache(transformed);
startTime = LogTime.getLogTime();
Resource<Z> result = transcode(transformed);
return result;
}
private void writeTransformedToCache(Resource<T> transformed) {
if (transformed == null || !diskCacheStrategy.cacheResult()) {
return;
}
long startTime = LogTime.getLogTime();
SourceWriter<Resource<T>> writer = new SourceWriter<Resource<T>>(loadProvider.getEncoder(), transformed);
diskCacheProvider.getDiskCache().put(resultKey, writer);
}
transformEncodeAndTranscode中先對圖片進行了轉(zhuǎn)換集索,然后調(diào)用writeTransformedToCache(transformed);判斷是否緩存處理后的圖片屿愚,是就對處理后的圖片進行了緩存。調(diào)用的同樣是DiskLruCache實例的put()方法务荆,不過這里用的緩存Key是resultKey妆距。
至此圖片磁盤緩存都講解完了,對照源碼看下之前的Glide磁盤緩存流程是不是清晰了很多函匕。
本篇我們主要講解了Glide的二級緩存機制娱据,雖然代碼比較長,但是基本流程比較清晰盅惜,大家通過對流程的梳理中剩,加深對Glide緩存機制的理解忌穿。