深入探索Glide圖片載入框架:做了哪些優化?如何管理生命週期?怎麼做大圖載入?

Button123發表於2021-06-08

前言

Glide可以說是最常用的圖片載入框架了,Glide鏈式呼叫使用方便,效能上也可以滿足大多數場景的使用,Glide原始碼與原理也是面試中的常客。
但是Glide的原始碼內容比較多,想要學習它的原始碼往往千頭萬緒,一時抓不住重點.
本文以Glide做了哪些優化為切入點,介紹與學習Glide的原始碼與原理,如果對您有所幫助,歡迎點贊.

原文首發:https://juejin.cn/post/6970683481127043085

Glide做了哪些優化?

要想要回答這個問題,我們可以先想一想,如果我們自己要實現一個圖片載入框架,我們會思考什麼問題?
1.圖片下載是個耗時過程,我們首先需要考慮的就是圖片快取的問題
2.圖片載入也是個耗記憶體的操作,很多OOM都是圖片載入導致的,所以我們也要考慮記憶體優化問題
3.圖片載入到一半,頁面關閉了,圖片載入也應該中止,這又牽扯到了生命週期管理的問題
4.還有就是圖片載入框架是否支援大圖載入?大圖情況下會有什麼問題?

以上就是我們提出的有關於Glide的幾個問題了,這樣我們可以輕鬆得出本文主要包括的內容
1.Glide圖片載入的總體流程介紹
2.Glide快取機制做了哪些優化?
3.Glide做了哪些記憶體優化?
4.Glide如何管理生命週期?
5.Glide怎麼做大圖載入?

下面就帶著問題進入正文~

1.Glide圖片載入總體流程介紹

在開始瞭解Glide具體做了哪些優化之前,我們先對Glide圖片載入的總體流程做一個簡單的介紹,讓大家首先有個整體概念。
同時在後面對Glide做的優化具體發生在哪一步也可以方便的知道.
概括來說,圖片載入包含封裝,解析,下載,解碼,變換,快取,顯示等操作,如下圖所示:

  • 1.封裝引數:從指定來源,到輸出結果,中間可能經歷很多流程,所以第一件事就是封裝引數,這些引數會貫穿整個過程;
  • 2.解析路徑:圖片的來源有多種,格式也不盡相同,需要規範化;
  • 3.讀取快取:為了減少計算,通常都會做快取;同樣的請求,從快取中取圖片(Bitmap)即可;
  • 4.查詢檔案/下載檔案:如果是本地的檔案,直接解碼即可;如果是網路圖片,需要先下載;
  • 5.解碼:這一步是整個過程中最複雜的步驟之一,有不少細節;
  • 6.變換:解碼出Bitmap之後,可能還需要做一些變換處理(圓角,濾鏡等);
  • 7.快取:得到最終bitmap之後,可以快取起來,以便下次請求時直接取結果;
  • 8.顯示:顯示結果,可能需要做些動畫(淡入動畫,crossFade等)。

2.Glide快取機制做了哪些優化?

我們知道,下載圖片是非常耗費資源的,所以圖片快取機制是圖片載入框架很重要的一部分,下面就以一張表格來說明下 Glide 快取。

快取型別 快取代表 說明
活動快取 ActiveResources 如果當前對應的圖片資源是從記憶體快取中獲取的,那麼會將這個圖片儲存到活動資源中。
記憶體快取 LruResourceCache 圖片解析完成並最近被載入過,則放入記憶體中
磁碟快取-資源型別 DiskLruCacheWrapper 被解碼後的圖片寫入磁碟檔案中
磁碟快取-原始資料 DiskLruCacheWrapper 網路請求成功後將原始資料在磁碟中快取

在介紹具體快取前,先來看一張載入快取執行順序,有個大概的印象

Glide的快取機制,主要分為2種快取,一種是記憶體快取,一種是磁碟快取。
之所以使用記憶體快取的原因是:防止應用重複將圖片讀入到記憶體,造成記憶體資源浪費。
之所以使用磁碟快取的原因是:防止應用重複的從網路或者其他地方下載和讀取資料。
正式因為有著這兩種快取的結合,才構成了Glide極佳的快取效果。

