Android | Glide細枝篇

《看完不忘系列》之Glide (樹幹篇)一文對Glide加載圖片的核心流程作了介紹,細枝篇做爲補充,將對一些具體實現細節進行深刻。本文篇幅略大,你們能夠根據目錄索引到感興趣的章節閱讀~html

源碼基於最新版本4.11.0,先上一張職責圖預覽下,一家人就要整整齊齊~java

本文約3200字,閱讀大約10分鐘。如個別大圖模糊(官方會壓縮),可前往我的站點閱讀git

Generated API

經過建立一些類,繼承相關接口,而後打上註解,由apt來處理這些類,從而實現接口擴展。github

全局配置

註解@GlideModule用來配置全局參數和註冊定製的能力,在application裏使用AppGlideModule,在library裏使用LibraryGlideModuleweb

@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的任何類),以便隨時能夠升級替換,這個中間層就能夠根據須要來自行擴展。

空Fragment取消請求

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); } 複製代碼

代碼看起來有點繞,大體以下圖,

Cache緩存

內存

內存緩存有兩級,一是處於活躍狀態,正被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方法,從DiskLruCacheDiskLruCacheWrapperget,而後再追,發現有兩個類調了get,分別是DataCacheGeneratorResourceCacheGenerator,前者是原始圖片的緩存,後者是通過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裏被調用,就不往上跟了。

合併內存緩存和磁盤緩存,

BitmapPool使人詬病

Glide有將Bitmap進行池化,默認是LruBitmapPool,他會決定怎麼複用Bitmap、什麼時候回收Bitmap、池子上限時清理,也就是說,他全盤接管了Bitmap的處理,若是項目中有在回調方法外持有Bitmap手動回收Bitmap的場景,會發生意料外的crash,詳見資源重用錯誤的徵兆。即,咱們要有這樣的意識,既然使用了Glide,就不要再關心Bitmap的事情了,全盤交由BitmapPool管理便可。

發散:所謂池化,就是設計模式中的享元模式,即維護一個有限個數的對象池來實現對象複用,從而避免頻繁的建立銷燬對象。好比Handler消息機制中的Message.obtain,就是從消息池(鏈表)裏取出對象來複用,池子的消息總數被限制在MAX_POOL_SIZE=50。Android內的不少實現都是基於Handler(消息驅動)的,池化能減小很大部分的建立銷燬。

Decoder解碼

鏈路有點長,直接看調用棧,

可見最終走的是native層的nativeDecodeStream,哈迪就不跟了,對inputstream轉成bitmap感興趣的讀者自行研究啦~

總結

Glide有以下優點:

  1. 空Fragment感知頁面生命週期,避免無效請求
  2. 高度可配置,詳見 配置
  3. 三級緩存(網絡層緩存如okhttp就不考慮了):內存活躍資源 ActiveResources、內存非活躍資源 LruResourceCache、磁盤緩存 DiskLruCache
  4. 可定製,引入apt處理註解,打包行爲,擴展接口。(哈迪沒怎麼用,感受有點雞肋,可能之後會真香)
  5. 可擴展,能夠替換網絡層、定製本身的圖片來源 ModelLoader,詳見 編寫定製的ModelLoader
  6. 無侵入,into能夠傳入最簡單的ImageView
  7. 優秀的設計模式運用、應用層優雅的鏈式調用

至於缺點吧,暫時還沒想到。本文只列出了哈迪以爲比較精彩的細節,可能還有遺漏的一些點,你們有補充的能夠留下評論,後續我會更新進本文。

參考資料


本文使用 mdnice 排版

相關文章
相關標籤/搜索