文章目錄
- 一、緩存簡介
- 二、緩存用法
- 內存緩存方式
- 磁盤緩存方式
- 三、緩存KEY
- 四、內存緩存
- 內存緩存流程
- 五、磁盤緩存
- 磁盤緩存流程
Android Glide圖片加載框架系列文章
Android Glide圖片加載框架(一)基本用法
Android Glide圖片加載框架(二)源碼解析之with()
Android Glide圖片加載框架(二)源碼解析之load()
Android Glide圖片加載框架(二)源碼解析之into()
Android Glide圖片加載框架(三)緩存機制
一、緩存簡介
Glide的緩存設計可以說是非常先進的,考慮的場景也很周全。在緩存這一功能上,Glide又將它分成了兩個模塊,一個是 內存緩存
,一個是 磁盤緩存
。
這兩個緩存模塊的作用各不相同,
-
內存緩存
的主要作用是防止應用重復將圖片數據讀取到內存當中; -
磁盤緩存
的主要作用是防止應用重復從網絡或其他地方重復下載和讀取數據。
內存緩存
和 硬盤緩存
的相互結合才構成了Glide極佳的圖片緩存效果,那么接下來我們就分別來分析一下這兩種緩存的使用方法以及它們的實現原理。
二、緩存用法
內存緩存方式
RequestOptions options = new RequestOptions();
// 禁止內存緩存
options.skipMemoryCache(true);Glide.with(this).load(url).apply(options).into(imageView);
磁盤緩存方式
RequestOptions options = new RequestOptions();
// 磁盤不緩存
options.diskCacheStrategy(DiskCacheStrategy.NONE);Glide.with(this).load(url).apply(options).into(imageView);
可以設置5種模式:
-
DiskCacheStrategy.NONE:
表示不緩存任何內容 -
DiskCacheStrategy.DATA:
表示只緩存原始圖片 -
DiskCacheStrategy.RESOURCE:
表示只緩存轉換過后的圖片 -
DiskCacheStrategy.ALL:
表示既緩存原始圖片,也緩存轉換過后的圖片 -
DiskCacheStrategy.AUTOMATIC:
表示讓Glide根據圖片資源智能地選擇使用哪一種緩存策略(默認選項)
三、緩存KEY
既然是緩存功能,就必然會有用于進行緩存的Key。那么Glide的緩存Key是怎么生成的呢?我不得不說,Glide的緩存Key生成規則非常繁瑣,決定緩存Key的參數竟然有8個之多。不過繁瑣歸繁瑣,至少邏輯還是比較簡單的,我們先來看一下Glide緩存Key的生成邏輯。
生成緩存Key的代碼在 Engine
類的 load()
方法當中,這部分代碼我們在上一篇文章當中已經分析過了,只不過當時忽略了緩存相關的內容,那么我們現在重新來看一下:
public class Engine implements EngineJobListener,MemoryCache.ResourceRemovedListener,EngineResource.ResourceListener {...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) {Util.assertMainThread();long startTime = VERBOSE_IS_LOGGABLE ? LogTime.getLogTime() : 0;EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations,resourceClass, transcodeClass, options);...}...
}
第27行可見,決定緩存Key的條件非常多,即使你用override()方法改變了一下圖片的width或者height,也會生成一個完全不同的緩存Key。
緩存key是一個 EngineKey
對象,該類重寫了 equals()
和 hashCode()
方法,保證只有傳入EngineKey的所有參數都相同的情況下才認為是同一個 EngineKey
對象,代碼如下:
class EngineKey implements Key {...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;}@Overridepublic 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;}...
}
四、內存緩存
默認情況下,Glide自動就是開啟內存緩存的
。也就是說,當我們使用Glide加載了一張圖片之后,這張圖片就會被緩存到內存當中,只要在它還沒從內存中被清除之前,下次使用Glide再加載這張圖片都會直接從內存當中讀取,而不用重新從網絡或硬盤上讀取了,這樣無疑就可以大幅度提升圖片的加載效率。比方說你在一個RecyclerView當中反復上下滑動,RecyclerView中只要是Glide加載過的圖片都可以直接從內存當中迅速讀取并展示出來,從而大大提升了用戶體驗。
而Glide最為人性化的是,你甚至不需要編寫任何額外的代碼就能自動享受到這個極為便利的內存緩存功能,因為Glide默認就已經將它開啟了。
那么既然已經默認開啟了這個功能,還有什么可講的用法呢?只有一點,如果你有什么特殊的原因需要禁用內存緩存功能,Glide對此提供了接口:
RequestOptions options = new RequestOptions();
options.skipMemoryCache(true);Glide.with(this).load(url).apply(options).into(img);
可以看到,只需要調用skipMemoryCache()方法并傳入true,就表示禁用掉Glide的內存緩存功能
。
接下來就讓我們就通過閱讀源碼來分析一下Glide的內存緩存功能是如何實現的。
內存緩存使用弱引用和LruCache算法結合完成的
,弱引用來緩存的是正在使用中的圖片。圖片封裝類Resources內部有個計數器判斷是該圖片否正在使用。
內存緩存流程
-
讀:
是先從弱引用中取,取不到再從lruCache取; -
存:
內存緩存取不到,從網絡拉取回來先放在弱引用里,渲染圖片,圖片對象Resources使用計數加一; -
渲染完圖片:
圖片對象Resources使用計數減一,如果計數為0,圖片緩存從弱引用中刪除,放入lruCache緩存。
上篇提到,Engine
在加載流程的中的入口方法是 load
方法:
public class Engine implements EngineJobListener,MemoryCache.ResourceRemovedListener,EngineResource.ResourceListener {...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) {Util.assertMainThread();long startTime = VERBOSE_IS_LOGGABLE ? LogTime.getLogTime() : 0;// 生成緩存keyEngineKey key = keyFactory.buildKey(model, signature, width, height, transformations,resourceClass, transcodeClass, options);// 從弱引用獲取圖片EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);if (active != null) {cb.onResourceReady(active, DataSource.MEMORY_CACHE);if (VERBOSE_IS_LOGGABLE) {logWithTimeAndKey("Loaded resource from active resources", startTime, key);}return null;}// 從LruCache獲取緩存圖片EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);if (cached != null) {cb.onResourceReady(cached, DataSource.MEMORY_CACHE);if (VERBOSE_IS_LOGGABLE) {logWithTimeAndKey("Loaded resource from cache", startTime, key);}return null;}EngineJob<?> current = jobs.get(key, onlyRetrieveFromCache);if (current != null) {current.addCallback(cb);if (VERBOSE_IS_LOGGABLE) {logWithTimeAndKey("Added to existing load", startTime, key);}return new LoadStatus(cb, current);}EngineJob<R> engineJob =engineJobFactory.build(key,isMemoryCacheable,useUnlimitedSourceExecutorPool,useAnimationPool,onlyRetrieveFromCache);DecodeJob<R> decodeJob =decodeJobFactory.build(glideContext,model,key,signature,width,height,resourceClass,transcodeClass,priority,diskCacheStrategy,transformations,isTransformationRequired,isScaleOnlyOrNoTransform,onlyRetrieveFromCache,options,engineJob);jobs.put(key, engineJob);engineJob.addCallback(cb);engineJob.start(decodeJob);if (VERBOSE_IS_LOGGABLE) {logWithTimeAndKey("Started new load", startTime, key);}return new LoadStatus(cb, engineJob);}...
}
上面是從內存緩存中讀取圖片的主流程:
-
生成緩存的key。
-
從弱引用獲取圖片。
-
弱引用沒取到,在從LruCache獲取緩存圖片。
-
內存緩存取不到,進入異步處理。
我們具體看取圖片的兩個方法 loadFromActiveResources()
和 loadFromCache()
。
-
loadFromActiveResources
使用的就是弱引用。 -
loadFromCache
使用的就是LruCache算法。
我們來看一下它們的源碼:
public class Engine implements EngineJobListener,MemoryCache.ResourceRemovedListener,EngineResource.ResourceListener {...private EngineResource<?> loadFromActiveResources(Key key, boolean isMemoryCacheable) {if (!isMemoryCacheable) {return null;}EngineResource<?> active = activeResources.get(key);if (active != null) {active.acquire();}return active;}private EngineResource<?> loadFromCache(Key key, boolean isMemoryCacheable) {if (!isMemoryCacheable) {return null;}EngineResource<?> cached = getEngineResourceFromCache(key);if (cached != null) {cached.acquire();activeResources.activate(key, cached);}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) {// Save an object allocation if we've cached an EngineResource (the typical case).result = (EngineResource<?>) cached;} else {result = new EngineResource<>(cached, true /*isMemoryCacheable*/, true /*isRecyclable*/);}return result;}...
}
loadFromActiveResources()
方法:
-
首先就判斷
isMemoryCacheable
是不是false
,如果是false的話就直接返回null。這就是skipMemoryCache()
方法設置的是否內存緩存已被禁用。 -
然后從
activeResources
當中取值,使用activeResources來緩存正在使用中的圖片,用來保護正在使用中的圖片不會被LruCache算法回收掉。
loadFromCache()
方法:
-
首先就判斷
isMemoryCacheable
是不是false
,如果是false的話就直接返回null。這就是skipMemoryCache()
方法設置的是否內存緩存已被禁用。 -
然后調用
getEngineResourceFromCache()
方法來獲取緩存。在這個方法中,會從中獲取圖片緩存LruResourceCache
,LruResourceCache其實使用的就是LruCache算法實現的緩存。 -
當我們從
LruResourceCache
中獲取到緩存圖片之后會將它從緩存中移除,將緩存圖片存儲到activeResources
當中。activeResources就是弱引用的HashMap,用來緩存正在使用中的圖片。
這樣我們把從內存讀取圖片緩存的流程搞清了,那是什么時候存儲的呢。想想什么時候合適?是不是應該在異步處理獲取到圖片后,再緩存到內存?
EngineJob
獲取到圖片后 會回調Engine的 onEngineJobComplete()
。我們來看下做了什么:
public class Engine implements EngineJobListener,MemoryCache.ResourceRemovedListener,EngineResource.ResourceListener {...public void onEngineJobComplete(EngineJob<?> engineJob, 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.activate(key, resource);}}jobs.removeIfCurrent(key, engineJob);}...
}
在 onEngineJobComplete()
方法里將正在加載的圖片放到弱引用緩存。那什么時候放在LruCache里呢?當然是在使用完,那什么時候使用完呢?
那我們來看 EngineResource
這個類是怎么標記自己是否在被使用的。EngineResource是用一個acquired變量用來記錄圖片被引用的次數,調用acquire()方法會讓變量加1,調用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);}}...
}
可以看出當引用計數acquired變量為0,就是沒有在使用了,然后調用了 listener.onResourceReleased(key, this);
。
這個 listener
就是 Engine
對象,我們來看下它的 onResourceReleased()
方法:
public class Engine implements EngineJobListener,MemoryCache.ResourceRemovedListener,EngineResource.ResourceListener {...public void onResourceReleased(Key cacheKey, EngineResource<?> resource) {Util.assertMainThread();activeResources.deactivate(cacheKey);if (resource.isCacheable()) {cache.put(cacheKey, resource);} else {resourceRecycler.recycle(resource);}}...
}
做了三件事:
-
從弱引用刪除圖片緩存
-
是否支持緩存,緩存到LruCache緩存
-
不支持緩存直接調用垃圾回收,回收圖片
到這里內存緩存的讀和存的流程就介紹完了,根據源碼回頭看看我們之前列的Glide內存緩存流程,就清晰很多了。
五、磁盤緩存
磁盤緩存流程
-
讀:
先找處理后(result)的圖片,沒有的話再找原圖。 -
存:
先存原圖,再存處理后的圖。
注:
diskCacheStrategy設置的的緩存模式即影響讀取,也影響存儲。
在判斷了兩級內存緩存之后,如果拿不到緩存,就會接著創建 EngineJob
和 DecodeJob
,然后接著就會調用進 DecodeJob
線程的 run()
方法:
class DecodeJob<R> implements DataFetcherGenerator.FetcherReadyCallback,Runnable,Comparable<DecodeJob<?>>,Poolable {...@Overridepublic void run() {// This should be much more fine grained, but since Java's thread pool implementation silently// swallows all otherwise fatal exceptions, this will at least make it obvious to developers// that something is failing.GlideTrace.beginSectionFormat("DecodeJob#run(model=%s)", model);// Methods in the try statement can invalidate currentFetcher, so set a local variable here to// ensure that the fetcher is cleaned up either way.DataFetcher<?> localFetcher = currentFetcher;try {if (isCancelled) {notifyFailed();return;}runWrapped();} catch (Throwable t) {// Catch Throwable and not Exception to handle OOMs. Throwables are swallowed by our// usage of .submit() in GlideExecutor so we're not silently hiding crashes by doing this. We// are however ensuring that our callbacks are always notified when a load fails. Without this// notification, uncaught throwables never notify the corresponding callbacks, which can cause// loads to silently hang forever, a case that's especially bad for users using Futures on// background threads.if (Log.isLoggable(TAG, Log.DEBUG)) {Log.d(TAG, "DecodeJob threw unexpectedly"+ ", isCancelled: " + isCancelled+ ", stage: " + stage, t);}// When we're encoding we've already notified our callback and it isn't safe to do so again.if (stage != Stage.ENCODE) {throwables.add(t);notifyFailed();}if (!isCancelled) {throw t;}} finally {// Keeping track of the fetcher here and calling cleanup is excessively paranoid, we call// close in all cases anyway.if (localFetcher != null) {localFetcher.cleanup();}GlideTrace.endSection();}}private void runWrapped() {switch (runReason) {case INITIALIZE:stage = getNextStage(Stage.INITIALIZE);currentGenerator = getNextGenerator();runGenerators();break;case SWITCH_TO_SOURCE_SERVICE:runGenerators();break;case DECODE_DATA:decodeFromRetrievedData();break;default:throw new IllegalStateException("Unrecognized run reason: " + runReason);}}...
}
run()
中主要還是調用的 runWrapper()
方法,繼而調用 runGenerator()
:
class DecodeJob<R> implements DataFetcherGenerator.FetcherReadyCallback,Runnable,Comparable<DecodeJob<?>>,Poolable {...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;}}// We've run out of stages and generators, give up.if ((stage == Stage.FINISHED || isCancelled) && !isStarted) {notifyFailed();}// Otherwise a generator started a new load and we expect to be called back in// onDataFetcherReady.}...
}
這里調用了一個循環獲取解析生成器 Generator
的方法,而解析生成器有多個實現類:ResourcesCacheGenerator
、SourceGenerator
、DataCacheGenerator
,它們負責各種硬盤緩存策略下的緩存管理,所以這里關鍵的條件在于 currentGenerator.startNext()
循環獲取每個Generator能否獲取到緩存,獲取不到就通過 getNextGenerator()
進行下一種:
class DecodeJob<R> implements DataFetcherGenerator.FetcherReadyCallback,Runnable,Comparable<DecodeJob<?>>,Poolable {...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);}}...
}
所以我們看看 ResourceCacheGenerator.startNext()
,看下它是用什么來緩存的,其中部分代碼如下:
class ResourceCacheGenerator implements DataFetcherGenerator,DataFetcher.DataCallback<Object> {...public boolean startNext() {...while (modelLoaders == null || !hasNextModelLoader()) {...Key sourceId = sourceIds.get(sourceIdIndex);Class<?> resourceClass = resourceClasses.get(resourceClassIndex);Transformation<?> transformation = helper.getTransformation(resourceClass);currentKey =new ResourceCacheKey(// NOPMD AvoidInstantiatingObjectsInLoopshelper.getArrayPool(),sourceId,helper.getSignature(),helper.getWidth(),helper.getHeight(),transformation,resourceClass,helper.getOptions());cacheFile = helper.getDiskCache().get(currentKey);if (cacheFile != null) {sourceKey = sourceId;modelLoaders = helper.getModelLoaders(cacheFile);modelLoaderIndex = 0;}}...return started;}...
}
這里通過一個資源的關鍵信息生成key,然后調用 helper.getDiskCache().get()
,我們跟進去 DiskCache
看看:
final class DecodeHelper<Transcode> {...DiskCache getDiskCache() {return diskCacheProvider.getDiskCache();}...
}
class DecodeJob<R> implements DataFetcherGenerator.FetcherReadyCallback,Runnable,Comparable<DecodeJob<?>>,Poolable {...interface DiskCacheProvider {DiskCache getDiskCache();}...
}
可以看到最終是調用了 DiskCacheProvider
接口的 getDiskCache()
方法獲取一個 DiskCache
對象,那么這個D對象又是什么來頭呢?
public interface DiskCache {...
}
可以看到這是一個用來緩存硬盤數據的接口,那么它的實現就是我們要找的最終目標:
public class DiskLruCacheWrapper implements DiskCache {...private DiskLruCache diskLruCache;...
}
里面的就不詳細分析下去了,這里主要維護了一個 DiskLruCache
,Glide就是通過這個來實現硬盤緩存的。
可以看到Glide的硬盤緩存是依靠DiskLruCache來進行緩存的,同樣也是Lru算法。