《看完不忘系列》之Glide (樹幹篇)一文對Glide
加載圖片的核心流程作了介紹,細枝篇做爲補充,將對一些具體實現細節進行深刻。本文篇幅略大,你們能夠根據目錄索引到感興趣的章節閱讀~html
源碼基於最新版本4.11.0
,先上一張職責圖預覽下,一家人就要整整齊齊~java
本文約3200字,閱讀大約10分鐘。如個別大圖模糊(官方會壓縮),可前往我的站點閱讀git
經過建立一些類,繼承相關接口,而後打上註解,由apt來處理這些類,從而實現接口擴展。github
註解@GlideModule
用來配置全局參數和註冊定製的能力,在application裏使用AppGlideModule
,在library裏使用LibraryGlideModule
,web
@GlideModule
public class MyAppGlideModule extends AppGlideModule { @Override public boolean isManifestParsingEnabled() { return false;//新版本不須要解析manifest裏的元數據(沒用過老版本,不太懂,按文檔返回false便可) } @Override public void applyOptions(Context context, GlideBuilder builder) { super.applyOptions(context, builder); //全局配置 //builder.setBitmapPool(xxx); //builder.setDefaultRequestOptions(xxx); //... } @Override public void registerComponents(Context context, Glide glide, Registry registry) { super.registerComponents(context, glide, registry); //註冊一些定製的能力,好比擴展新的圖片來源ModelLoader //registry.register(xxx); } } 複製代碼
好比如今的Glide
的Bitmap默認配置是ARGB_8888
,若是項目圖片類型比較單一,不須要透明度通道和高色域,能夠配置全局的RGB_565
減小一半內存。見默認請求選項,算法
@GlideModule
public class MyAppGlideModule extends AppGlideModule { @Override public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) { super.applyOptions(context, builder); builder.setDefaultRequestOptions(new RequestOptions() .format(DecodeFormat.PREFER_RGB_565)); //注:因爲png須要透明度通道,這類圖依舊會採用8888 } } 複製代碼
或者能夠根據設備評分來衡量,普通機型配置RGB_565
(在須要透明度通道的場景局部使用ARGB_8888
),高端機型則能夠直接配置ARGB_8888
,縱享奢華體驗。設計模式
註解@GlideExtension
能夠將一些通用行爲打包起來,擴展一個接口方便業務層調用。好比電商App不少頁面都有商品列表,這些商品圖片的寬高若是是固定的,就能夠包裝起來,緩存
@GlideExtension
public class MyAppExtension { private static final int GOODS_W = 300; //商品圖寬度 private static final int GOODS_H = 400; //商品圖高度 private MyAppExtension() { //私有化構造方法 } @GlideOption public static BaseRequestOptions<?> goods(BaseRequestOptions<?> options) { return options .fitCenter() .override(GOODS_W, GOODS_H) //寬高 .placeholder(R.mipmap.ic_launcher) //商品佔位圖 .error(R.mipmap.ic_launcher); //商品圖加載失敗時 } } 複製代碼
rebuild一下項目,生成類build/generated/ap_generated_sources/debug/out/com/holiday/srccodestudy/glide/GlideOptions.java
,裏面會多出一個方法,網絡
class GlideOptions extends RequestOptions implements Cloneable {
public GlideOptions goods() { return (GlideOptions) MyAppExtension.goods(this); } } 複製代碼
這時,就能夠用goods來直接使用這一組打包好的行爲了,app
//要用GlideApp
GlideApp.with(this).load(url).goods().into(img); 複製代碼
Generated API
比較適合短週期/小型項目,中大型項目每每不會直接裸使用Glide
,會包一箇中間層來進行隔離(禁止業務層用到Glide
的任何類),以便隨時能夠升級替換,這個中間層就能夠根據須要來自行擴展。
Glide.with(context),當context是Activity時,每一個頁面都會被添加一個空fragment,由空fragment持有頁面級別RequestManager
來管理請求,那退出頁面時是如何取消請求的呢?
with經過RequestManagerRetriever
獲取SupportRequestManagerFragment
,
//SupportRequestManagerFragment.java
//建立SupportRequestManagerFragment public SupportRequestManagerFragment() { //建立Lifecycle this(new ActivityFragmentLifecycle()); } //RequestManager.java //建立RequestManager,傳入Lifecycle RequestManager( Glide glide, Lifecycle lifecycle, //... Context context) { //lifecycle添加RequestManager爲觀察者 lifecycle.addListener(this); } //ActivityFragmentLifecycle.java public void addListener(LifecycleListener listener) { //記錄觀察者們 lifecycleListeners.add(listener); } 複製代碼
退出頁面時,
//SupportRequestManagerFragment.java
public void onDestroy() { lifecycle.onDestroy(); } //ActivityFragmentLifecycle.java void onDestroy() { for (LifecycleListener lifecycleListener : Util.getSnapshot(lifecycleListeners)) { lifecycleListener.onDestroy(); } } //RequestManager.java public synchronized void onDestroy() { //各類取消、註銷操做 targetTracker.onDestroy(); for (Target<?> target : targetTracker.getAll()) { clear(target); } targetTracker.clear(); requestTracker.clearRequests(); lifecycle.removeListener(this); lifecycle.removeListener(connectivityMonitor); mainHandler.removeCallbacks(addSelfToLifecycle); glide.unregisterRequestManager(this); } 複製代碼
代碼看起來有點繞,大體以下圖,
內存緩存有兩級,一是處於活躍狀態,正被view使用着的緩存,稱活躍資源
;二是沒被view使用的,就叫他非活躍資源
吧,
讀取內存:
//Engine.java
public <R> LoadStatus load(...){ //獲取內存緩存 memoryResource = loadFromMemory(key, isMemoryCacheable, startTime); } private EngineResource<?> loadFromMemory( EngineKey key, boolean isMemoryCacheable, long startTime) { //活躍資源,從ActiveResources的Map中獲取 //Map<Key, ResourceWeakReference> activeEngineResources,值是弱引用,會手動計數 EngineResource<?> active = loadFromActiveResources(key); if (active != null) { return active; } //非活躍資源,從LruResourceCache獲取,也有手動計數 //返回後,說明這個緩存被view給用上了,非活躍資源則變成活躍 EngineResource<?> cached = loadFromCache(key); if (cached != null) { return cached; } //內存沒有緩存,load就會去請求 return null; } 複製代碼
寫入內存:
//Engine.java
public synchronized void onEngineJobComplete( EngineJob<?> engineJob, Key key, EngineResource<?> resource) { if (resource != null && resource.isMemoryCacheable()) { //簡單理解,就是圖片加載完成,這時寫入活躍資源的 activeResources.activate(key, resource); } } public void onResourceReleased(Key cacheKey, EngineResource<?> resource) { //活躍資源已經沒有被引用了,就移出 activeResources.deactivate(cacheKey); if (resource.isMemoryCacheable()) { //轉入非活躍資源 cache.put(cacheKey, resource); } } 複製代碼
以下圖:
看看緩存目錄/data/data/com.holiday.srccodestudy/cache/image_manager_disk_cache/
,
先看日誌文件journal
,
libcore.io.DiskLruCache //頭部名字 1 //磁盤緩存版本 1 //App版本 1 //每一個entry(日誌條目)存放的文件數,默認爲1,即一個entry對應一個圖片文件,好比下面就有4個entry,即4張圖片 DIRTY 64d4b00d8ce8b0942d53b3048d5cf6aaa7173acd321e17420891bbc35b98629f CLEAN 64d4b00d8ce8b0942d53b3048d5cf6aaa7173acd321e17420891bbc35b98629f 5246 DIRTY 2c23e32bd9b208092b3cbee8db6f1aff5bc11cb0d4ebd092604ee53099beff37 CLEAN 2c23e32bd9b208092b3cbee8db6f1aff5bc11cb0d4ebd092604ee53099beff37 404730 READ 64d4b00d8ce8b0942d53b3048d5cf6aaa7173acd321e17420891bbc35b98629f READ 2c23e32bd9b208092b3cbee8db6f1aff5bc11cb0d4ebd092604ee53099beff37 DIRTY b566e62aa0e2fb8cb219ad3aa7a0ade9a96521526501ccd775d70aa4f6489272 CLEAN b566e62aa0e2fb8cb219ad3aa7a0ade9a96521526501ccd775d70aa4f6489272 9878 READ 2c23e32bd9b208092b3cbee8db6f1aff5bc11cb0d4ebd092604ee53099beff37 READ b566e62aa0e2fb8cb219ad3aa7a0ade9a96521526501ccd775d70aa4f6489272 DIRTY 55f4af9c1020e3272ce8063c351aff3518f3a1c9508f38345eab27686e263a4c CLEAN 55f4af9c1020e3272ce8063c351aff3518f3a1c9508f38345eab27686e263a4c 69284 複製代碼
下半部分是操做記錄,行開頭指操做行爲,DIRTY
表示在編輯(處於髒數據狀態,別讀),CLEAN
(乾淨狀態)表示寫好了,能夠讀了,READ
表示被讀入了,REMOVE
則表示被刪除,中間很長的一串字符就是緩存鍵或文件名字,最後的數字是文件大小,如404730 B=395.2 KB,只有處於CLEAN
狀態纔會寫大小。那麼圖中的文件名是什麼意思,爲啥key的後面還有.0
後綴?由於一個entry
(日誌條目)能夠對應多個圖片,.0
表明entry
的第一張圖片,若是有配置1對多,那就會有.1
、.2
這樣的後綴。選一個.0
文件點擊右鍵,Save as
保存到電腦,改個jpg後綴,就能看圖了。
來到DiskLruCache
類(看名字知道仍是最近最少使用算法
),
//DiskLruCache.java
//有序Map,實現最近最少使用算法 private final LinkedHashMap<String, Entry> lruEntries = new LinkedHashMap<String, Entry>(0, 0.75f, true); //讀取磁盤緩存 public synchronized Value get(String key) throws IOException { //根據key找到entry Entry entry = lruEntries.get(key); if (entry == null) { return null; } //還不能夠讀,返回null if (!entry.readable) { return null; } //追加一行日誌:READ journalWriter.append(READ); journalWriter.append(' '); journalWriter.append(key); journalWriter.append('\n'); //Value就是用來封裝的實體 return new Value(key, entry.sequenceNumber, entry.cleanFiles, entry.lengths); } //寫入磁盤緩存(這裏只是存進內存的Map,真正的寫入在DiskLruCacheWrapper) private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException { Entry entry = lruEntries.get(key); if (entry == null) { entry = new Entry(key); //存進LinkedHashMap lruEntries.put(key, entry); } Editor editor = new Editor(entry); entry.currentEditor = editor; //追加一行日誌:DIRTY journalWriter.append(DIRTY); return editor; } //刪除磁盤緩存 public synchronized boolean remove(String key) throws IOException { Entry entry = lruEntries.get(key); if (entry == null || entry.currentEditor != null) { return false; } //刪除entry對應的圖片文件 for (int i = 0; i < valueCount; i++) { File file = entry.getCleanFile(i); size -= entry.lengths[i]; entry.lengths[i] = 0; } //追加一行日誌:REMOVE journalWriter.append(REMOVE); //從內存Map中移除 lruEntries.remove(key); return true; } //當日志操做數和entry數都達到2000,就清空日誌重寫 private boolean journalRebuildRequired() { final int redundantOpCompactThreshold = 2000; return redundantOpCount >= redundantOpCompactThreshold // && redundantOpCount >= lruEntries.size(); } 複製代碼
那麼讀取和寫入時機在哪呢?咱們反向追蹤一波get
方法,從DiskLruCache
到DiskLruCacheWrapper
的get
,而後再追,發現有兩個類調了get
,分別是DataCacheGenerator
和ResourceCacheGenerator
,前者是原始圖片的緩存,後者是通過downsampled
向下採樣或transformed
轉換過的圖片,在磁盤緩存策略中提到:
目前支持的策略容許你阻止加載過程使用或寫入磁盤緩存,選擇性地僅緩存無修改的原生數據,或僅緩存變換過的縮略圖,或是兼而有之。
默認狀況下,網絡圖片緩存的是原始數據,那咱們繼續跟DataCacheGenerator
,
//DataCacheGenerator.java
public boolean startNext() { while (modelLoaders == null || !hasNextModelLoader()) { sourceIdIndex++; if (sourceIdIndex >= cacheKeys.size()) { return false; } Key sourceId = cacheKeys.get(sourceIdIndex); Key originalKey = new DataCacheKey(sourceId, helper.getSignature()); //獲取磁盤緩存的圖片文件 cacheFile = helper.getDiskCache().get(originalKey); if (cacheFile != null) { this.sourceKey = sourceId; //獲取可以處理File類型的modelLoaders集合, //modelLoader就是圖片加載類型,好比網絡url、本地Uri、文件File都有各自的loader modelLoaders = helper.getModelLoaders(cacheFile); modelLoaderIndex = 0; } } loadData = null; boolean started = false; while (!started && hasNextModelLoader()) { //成功找到ByteBufferFileLoader,能夠處理File ModelLoader<File, ?> modelLoader = modelLoaders.get(modelLoaderIndex++); //傳入磁盤緩存的圖片文件cacheFile loadData = modelLoader.buildLoadData( cacheFile, helper.getWidth(), helper.getHeight(), helper.getOptions()); if (loadData != null && helper.hasLoadPath(loadData.fetcher.getDataClass())) { started = true; loadData.fetcher.loadData(helper.getPriority(), this); } } return started; } 複製代碼
繼續跟modelLoader.buildLoadData
,後邊就是把圖片文件cacheFile封裝成ByteBufferFetcher
,而後調用上邊的loadData.fetcher.loadData
進行回調,就不繼續跟了,startNext
方法在DecodeJob
裏會被調用,樹幹篇中可知他就是圖片加載過程用到的一個Runnable,好了,下面看看緩存寫入時機,反向追蹤edit
方法,
//DiskLruCacheWrapper.java
public void put(Key key, Writer writer) { String safeKey = safeKeyGenerator.getSafeKey(key); writeLocker.acquire(safeKey); try { try { DiskLruCache diskCache = getDiskCache(); Value current = diskCache.get(safeKey); //已經有緩存,結束 if (current != null) { return; } //獲取Editor DiskLruCache.Editor editor = diskCache.edit(safeKey); try { File file = editor.getFile(0); if (writer.write(file)) {//編碼寫入文件 //提交「事務」,追加一行日誌:CLEAN,表示該條目對應的緩存文件已經乾淨可使用了 editor.commit(); } } finally { editor.abortUnlessCommitted(); } } catch (IOException e) { } } finally { writeLocker.release(safeKey); } } 複製代碼
一樣,put
方法也會在DecodeJob
裏被調用,就不往上跟了。
合併內存緩存和磁盤緩存,
Glide
有將Bitmap進行池化,默認是LruBitmapPool
,他會決定怎麼複用Bitmap、什麼時候回收Bitmap、池子上限時清理,也就是說,他全盤接管了Bitmap的處理,若是項目中有在回調方法外持有Bitmap
、手動回收Bitmap
的場景,會發生意料外的crash,詳見資源重用錯誤的徵兆。即,咱們要有這樣的意識,既然使用了Glide
,就不要再關心Bitmap的事情了,全盤交由BitmapPool
管理便可。
發散:所謂池化,就是設計模式中的享元模式,即維護一個有限個數的對象池來實現對象複用,從而避免頻繁的建立銷燬對象。好比Handler消息機制中的
Message.obtain
,就是從消息池(鏈表)裏取出對象來複用,池子的消息總數被限制在MAX_POOL_SIZE=50。Android內的不少實現都是基於Handler(消息驅動)的,池化能減小很大部分的建立銷燬。
鏈路有點長,直接看調用棧,
可見最終走的是native層的nativeDecodeStream
,哈迪就不跟了,對inputstream轉成bitmap感興趣的讀者自行研究啦~
Glide
有以下優點:
ActiveResources
、內存非活躍資源
LruResourceCache
、磁盤緩存
DiskLruCache
ModelLoader
,詳見
編寫定製的ModelLoader
至於缺點吧,暫時還沒想到。本文只列出了哈迪以爲比較精彩的細節,可能還有遺漏的一些點,你們有補充的能夠留下評論,後續我會更新進本文。
本文使用 mdnice 排版