在本系列的上一篇文章中,我帶着大家一起閱讀了一遍Glide的源碼,初步了解了這個強大的圖片加載框架的基本執行流程。
不過,上一篇文章只能說是比較粗略地閱讀了Glide整個執行流程方面的源碼,搞明白了Glide的基本工作原理,但並沒有去深入分析每一處的細節(事實上也不可能在一篇文章中深入分析每一處源碼的細節)。那么從本篇文章開始,我們就一篇篇地來針對Glide某一塊功能進行深入地分析,慢慢將Glide中的各項功能進行全面掌握。
今天我們就先從緩存這一塊內容開始入手吧。不過今天文章中的源碼都建在上一篇源碼分析的基礎之上,還沒有看過上一篇文章的朋友,建議先去閱讀 Android圖片加載框架最全解析(二),從源碼的角度理解Glide的執行流程 。
Glide緩存簡介
Glide的緩存設計可以說是非常先進的,考慮的場景也很周全。在緩存這一功能上,Glide又將它分成了兩個模塊,一個是內存緩存,一個是硬盤緩存。
這兩個緩存模塊的作用各不相同,內存緩存的主要作用是防止應用重復將圖片數據讀取到內存當中,而硬盤緩存的主要作用是防止應用重復從網絡或其他地方重復下載和讀取數據。
內存緩存和硬盤緩存的相互結合才構成了Glide極佳的圖片緩存效果,那么接下來我們就分別來分析一下這兩種緩存的使用方法以及它們的實現原理。
緩存Key
既然是緩存功能,就必然會有用於進行緩存的Key。那么Glide的緩存Key是怎么生成的呢?我不得不說,Glide的緩存Key生成規則非常繁瑣,決定緩存Key的參數竟然有10個之多。不過繁瑣歸繁瑣,至少邏輯還是比較簡單的,我們先來看一下Glide緩存Key的生成邏輯。
生成緩存Key的代碼在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(); EngineKey key = keyFactory.buildKey(id, signature, width, height, loadProvider.getCacheDecoder(), loadProvider.getSourceDecoder(), transformation, loadProvider.getEncoder(), transcoder, loadProvider.getSourceEncoder()); ... } ... }
可以看到,這里在第11行調用了fetcher.getId()方法獲得了一個id字符串,這個字符串也就是我們要加載的圖片的唯一標識,比如說如果是一張網絡上的圖片的話,那么這個id就是這張圖片的url地址。
接下來在第12行,將這個id連同着signature、width、height等等10個參數一起傳入到EngineKeyFactory的buildKey()方法當中,從而構建出了一個EngineKey對象,這個EngineKey也就是Glide中的緩存Key了。
可見,決定緩存Key的條件非常多,即使你用override()方法改變了一下圖片的width或者height,也會生成一個完全不同的緩存Key。
EngineKey類的源碼大家有興趣可以自己去看一下,其實主要就是重寫了equals()和hashCode()方法,保證只有傳入EngineKey的所有參數都相同的情況下才認為是同一個EngineKey對象,我就不在這里將源碼貼出來了。
內存緩存
有了緩存Key,接下來就可以開始進行緩存了,那么我們先從內存緩存看起。
首先你要知道,默認情況下,Glide自動就是開啟內存緩存的。也就是說,當我們使用Glide加載了一張圖片之后,這張圖片就會被緩存到內存當中,只要在它還沒從內存中被清除之前,下次使用Glide再加載這張圖片都會直接從內存當中讀取,而不用重新從網絡或硬盤上讀取了,這樣無疑就可以大幅度提升圖片的加載效率。比方說你在一個RecyclerView當中反復上下滑動,RecyclerView中只要是Glide加載過的圖片都可以直接從內存當中迅速讀取並展示出來,從而大大提升了用戶體驗。
而Glide最為人性化的是,你甚至不需要編寫任何額外的代碼就能自動享受到這個極為便利的內存緩存功能,因為Glide默認就已經將它開啟了。
那么既然已經默認開啟了這個功能,還有什么可講的用法呢?只有一點,如果你有什么特殊的原因需要禁用內存緩存功能,Glide對此提供了接口:
Glide.with(this) .load(url) .skipMemoryCache(true) .into(imageView);
可以看到,只需要調用skipMemoryCache()方法並傳入true,就表示禁用掉Glide的內存緩存功能。
沒錯,關於Glide內存緩存的用法就只有這么多,可以說是相當簡單。但是我們不可能只停留在這么簡單的層面上,接下來就讓我們就通過閱讀源碼來分析一下Glide的內存緩存功能是如何實現的。
其實說到內存緩存的實現,非常容易就讓人想到LruCache算法(Least Recently Used),也叫近期最少使用算法。它的主要算法原理就是把最近使用的對象用強引用存儲在LinkedHashMap中,並且把最近最少使用的對象在緩存值達到預設定值之前從內存中移除。LruCache的用法也比較簡單,我在 Android高效加載大圖、多圖解決方案,有效避免程序OOM 這篇文章當中有提到過它的用法,感興趣的朋友可以去參考一下。
那么不必多說,Glide內存緩存的實現自然也是使用的LruCache算法。不過除了LruCache算法之外,Glide還結合了一種弱引用的機制,共同完成了內存緩存功能,下面就讓我們來通過源碼分析一下。
首先回憶一下,在上一篇文章的第二步load()方法中,我們當時分析到了在loadGeneric()方法中會調用Glide.buildStreamModelLoader()方法來獲取一個ModelLoader對象。當時沒有再跟進到這個方法的里面再去分析,那么我們現在來看下它的源碼:
public class Glide { public static <T, Y> ModelLoader<T, Y> buildModelLoader(Class<T> modelClass, Class<Y> resourceClass, Context context) { if (modelClass == null) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Unable to load null model, setting placeholder only"); } return null; } return Glide.get(context).getLoaderFactory().buildModelLoader(modelClass, resourceClass); } public static Glide get(Context context) { if (glide == null) { synchronized (Glide.class) { if (glide == null) { Context applicationContext = context.getApplicationContext(); List<GlideModule> modules = new ManifestParser(applicationContext).parse(); GlideBuilder builder = new GlideBuilder(applicationContext); for (GlideModule module : modules) { module.applyOptions(applicationContext, builder); } glide = builder.createGlide(); for (GlideModule module : modules) { module.registerComponents(applicationContext, glide); } } } } return glide; } ... }
這里我們還是只看關鍵,在第11行去構建ModelLoader對象的時候,先調用了一個Glide.get()方法,而這個方法就是關鍵。我們可以看到,get()方法中實現的是一個單例功能,而創建Glide對象則是在第24行調用GlideBuilder的createGlide()方法來創建的,那么我們跟到這個方法當中:
public class GlideBuilder { ... Glide createGlide() { if (sourceService == null) { final int cores = Math.max(1, Runtime.getRuntime().availableProcessors()); sourceService = new FifoPriorityThreadPoolExecutor(cores); } if (diskCacheService == null) { diskCacheService = new FifoPriorityThreadPoolExecutor(1); } MemorySizeCalculator calculator = new MemorySizeCalculator(context); if (bitmapPool == null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { int size = calculator.getBitmapPoolSize(); bitmapPool = new LruBitmapPool(size); } else { bitmapPool = new BitmapPoolAdapter(); } } if (memoryCache == null) { memoryCache = new LruResourceCache(calculator.getMemoryCacheSize()); } if (diskCacheFactory == null) { diskCacheFactory = new InternalCacheDiskCacheFactory(context); } if (engine == null) { engine = new Engine(memoryCache, diskCacheFactory, diskCacheService, sourceService); } if (decodeFormat == null) { decodeFormat = DecodeFormat.DEFAULT; } return new Glide(engine, memoryCache, bitmapPool, context, decodeFormat); } }
這里也就是構建Glide對象的地方了。那么觀察第22行,你會發現這里new出了一個LruResourceCache,並把它賦值到了memoryCache這個對象上面。你沒有猜錯,這個就是Glide實現內存緩存所使用的LruCache對象了。不過我這里並不打算展開來講LruCache算法的具體實現,如果你感興趣的話可以自己研究一下它的源碼。
現在創建好了LruResourceCache對象只能說是把准備工作做好了,接下來我們就一步步研究Glide中的內存緩存到底是如何實現的。
剛才在Engine的load()方法中我們已經看到了生成緩存Key的代碼,而內存緩存的代碼其實也是在這里實現的,那么我們重新來看一下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(); EngineKey key = keyFactory.buildKey(id, signature, width, height, loadProvider.getCacheDecoder(), loadProvider.getSourceDecoder(), transformation, loadProvider.getEncoder(), transcoder, loadProvider.getSourceEncoder()); 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); } ... }
可以看到,這里在第17行調用了loadFromCache()方法來獲取緩存圖片,如果獲取到就直接調用cb.onResourceReady()方法進行回調。如果沒有獲取到,則會在第26行調用loadFromActiveResources()方法來獲取緩存圖片,獲取到的話也直接進行回調。只有在兩個方法都沒有獲取到緩存的情況下,才會繼續向下執行,從而開啟線程來加載圖片。
也就是說,Glide的圖片加載過程中會調用兩個方法來獲取內存緩存,loadFromCache()和loadFromActiveResources()。這兩個方法中一個使用的就是LruCache算法,另一個使用的就是弱引用。我們來看一下它們的源碼:
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()方法嗎?如果在這個方法中傳入true,那么這里的isMemoryCacheable就會是false,表示內存緩存已被禁用。
我們繼續住下看,接着調用了getEngineResourceFromCache()方法來獲取緩存。在這個方法中,會使用緩存Key來從cache當中取值,而這里的cache對象就是在構建Glide對象時創建的LruResourceCache,那么說明這里其實使用的就是LruCache算法了。
但是呢,觀察第22行,當我們從LruResourceCache中獲取到緩存圖片之后會將它從緩存中移除,然后在第16行將這個緩存圖片存儲到activeResources當中。activeResources就是一個弱引用的HashMap,用來緩存正在使用中的圖片,我們可以看到,loadFromActiveResources()方法就是從activeResources這個HashMap當中取值的。使用activeResources來緩存正在使用中的圖片,可以保護這些圖片不會被LruCache算法回收掉。
好的,從內存緩存中讀取數據的邏輯大概就是這些了。概括一下來說,就是如果能從內存緩存當中讀取到要加載的圖片,那么就直接進行回調,如果讀取不到的話,才會開啟線程執行后面的圖片加載邏輯。
現在我們已經搞明白了內存緩存讀取的原理,接下來的問題就是內存緩存是在哪里寫入的呢?這里我們又要回顧一下上一篇文章中的內容了。還記不記得我們之前分析過,當圖片加載完成之后,會在EngineJob當中通過Handler發送一條消息將執行邏輯切回到主線程當中,從而執行handleResultOnMainThread()方法。那么我們現在重新來看一下這個方法,代碼如下所示:
class EngineJob implements EngineRunnable.EngineRunnableManager {
private final EngineResourceFactory engineResourceFactory; ... private void handleResultOnMainThread() { if (isCancelled) { resource.recycle(); return; } else if (cbs.isEmpty()) { throw new IllegalStateException("Received a resource without any callbacks to notify"); } engineResource = engineResourceFactory.build(resource, isCacheable); hasResource = true; engineResource.acquire(); listener.onEngineJobComplete(key, engineResource); for (ResourceCallback cb : cbs) { if (!isInIgnoredCallbacks(cb)) { engineResource.acquire(); cb.onResourceReady(engineResource); } } engineResource.release(); } static class EngineResourceFactory { public <R> EngineResource<R> build(Resource<R> resource, boolean isMemoryCacheable) { return new EngineResource<R>(resource, isMemoryCacheable); } } ... }
在第13行,這里通過EngineResourceFactory構建出了一個包含圖片資源的EngineResource對象,然后會在第16行將這個對象回調到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); } ... }
現在就非常明顯了,可以看到,在第13行,回調過來的EngineResource被put到了activeResources當中,也就是在這里寫入的緩存。
那么這只是弱引用緩存,還有另外一種LruCache緩存是在哪里寫入的呢?這就要介紹一下EngineResource中的一個引用機制了。觀察剛才的handleResultOnMainThread()方法,在第15行和第19行有調用EngineResource的acquire()方法,在第23行有調用它的release()方法。其實,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的時候,說明圖片正在使用中,也就應該放到activeResources弱引用緩存當中。而經過release()之后,如果acquired變量等於0了,說明圖片已經不再被使用了,那么此時會在第24行調用listener的onResourceReleased()方法來釋放資源,這個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); } } ... }
可以看到,這里首先會將緩存圖片從activeResources中移除,然后再將它put到LruResourceCache當中。這樣也就實現了正在使用中的圖片使用弱引用來進行緩存,不在使用中的圖片使用LruCache來進行緩存的功能。
這就是Glide內存緩存的實現原理。
硬盤緩存
接下來我們開始學習硬盤緩存方面的內容。
不知道你還記不記得,在本系列的第一篇文章中我們就使用過硬盤緩存的功能了。當時為了禁止Glide對圖片進行硬盤緩存而使用了如下代碼:
Glide.with(this) .load(url) .diskCacheStrategy(DiskCacheStrategy.NONE) .into(imageView);
調用diskCacheStrategy()方法並傳入DiskCacheStrategy.NONE,就可以禁用掉Glide的硬盤緩存功能了。
這個diskCacheStrategy()方法基本上就是Glide硬盤緩存功能的一切,它可以接收四種參數:
- DiskCacheStrategy.NONE: 表示不緩存任何內容。
- DiskCacheStrategy.SOURCE: 表示只緩存原始圖片。
- DiskCacheStrategy.RESULT: 表示只緩存轉換過后的圖片(默認選項)。
- DiskCacheStrategy.ALL : 表示既緩存原始圖片,也緩存轉換過后的圖片。
上面四種參數的解釋本身並沒有什么難理解的地方,但是有一個概念大家需要了解,就是當我們使用Glide去加載一張圖片的時候,Glide默認並不會將原始圖片展示出來,而是會對圖片進行壓縮和轉換(我們會在后面學習這方面的內容)。總之就是經過種種一系列操作之后得到的圖片,就叫轉換過后的圖片。而Glide默認情況下在硬盤緩存的就是轉換過后的圖片,我們通過調用diskCacheStrategy()方法則可以改變這一默認行為。
好的,關於Glide硬盤緩存的用法也就只有這么多,那么接下來還是老套路,我們通過閱讀源碼來分析一下,Glide的硬盤緩存功能是如何實現的。
首先,和內存緩存類似,硬盤緩存的實現也是使用的LruCache算法,而且Google還提供了一個現成的工具類DiskLruCache。我之前也專門寫過一篇文章對這個DiskLruCache工具進行了比較全面的分析,感興趣的朋友可以參考一下 Android DiskLruCache完全解析,硬盤緩存的最佳方案 。當然,Glide是使用的自己編寫的DiskLruCache工具類,但是基本的實現原理都是差不多的。
接下來我們看一下Glide是在哪里讀取硬盤緩存的。這里又需要回憶一下上篇文章中的內容了,Glide開啟線程來加載圖片后會執行EngineRunnable的run()方法,run()方法中又會調用一個decode()方法,那么我們重新再來看一下這個decode()方法的源碼:
private Resource<?> decode() throws Exception { if (isDecodingFromCache()) { return decodeFromCache(); } else { return decodeFromSource(); } }
可以看到,這里會分為兩種情況,一種是調用decodeFromCache()方法從硬盤緩存當中讀取圖片,一種是調用decodeFromSource()來讀取原始圖片。默認情況下Glide會優先從緩存當中讀取,只有緩存中不存在要讀取的圖片時,才會去讀取原始圖片。那么我們現在來看一下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; }
可以看到,這里會先去調用DecodeJob的decodeResultFromCache()方法來獲取緩存,如果獲取不到,會再調用decodeSourceFromCache()方法獲取緩存,這兩個方法的區別其實就是DiskCacheStrategy.RESULT和DiskCacheStrategy.SOURCE這兩個參數的區別,相信不需要我再做什么解釋吧。
那么我們來看一下這兩個方法的源碼吧,如下所示:
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); }
可以看到,它們都是調用了loadFromCache()方法從緩存當中讀取數據,如果是decodeResultFromCache()方法就直接將數據解碼並返回,如果是decodeSourceFromCache()方法,還要調用一下transformEncodeAndTranscode()方法先將數據轉換一下再解碼並返回。
然而我們注意到,這兩個方法中在調用loadFromCache()方法時傳入的參數卻不一樣,一個傳入的是resultKey,另外一個卻又調用了resultKey的getOriginalKey()方法。這個其實非常好理解,剛才我們已經解釋過了,Glide的緩存Key是由10個參數共同組成的,包括圖片的width、height等等。但如果我們是緩存的原始圖片,其實並不需要這么多的參數,因為不用對圖片做任何的變化。那么我們來看一下getOriginalKey()方法的源碼:
public Key getOriginalKey() { if (originalKey == null) { originalKey = new OriginalKey(id, signature); } return originalKey; }
可以看到,這里其實就是忽略了絕大部分的參數,只使用了id和signature這兩個參數來構成緩存Key。而signature參數絕大多數情況下都是用不到的,因此基本上可以說就是由id(也就是圖片url)來決定的Original緩存Key。
搞明白了這兩種緩存Key的區別,那么接下來我們看一下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; }
這個方法的邏輯非常簡單,調用getDiskCache()方法獲取到的就是Glide自己編寫的DiskLruCache工具類的實例,然后調用它的get()方法並把緩存Key傳入,就能得到硬盤緩存的文件了。如果文件為空就返回null,如果文件不為空則將它解碼成Resource對象后返回即可。
這樣我們就將硬盤緩存讀取的源碼分析完了,那么硬盤緩存又是在哪里寫入的呢?趁熱打鐵我們趕快繼續分析下去。
剛才已經分析過了,在沒有緩存的情況下,會調用decodeFromSource()方法來讀取原始圖片。那么我們來看下這個方法:
public Resource<Z> decodeFromSource() throws Exception { Resource<T> decoded = decodeSource(); return transformEncodeAndTranscode(decoded); }
這個方法中只有兩行代碼,decodeSource()顧名思義是用來解析原圖片的,而transformEncodeAndTranscode()則是用來對圖片進行轉換和轉碼的。我們先來看decodeSource()方法:
private Resource<T> decodeSource() throws Exception { Resource<T> decoded = null; try { long startTime = LogTime.getLogTime(); 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; 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; }
這里會在第5行先調用fetcher的loadData()方法讀取圖片數據,然后在第9行調用decodeFromSourceData()方法來對圖片進行解碼。接下來會在第18行先判斷是否允許緩存原始圖片,如果允許的話又會調用cacheAndDecodeSourceData()方法。而在這個方法中同樣調用了getDiskCache()方法來獲取DiskLruCache實例,接着調用它的put()方法就可以寫入硬盤緩存了,注意原始圖片的緩存Key是用的resultKey.getOriginalKey()。
好的,原始圖片的緩存寫入就是這么簡單,接下來我們分析一下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); }
這里的邏輯就更加簡單明了了。先是在第3行調用transform()方法來對圖片進行轉換,然后在writeTransformedToCache()方法中將轉換過后的圖片寫入到硬盤緩存中,調用的同樣是DiskLruCache實例的put()方法,不過這里用的緩存Key是resultKey。
這樣我們就將Glide硬盤緩存的實現原理也分析完了。雖然這些源碼看上去如此的復雜,但是經過Glide出色的封裝,使得我們只需要通過skipMemoryCache()和diskCacheStrategy()這兩個方法就可以輕松自如地控制Glide的緩存功能了。
了解了Glide緩存的實現原理之后,接下來我們再來學習一些Glide緩存的高級技巧吧。
高級技巧
雖說Glide將緩存功能高度封裝之后,使得用法變得非常簡單,但同時也帶來了一些問題。
比如之前有一位群里的朋友就跟我說過,他們項目的圖片資源都是存放在七牛雲上面的,而七牛雲為了對圖片資源進行保護,會在圖片url地址的基礎之上再加上一個token參數。也就是說,一張圖片的url地址可能會是如下格式:
http://url.com/image.jpg?token=d9caa6e02c990b0a
而使用Glide加載這張圖片的話,也就會使用這個url地址來組成緩存Key。
但是接下來問題就來了,token作為一個驗證身份的參數並不是一成不變的,很有可能時時刻刻都在變化。而如果token變了,那么圖片的url也就跟着變了,圖片url變了,緩存Key也就跟着變了。結果就造成了,明明是同一張圖片,就因為token不斷在改變,導致Glide的緩存功能完全失效了。
這其實是個挺棘手的問題,而且我相信絕對不僅僅是七牛雲這一個個例,大家在使用Glide的時候很有可能都會遇到這個問題。
那么該如何解決這個問題呢?我們還是從源碼的層面進行分析,首先再來看一下Glide生成緩存Key這部分的代碼:
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(); EngineKey key = keyFactory.buildKey(id, signature, width, height, loadProvider.getCacheDecoder(), loadProvider.getSourceDecoder(), transformation, loadProvider.getEncoder(), transcoder, loadProvider.getSourceEncoder()); ... } ... }
來看一下第11行,剛才已經說過了,這個id其實就是圖片的url地址。那么,這里是通過調用fetcher.getId()方法來獲取的圖片url地址,而我們在上一篇文章中已經知道了,fetcher就是HttpUrlFetcher的實例,我們就來看一下它的getId()方法的源碼吧,如下所示:
public class HttpUrlFetcher implements DataFetcher<InputStream> { private final GlideUrl glideUrl; ... public HttpUrlFetcher(GlideUrl glideUrl) { this(glideUrl, DEFAULT_CONNECTION_FACTORY); } HttpUrlFetcher(GlideUrl glideUrl, HttpUrlConnectionFactory connectionFactory) { this.glideUrl = glideUrl; this.connectionFactory = connectionFactory; } @Override public String getId() { return glideUrl.getCacheKey(); } ... }
可以看到,getId()方法中又調用了GlideUrl的getCacheKey()方法。那么這個GlideUrl對象是從哪里來的呢?其實就是我們在load()方法中傳入的圖片url地址,然后Glide在內部把這個url地址包裝成了一個GlideUrl對象。
很明顯,接下來我們就要看一下GlideUrl的getCacheKey()方法的源碼了,如下所示:
public class GlideUrl { private final URL url; private final String stringUrl; ... public GlideUrl(URL url) { this(url, Headers.DEFAULT); } public GlideUrl(String url) { this(url, Headers.DEFAULT); } public GlideUrl(URL url, Headers headers) { ... this.url = url; stringUrl = null; } public GlideUrl(String url, Headers headers) { ... this.stringUrl = url; this.url = null; } public String getCacheKey() { return stringUrl != null ? stringUrl : url.toString(); } ... }
這里我將代碼稍微進行了一點簡化,這樣看上去更加簡單明了。GlideUrl類的構造函數接收兩種類型的參數,一種是url字符串,一種是URL對象。然后getCacheKey()方法中的判斷邏輯非常簡單,如果傳入的是url字符串,那么就直接返回這個字符串本身,如果傳入的是URL對象,那么就返回這個對象toString()后的結果。
其實看到這里,我相信大家已經猜到解決方案了,因為getCacheKey()方法中的邏輯太直白了,直接就是將圖片的url地址進行返回來作為緩存Key的。那么其實我們只需要重寫這個getCacheKey()方法,加入一些自己的邏輯判斷,就能輕松解決掉剛才的問題了。
創建一個MyGlideUrl繼承自GlideUrl,代碼如下所示:
public class MyGlideUrl extends GlideUrl { private String mUrl; public MyGlideUrl(String url) { super(url); mUrl = url; } @Override public String getCacheKey() { return mUrl.replace(findTokenParam(), ""); } private String findTokenParam() { String tokenParam = ""; int tokenKeyIndex = mUrl.indexOf("?token=") >= 0 ? mUrl.indexOf("?token=") : mUrl.indexOf("&token="); if (tokenKeyIndex != -1) { int nextAndIndex = mUrl.indexOf("&", tokenKeyIndex + 1); if (nextAndIndex != -1) { tokenParam = mUrl.substring(tokenKeyIndex + 1, nextAndIndex + 1); } else { tokenParam = mUrl.substring(tokenKeyIndex); } } return tokenParam; } }
可以看到,這里我們重寫了getCacheKey()方法,在里面加入了一段邏輯用於將圖片url地址中token參數的這一部分移除掉。這樣getCacheKey()方法得到的就是一個沒有token參數的url地址,從而不管token怎么變化,最終Glide的緩存Key都是固定不變的了。
當然,定義好了MyGlideUrl,我們還得使用它才行,將加載圖片的代碼改成如下方式即可:
Glide.with(this) .load(new MyGlideUrl(url)) .into(imageView);
也就是說,我們需要在load()方法中傳入這個自定義的MyGlideUrl對象,而不能再像之前那樣直接傳入url字符串了。不然的話Glide在內部還是會使用原始的GlideUrl類,而不是我們自定義的MyGlideUrl類。
這樣我們就將這個棘手的緩存問題給解決掉了。
好了,關於Glide緩存方面的內容今天就分析到這里,現在我們不光掌握了Glide緩存的基本用法和高級技巧,還了解了它背后的實現原理,又是收獲滿滿的一篇文章啊。下一篇文章當中,我會繼續帶着大家深入分析Glide的其他功能模塊,講一講回調方面的知識,感興趣的朋友請繼續閱讀 Android圖片加載框架最全解析(四),玩轉Glide的回調與監聽 。
