Android Glide图片加载框架(三)缓存机制
文章目錄
- 一、緩存簡介
- 二、緩存用法
- 內(nèi)存緩存方式
- 磁盤緩存方式
- 三、緩存KEY
- 四、內(nèi)存緩存
- 內(nèi)存緩存流程
- 五、磁盤緩存
- 磁盤緩存流程
Android Glide圖片加載框架系列文章
Android Glide圖片加載框架(一)基本用法
Android Glide圖片加載框架(二)源碼解析之with()
Android Glide圖片加載框架(二)源碼解析之load()
Android Glide圖片加載框架(二)源碼解析之into()
Android Glide圖片加載框架(三)緩存機制
一、緩存簡介
Glide的緩存設(shè)計可以說是非常先進的,考慮的場景也很周全。在緩存這一功能上,Glide又將它分成了兩個模塊,一個是 內(nèi)存緩存 ,一個是 磁盤緩存 。
這兩個緩存模塊的作用各不相同,
-
內(nèi)存緩存 的主要作用是防止應(yīng)用重復(fù)將圖片數(shù)據(jù)讀取到內(nèi)存當(dāng)中;
-
磁盤緩存 的主要作用是防止應(yīng)用重復(fù)從網(wǎng)絡(luò)或其他地方重復(fù)下載和讀取數(shù)據(jù)。
內(nèi)存緩存 和 硬盤緩存 的相互結(jié)合才構(gòu)成了Glide極佳的圖片緩存效果,那么接下來我們就分別來分析一下這兩種緩存的使用方法以及它們的實現(xiàn)原理。
二、緩存用法
內(nèi)存緩存方式
RequestOptions options = new RequestOptions(); // 禁止內(nèi)存緩存 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);可以設(shè)置5種模式:
-
DiskCacheStrategy.NONE: 表示不緩存任何內(nèi)容
-
DiskCacheStrategy.DATA: 表示只緩存原始圖片
-
DiskCacheStrategy.RESOURCE: 表示只緩存轉(zhuǎn)換過后的圖片
-
DiskCacheStrategy.ALL: 表示既緩存原始圖片,也緩存轉(zhuǎn)換過后的圖片
-
DiskCacheStrategy.AUTOMATIC: 表示讓Glide根據(jù)圖片資源智能地選擇使用哪一種緩存策略(默認選項)
三、緩存KEY
既然是緩存功能,就必然會有用于進行緩存的Key。那么Glide的緩存Key是怎么生成的呢?我不得不說,Glide的緩存Key生成規(guī)則非常繁瑣,決定緩存Key的參數(shù)竟然有8個之多。不過繁瑣歸繁瑣,至少邏輯還是比較簡單的,我們先來看一下Glide緩存Key的生成邏輯。
生成緩存Key的代碼在 Engine 類的 load() 方法當(dāng)中,這部分代碼我們在上一篇文章當(dāng)中已經(jīng)分析過了,只不過當(dāng)時忽略了緩存相關(guān)的內(nèi)容,那么我們現(xiàn)在重新來看一下:
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的所有參數(shù)都相同的情況下才認為是同一個 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;}... }四、內(nèi)存緩存
默認情況下,Glide自動就是開啟內(nèi)存緩存的 。也就是說,當(dāng)我們使用Glide加載了一張圖片之后,這張圖片就會被緩存到內(nèi)存當(dāng)中,只要在它還沒從內(nèi)存中被清除之前,下次使用Glide再加載這張圖片都會直接從內(nèi)存當(dāng)中讀取,而不用重新從網(wǎng)絡(luò)或硬盤上讀取了,這樣無疑就可以大幅度提升圖片的加載效率。比方說你在一個RecyclerView當(dāng)中反復(fù)上下滑動,RecyclerView中只要是Glide加載過的圖片都可以直接從內(nèi)存當(dāng)中迅速讀取并展示出來,從而大大提升了用戶體驗。
而Glide最為人性化的是,你甚至不需要編寫任何額外的代碼就能自動享受到這個極為便利的內(nèi)存緩存功能,因為Glide默認就已經(jīng)將它開啟了。
那么既然已經(jīng)默認開啟了這個功能,還有什么可講的用法呢?只有一點,如果你有什么特殊的原因需要禁用內(nèi)存緩存功能,Glide對此提供了接口:
RequestOptions options = new RequestOptions(); options.skipMemoryCache(true);Glide.with(this).load(url).apply(options).into(img);可以看到,只需要調(diào)用skipMemoryCache()方法并傳入true,就表示禁用掉Glide的內(nèi)存緩存功能。
接下來就讓我們就通過閱讀源碼來分析一下Glide的內(nèi)存緩存功能是如何實現(xiàn)的。
內(nèi)存緩存使用弱引用和LruCache算法結(jié)合完成的,弱引用來緩存的是正在使用中的圖片。圖片封裝類Resources內(nèi)部有個計數(shù)器判斷是該圖片否正在使用。
內(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 <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);}... }上面是從內(nèi)存緩存中讀取圖片的主流程:
-
生成緩存的key。
-
從弱引用獲取圖片。
-
弱引用沒取到,在從LruCache獲取緩存圖片。
-
內(nèi)存緩存取不到,進入異步處理。
我們具體看取圖片的兩個方法 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() 方法設(shè)置的是否內(nèi)存緩存已被禁用。
-
然后從 activeResources 當(dāng)中取值,使用activeResources來緩存正在使用中的圖片,用來保護正在使用中的圖片不會被LruCache算法回收掉。
loadFromCache() 方法:
-
首先就判斷 isMemoryCacheable 是不是 false ,如果是false的話就直接返回null。這就是 skipMemoryCache() 方法設(shè)置的是否內(nèi)存緩存已被禁用。
-
然后調(diào)用 getEngineResourceFromCache() 方法來獲取緩存。在這個方法中,會從中獲取圖片緩存 LruResourceCache ,LruResourceCache其實使用的就是LruCache算法實現(xiàn)的緩存。
-
當(dāng)我們從 LruResourceCache 中獲取到緩存圖片之后會將它從緩存中移除,將緩存圖片存儲到 activeResources 當(dāng)中。activeResources就是弱引用的HashMap,用來緩存正在使用中的圖片。
這樣我們把從內(nèi)存讀取圖片緩存的流程搞清了,那是什么時候存儲的呢。想想什么時候合適?是不是應(yīng)該在異步處理獲取到圖片后,再緩存到內(nèi)存?
EngineJob 獲取到圖片后 會回調(diào)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里呢?當(dāng)然是在使用完,那什么時候使用完呢?
那我們來看 EngineResource 這個類是怎么標(biāo)記自己是否在被使用的。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);}}... }可以看出當(dāng)引用計數(shù)acquired變量為0,就是沒有在使用了,然后調(diào)用了 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緩存
-
不支持緩存直接調(diào)用垃圾回收,回收圖片
到這里內(nèi)存緩存的讀和存的流程就介紹完了,根據(jù)源碼回頭看看我們之前列的Glide內(nèi)存緩存流程,就清晰很多了。
五、磁盤緩存
磁盤緩存流程
-
讀: 先找處理后(result)的圖片,沒有的話再找原圖。
-
存: 先存原圖,再存處理后的圖。
注: diskCacheStrategy設(shè)置的的緩存模式即影響讀取,也影響存儲。
在判斷了兩級內(nèi)存緩存之后,如果拿不到緩存,就會接著創(chuàng)建 EngineJob 和 DecodeJob ,然后接著就會調(diào)用進 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() 中主要還是調(diào)用的 runWrapper() 方法,繼而調(diào)用 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.}... }這里調(diào)用了一個循環(huán)獲取解析生成器 Generator 的方法,而解析生成器有多個實現(xiàn)類:ResourcesCacheGenerator、SourceGenerator、DataCacheGenerator,它們負責(zé)各種硬盤緩存策略下的緩存管理,所以這里關(guān)鍵的條件在于 currentGenerator.startNext() 循環(huán)獲取每個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;}... }這里通過一個資源的關(guān)鍵信息生成key,然后調(diào)用 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();}... }可以看到最終是調(diào)用了 DiskCacheProvider 接口的 getDiskCache() 方法獲取一個 DiskCache 對象,那么這個D對象又是什么來頭呢?
public interface DiskCache {... }可以看到這是一個用來緩存硬盤數(shù)據(jù)的接口,那么它的實現(xiàn)就是我們要找的最終目標(biāo):
public class DiskLruCacheWrapper implements DiskCache {...private DiskLruCache diskLruCache;... }里面的就不詳細分析下去了,這里主要維護了一個 DiskLruCache ,Glide就是通過這個來實現(xiàn)硬盤緩存的。
可以看到Glide的硬盤緩存是依靠DiskLruCache來進行緩存的,同樣也是Lru算法。
總結(jié)
以上是生活随笔為你收集整理的Android Glide图片加载框架(三)缓存机制的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 《暗区突围》M45A1手枪介绍 M45A
- 下一篇: Android学习指南