2.1 記憶體快取

Glide預設開啟記憶體快取,我們也可以通過skipMemoryCache關閉
上面我們可以看到記憶體快取其實分兩個部分,ActiveResource快取與LRU快取
ActiveResources 就是一個弱引用的 HashMap ,用來快取正在使用中的圖片,使用 ActiveResources 來快取正在使用中的圖片,可以保護這些圖片不會被 LruCache 演算法回收掉

記憶體快取載入順序如下:
1.根據圖片地址,寬高,變換,簽名等生成key
2.第一次載入沒有獲取到活動快取。
3.接著載入記憶體資源快取,先清理掉記憶體快取,在新增進行活動快取。
4.第二次載入活動快取已經存在。
5.當前圖片引用為 0 的時候,清理活動資源,並且新增進記憶體資源。
6.又回到了第一步,然後就這樣環環相扣。

總結為流程圖如下:

我們上面總結了Glide記憶體快取載入的流程,看到這裡我們很容易有個疑問,為什麼Glide要設計兩種記憶體快取?

2.1.1 為什麼設計兩種記憶體快取?

LruCache演算法的實現,你會發現它其實是用一個Set來快取物件的,每次記憶體超出快取設定觸發trim操作的時候,其實是對這個Set進行遍歷,然後移除快取。但是我們都知道Set是無序的,因此遍歷的時候有可能會把正在使用的快取給誤傷了,我還在用著它呢就給移出去了。因此這個弱引用可能是對正在使用中的圖片的一種保護,使用的時候先從LruCache裡面移出去,用完了再把它重新加到快取裡面。

舉個例子

比如我們 Lru 記憶體快取 size 設定裝 99 張圖片,在滑動 RecycleView 的時候,如果剛剛滑動到 100 張,那麼就會回收掉我們已經載入出來的第一張,這個時候如果返回滑動到第一張,會重新判斷是否有記憶體快取,如果沒有就會重新開一個 Request 請求,很明顯這裡如果清理掉了第一張圖片並不是我們要的效果。所以在從記憶體快取中拿到資源資料的時候就主動新增到活動資源中,並且清理掉記憶體快取中的資源。這麼做很顯然好處是 保護不想被回收掉的圖片不被 LruCache 演算法回收掉,充分利用了資源。

2.1.1 小結

本節主要總結了Glide記憶體快取載入的流程
1.首先去獲取活動快取,如果載入到則直接返回,沒有則進入下一步
2.接著去獲取LRU快取,在獲取時會將其從LRU中刪除並新增到活動快取中
3.下次載入就可以直接載入活動快取了
4.當圖片引用為0時,會從活動快取中清除並新增到LRU快取中
5.之所以要設計兩種記憶體快取的原因是為了防止載入中的圖片被LRU回收

2.2 磁碟快取

首先了解一下磁碟快取策略

  • DiskCacheStrategy.NONE: 表示不快取任何內容。
  • DiskCacheStrategy.RESOURCE: 在資源解碼後將資料寫入磁碟快取,即經過縮放等轉換後的圖片資源。
  • DiskCacheStrategy.DATA: 在資源解碼前將原始資料寫入磁碟快取。
  • DiskCacheStrategy.ALL : 使用DATARESOURCE快取遠端資料,僅使用RESOURCE來快取本地資料。
  • DiskCacheStrategy.AUTOMATIC:它會嘗試對本地和遠端圖片使用最佳的策略。當你載入遠端資料時,AUTOMATIC 策略僅會儲存未被你的載入過程修改過的原始資料,因為下載遠端資料相比調整磁碟上已經存在的資料要昂貴得多。對於本地資料,AUTOMATIC 策略則會僅儲存變換過的縮圖,因為即使你需要再次生成另一個尺寸或型別的圖片,取回原始資料也很容易。預設使用這種快取策略

在瞭解磁碟快取時我們主要需要明確一個概念,是當我們使用 Glide 去載入一張圖片的時候,Glide 預設並不會將原始圖片展示出來,而是會對圖片進行壓縮和轉換,總之就是經過種種一系列操作之後得到的圖片,就叫轉換過後的圖片。
我們既可以快取變換之前的原始圖片,也可以快取變換後的圖片

2.2.1 為什麼需要兩種磁碟快取

上文已經說了,DiskCacheStrategy.RESOURCE快取的是變換後的資源,DiskCacheStrategy.DATA快取的是變換前的資源
舉個例子,同一張圖片,我們先在100*100View是展示,再在200*200View上展示
如果不快取變換後的型別相當於每次都要進行一次變換操作,如果不快取原始資料則每次都要去重新下載資料
如下可以看出,兩種快取的key不一樣

DiskCacheStrategy.RESOURCE
currentKey = new ResourceCacheKey(helper.getArrayPool(),sourceId,helper.getSignature(),helper.getWidth(),helper.getHeight(),transformation,resourceClass,helper.getOptions());

DiskCacheStrategy.DATA
DataCacheKey newOriginalKey = new DataCacheKey(loadData.sourceKey, helper.getSignature());

3.Glide做了哪些記憶體優化?

Glide的記憶體優化主要也是對Bitmap的優化,在回答這個問題前,我們可以想想有哪些常見的Bitmap優化手段
1.當圖片大小與View大小不一致時,可以用inSampleSize進行尺寸優化
2.圖片所佔記憶體即寬每畫素所佔記憶體大小,不同的模式每個畫素所佔的記憶體大小不同,我們可以利用inpreferredconfig配置
3.Bitmpa所佔記憶體比較大,如果頻繁建立回收Bitmap記憶體可能造成記憶體抖動,我們可以利用inBitmap利用Bitmap記憶體
4.記憶體快取,上文我們已經介紹了Glide的弱引用快取與LRU快取

其實常見的Bitmap記憶體優化也就這麼幾種了,不過我們在工作中比較少直接使用他們。
下面我們就介紹下Glide中具體是怎麼使用他們的.

3.1 尺寸優化

當裝載圖片的容器例如ImageView只有100*100,而圖片的解析度為800 * 800,這個時候將圖片直接放置在容器上,很容易OOM,同時也是對圖片和記憶體資源的一種浪費。當容器的寬高都很小於圖片的寬高,其實就需要對圖片進行尺寸上的壓縮,將圖片的解析度調整為ImageView寬高的大小,一方面不會對圖片的質量有影響,同時也可以很大程度上減少記憶體的佔用

我們通常使用inSampleSizeBitmap進行尺寸縮放

如果inSampleSize 設定的值大於1,則請求解碼器對原始的bitmap進行子取樣影像,然後返回較小的圖片來減少記憶體的佔用,例如inSampleSize == 4,則取樣後的影像寬高為原影像的1/4,而畫素值為原圖的1/16,也就是說取樣後的影像所佔記憶體也為原圖所佔記憶體的1/16;當inSampleSize <=1時,就當作1來處理也就是和原圖一樣大小。另外最後一句還註明,inSampleSize的值一直為2的冪,如1,2,4,8。任何其他的值也都是四捨五入到最接近2的冪。

    //1
    int widthScaleFactor = orientedSourceWidth / outWidth;
    int heightScaleFactor = orientedSourceHeight / outHeight;
    //2
    int scaleFactor =
        rounding == SampleSizeRounding.MEMORY
            ? Math.max(widthScaleFactor, heightScaleFactor)
            : Math.min(widthScaleFactor, heightScaleFactor);

    int powerOfTwoSampleSize;
    //3
    if (Build.VERSION.SDK_INT <= 23
        && NO_DOWNSAMPLE_PRE_N_MIME_TYPES.contains(options.outMimeType)) {
      powerOfTwoSampleSize = 1;
    } else {
      //4
      powerOfTwoSampleSize = Math.max(1, Integer.highestOneBit(scaleFactor));
      //5
      if (rounding == SampleSizeRounding.MEMORY
      	  // exactScaleFactor由各個裁剪策略如CenterCrop重寫得到,詳情可見程式碼
          && powerOfTwoSampleSize < (1.f / exactScaleFactor)) {
        powerOfTwoSampleSize = powerOfTwoSampleSize << 1;
      }
    }
    options.inSampleSize = powerOfTwoSampleSize;

如上就是Glide圖片進行尺寸縮放相關的程式碼
1.首先計算出圖片與View的寬高比
2.根據縮放策略是省記憶體還是高品質,決定取寬高比的最大值還是最小值
3.當Build.VERSION.SDK_INT<=23時,一些格式的圖片不能縮放
4.highestOneBit的功能是把我們計算的比例四捨五入到最接近2的冪
5.如果縮放策略為省記憶體,並且我們計算的SampleSize<exactScaleFactor,將inSampleSize*2

如上就是Glide圖片載入時做尺寸優化的大概邏輯

3.2 圖片格式優化

我們知道,Bitmap所佔記憶體大小,由寬*高*每畫素所佔記憶體決定
上面的尺寸優化決定寬高,圖片格式優化決定每畫素所佔記憶體

API29中,將Bitmap分為ALPHA_8, RGB_565, ARGB_4444, ARGB_8888, RGBA_F16, HARDWARE六個等級。

  • ALPHA_8:不儲存顏色資訊,每個畫素佔1個位元組;
  • RGB_565:僅儲存RGB通道,每個畫素佔2個位元組,對Bitmap色彩沒有高要求,可以使用該模式;
  • ARGB_4444:已棄用,用ARGB_8888代替;
  • ARGB_8888:每個畫素佔用4個位元組,保持高質量的色彩保真度,預設使用該模式;
  • RGBA_F16:每個畫素佔用8個位元組,適合寬色域和HDR
  • HARDWARE:一種特殊的配置,減少了記憶體佔用同時也加快了Bitmap的繪製。

每個等級每個畫素所佔用的位元組也都不一樣,所儲存的色彩資訊也不同。同一張100畫素的圖片,ARGB_8888就佔了400位元組,RGB_565才佔200位元組,RGB_565在記憶體上取得了優勢,但是Bitmap的色彩值以及清晰度卻不如ARGB_8888模式下的Bitmap

值得注意的是在Glide4.0之前,Glide預設使用RGB565格式,比較省記憶體
但是Glide4.0之後,預設格式已經變成了ARGB_8888格式了,這一優勢也就不存在了。
這本身也就是質量與記憶體之間的取捨,如果應用所需圖片的質量要求不高,也可以修改預設格式

//預設格式修改為了ARGB_8888
 public static final Option<DecodeFormat> DECODE_FORMAT =
      Option.memory(
          "com.bumptech.glide.load.resource.bitmap.Downsampler.DecodeFormat", DecodeFormat.DEFAULT);

3.3 記憶體複用優化

Bitmap所佔記憶體比較大,如果我們頻繁建立與回收Bitmap,那麼很容易造成記憶體抖動,所以我們應該儘量複用Bitmap記憶體
Glide主要使用了inBitmapBitmapPool來實現記憶體的複用

3.3.1 inBitmap介紹

Android 3.0(API 級別 11)開始,系統引入了 BitmapFactory.Options.inBitmap 欄位。如果設定了此選項,那麼採用 Options 物件的解碼方法會在生成目標 Bitmap 時嘗試複用 inBitmap,這意味著 inBitmap 的記憶體得到了重複使用,從而提高了效能,同時移除了記憶體分配和取消分配。不過 inBitmap 的使用方式存在某些限制,在 Android 4.4(API 級別 19)之前系統僅支援複用大小相同的點陣圖,4.4 之後只要 inBitmap 的大小比目標 Bitmap 大即可

3.3.2 BitmapPool介紹

通過上文我們知道了可以通過inBitmap複用記憶體,但是還需要一個地方儲存可複用的Bitmap,這就是BitmapPool
JDK 中的 ThreadPoolExecutor 相信大多數開發者都很熟悉,我們一般將之稱為“執行緒池”。池化是一個很常見的概念,其目的都是為了實現物件複用,例如 ThreadPoolExecutor 就實現了執行緒的複用機制
BitmapPool即實現了Bitmap的池化

3.3.3 Glide的應用

  private static void setInBitmap(
      BitmapFactory.Options options, BitmapPool bitmapPool, int width, int height) {
    @Nullable Bitmap.Config expectedConfig = null;
    if (expectedConfig == null) {
      expectedConfig = options.inPreferredConfig;
    }
    // BitmapFactory will clear out the Bitmap before writing to it, so getDirty is safe.
    options.inBitmap = bitmapPool.getDirty(width, height, expectedConfig);
  }

如上即是Glide設定inBitmap的程式碼,向BitmapPool中傳入寬高與格式,得到一個可複用的物件,這樣就實現了Bitmap的記憶體複用

4.Glide如何管理生命週期?

當我們在做一個網路請示時,頁面退出時應該中止請示,不然容易造成記憶體洩漏
對於圖片載入也是如此,我們在頁面退出時應該中止請示,銷燬資源。
但是我們使用Glide的時候卻不需要在頁面退出時做什麼操作,說明Glide可以做到在頁面關閉時自動釋放資源
下面我們一起看下Glide是如何實現的
主要是兩步:
1.呼叫時通過Glide.with傳入context,利用context構建一個Fragment
2.監聽Fragment生命週期,銷燬時釋放Glide資源

4.1 傳入context構建Fragment

//通過Activity拿到RequestManager
public RequestManager get(@NonNull Activity activity) {
      //拿到當前Activity的FragmentManager
      android.app.FragmentManager fm = activity.getFragmentManager();
      //生成一個Fragment去繫結一個請求管理RequestManager
      return fragmentGet(
          activity, fm, /*parentHint=*/ null, isActivityVisible(activity));
  }

 private RequestManager fragmentGet(@NonNull Context context,
     @NonNull android.app.FragmentManager fm,
     @Nullable android.app.Fragment parentHint,
     boolean isParentVisible) {
   //①在當前Activity新增一個Fragment用於管理請求的生命週期
   RequestManagerFragment current = getRequestManagerFragment(fm, parentHint, isParentVisible);
   //獲取RequestManager
   RequestManager requestManager = current.getRequestManager();
   //如果不存在RequestManager,則建立
   if (requestManager == null) {
     Glide glide = Glide.get(context);
     //②構建RequestManager  
     //current.getGlideLifecycle()就是ActivityFragmentLifecycle,也就是構建RequestManager時會傳入fragment中的ActivityFragmentLifecycle
     requestManager =
         factory.build(
             glide, current.getGlideLifecycle(), current.getRequestManagerTreeNode(), context);
     //將構建出來的RequestManager繫結到fragment中
     current.setRequestManager(requestManager);
   }
   //返回當前請求的管理者
   return requestManager;
 }  

如上所示:
1.在當前Activity新增一個透明Fragment用於管理請示生命週期
2.構建RequestManager並傳入Fragment生命週期

4.2 RequestManager監聽生命週期

public class RequestManager implements LifecycleListener,
    ModelTypes<RequestBuilder<Drawable>> { 

	RequestManager(
      Glide glide,
      Lifecycle lifecycle,
      RequestManagerTreeNode treeNode,
      RequestTracker requestTracker,
      ConnectivityMonitorFactory factory,
      Context context) {
    ... 
    //將當前物件註冊到ActivityFragmentLifecycle
    lifecycle.addListener(this);
  }
  //...

  //RequestManager實現了fragment生命週期回撥
  @Override
  public synchronized void onStart() {
    resumeRequests();
    targetTracker.onStart();
  }

  @Override
  public synchronized void onStop() {
    pauseRequests();
    targetTracker.onStop();
  }

  @Override
  public synchronized void onDestroy() {
    targetTracker.onDestroy();
  }

}

public class RequestManagerFragment extends Fragment {
  //生命週期的關鍵就在ActivityFragmentLifecycle
  private final ActivityFragmentLifecycle lifecycle;
  public RequestManagerFragment() {
    this(new ActivityFragmentLifecycle());
  }

  RequestManagerFragment(@NonNull ActivityFragmentLifecycle lifecycle) {
    this.lifecycle = lifecycle;
  }
  @Override
  public void onStart() {
    super.onStart();
    lifecycle.onStart();
  }

  @Override
  public void onStop() {
    super.onStop();
    lifecycle.onStop();
  }

  @Override
  public void onDestroy() {
    super.onDestroy();
    lifecycle.onDestroy();
    unregisterFragmentWithRoot();
  }
  //...
}

邏輯很簡單:Fragment生命週期變化會回撥RequestManager生命週期,然後在進行相關的資源釋放工作

4.3 小結

Glide.with(this)繫結了Activity的生命週期。在Activity內新建了一個無UIFragment,這個Fragment持有一個Lifecycle,通過LifecycleFragment關鍵生命週期通知RequestManager進行相關從操作。在生命週期onStart時繼續載入,onStop時暫停載入,onDestory時停止載入任務和清除操作。

5.Glide怎麼做大圖載入

對於圖片載入還有種情況,就是單個圖片非常巨大,並且還不允許壓縮。比如顯示:世界地圖、清明上河圖、微博長圖等
首先不壓縮,按照原圖尺寸載入,那麼螢幕肯定是不夠大的,並且考慮到記憶體的情況,不可能一次性整圖載入到記憶體中
所以這種情況的優化思路一般是區域性載入,通過BitmapRegionDecoder來實現
這種情況下通常Glide只負責將圖片下載下來,圖片的載入由我們自定義的ImageView來實現

5.1 BitmapRegionDecoder介紹

BitmapRegionDecoder主要用於顯示圖片的某一塊矩形區域,如果你需要顯示某個圖片的指定區域,那麼這個類非常合適。
對於該類的用法,非常簡單,既然是顯示圖片的某一塊區域,那麼至少只需要一個方法去設定圖片;一個方法傳入顯示的區域即可
舉個例子:

//設定顯示圖片的中心區域
BitmapRegionDecoder bitmapRegionDecoder = BitmapRegionDecoder.newInstance(inputStream, false);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
Bitmap bitmap = bitmapRegionDecoder.decodeRegion(new Rect(width / 2 - 100, height / 2 - 100, width / 2 + 100, height / 2 + 100), options);
mImageView.setImageBitmap(bitmap);

不過這種方法雖然也能載入大圖,但做的還不夠,滑動時記憶體抖動,卡頓現象比較明顯,不能用於線上

下面介紹一種可以用於線上的大圖載入方案

5.2 可用於線上的大圖載入方案

SubsamplingScaleImageView將大圖切片,再判斷是否可見,如果可見則加入記憶體中,否則回收,減少了記憶體佔用與抖動 同時根據不同的縮放比例選擇合適的取樣率,進一步減少記憶體佔用 同時在子執行緒進行decodeRegion操作,解碼成功後回撥至主執行緒,減少UI卡頓.

總結

本文主要以Glide做了哪些優化為切入點,回答瞭如下幾個問題
1.說一下Glide圖片載入的總體流程
2.Glide快取機制做了哪些優化?
3.Glide做了哪些記憶體優化?
4.Glide如何管理生命週期?
5.Glide怎麼做大圖載入?

好了,今天的文章就到這裡,感謝閱讀,喜歡的話不要忘了三連。大家的支援和認可,是我分享的最大動力。

Android高階開發系統進階筆記、最新面試複習筆記PDF,我的GitHub

相關文章