android 圖片加載庫(2)- Glide

說在開頭

Glide 最早是 google 內部員工的作品,后來被 google 發掘力推,15,16年可火了,但是奈何漸漸不敵 Fresco ,Fresco 的占用內存低的優勢真是太強大了,因為 Fresco 使用了系統的共享內存,而不是 JVM 虛擬機所屬的內存塊。但是 Glide 是非常適合用來學習的,Glide 的代碼結構非常優秀,記得郭琳郭姐說過,要是能把 Glide 用心讀一遍,都會獲得脫胎換骨的變化的,對于我們加強,進化自己的 java 基本功大有裨益啊。

Glide 官方中文文檔: Glide v4 快速高效的Android圖片加載庫 ,推薦大家都去看一下,尤其是打算研究源碼的同學們


Glide 集成

Glide 現在處于 V 4.X 版本,較上一個版本 V 3.X 變動較大,大家注意一下。

Glide 依賴
repositories {
  mavenCentral()
  maven { url 'https://maven.google.com' }
}

dependencies {
    compile 'com.github.bumptech.glide:glide:4.5.0'
    annotationProcessor 'com.github.bumptech.glide:compiler:4.5.0'
}

上面是官方給出的依賴地址,注意遠程庫地址可是 google 自己的地址,一般人都沒寫過這個地址,注意加上啊。第二個依賴是 Glide 的注解,版本號保持和 Glide 一致,這個注解的依賴可以不加,但是若是要全局設置 Glide 參數就必須使用了。

Glide 版本 android 編譯版本
glide:4.5.0 appcompat-v7:27.0.2
glide:4.4.0 appcompat-v7:27.0.2
glide:4.3.1 appcompat-v7:26.1.0

注意 Glide 版本號對 android 編譯版本有最低要求,不匹配就報錯,這點要特殊注意。下面是 Glide 歷史版本對 android 編譯版本匹配要求:

Glide 版本 android 編譯版本
glide:4.5.0 appcompat-v7:27.0.2
glide:4.4.0 appcompat-v7:27.0.2
glide:4.3.1 appcompat-v7:26.1.0

如果你需要使用不同的支持庫版本,你需要在你的 build.gradle 文件里去從 Glide 的依賴中去除 "com.android.support"。例如,假如你想使用 v26 的支持庫:

dependencies {
  implementation ("com.github.bumptech.glide:glide:4.5.0") {
    exclude group: "com.android.support"
  }
  implementation "com.android.support:support-fragment:26.1.0"
}

注意 Glide 依賴與支持庫版本不兼容,會出現下面這個運行時異常:

java.lang.NoSuchMethodError: No static method getFont(Landroid/content/Context;ILandroid/util/TypedValue;ILandroid/widget/TextView;)Landroid/graphics/Typeface; in class Landroid/support/v4/content/res/ResourcesCompat; or its super classes (declaration of 'android.support.v4.content.res.ResourcesCompat' 
at android.support.v7.widget.TintTypedArray.getFont(TintTypedArray.java:119)

不推薦在依賴中使用 @aar ,如果必須這么做,請添加 transitive=true 以確保所有必要的類都被包含到你的 API 中:

dependencies {
    implementation ("com.github.bumptech.glide:glide:4.5.0@aar") {
        transitive = true
    }

不設置 transitive 可能會出現這個異常,另外若是還出現這個異常,就不要在依賴中使用 @aar

java.lang.NoClassDefFoundError: com.bumptech.glide.load.resource.gif.GifBitmapProvider
    at com.bumptech.glide.load.resource.gif.ByteBufferGifDecoder.<init>(ByteBufferGifDecoder.java:68)
    at com.bumptech.glide.load.resource.gif.ByteBufferGifDecoder.<init>(ByteBufferGifDecoder.java:54)
    at com.bumptech.glide.Glide.<init>(Glide.java:327)
    at com.bumptech.glide.GlideBuilder.build(GlideBuilder.java:445)
    at com.bumptech.glide.Glide.initializeGlide(Glide.java:257)
    at com.bumptech.glide.Glide.initializeGlide(Glide.java:212)
    at com.bumptech.glide.Glide.checkAndInitializeGlide(Glide.java:176)
    at com.bumptech.glide.Glide.get(Glide.java:160)
    at com.bumptech.glide.Glide.getRetriever(Glide.java:612)
    at com.bumptech.glide.Glide.with(Glide.java:684)
Glide 混淆
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep public class * extends com.bumptech.glide.module.AppGlideModule
-keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** {
  **[] $VALUES;
  public *;
}

# for DexGuard only
-keepresourcexmlelements manifest/application/meta-data@value=GlideModule

Glide 所需權限

Glide 權限這塊就是需要 : 讀寫 SD 卡權限,其他沒有了

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

Generated API - Glide 的全局設置

這個是 Glide V4 版本新添加的內容,全局設置的 option 可以在單個請求中替換。Glide 給大家提供這樣一個全局設置的位置,就是想讓我們的可以服用這些關于圖片加載的公共業務設置,以方便我們可以更高度,更優秀的組織我們的圖片加載代碼,甚至是編寫我們自己的圖片加載業務庫,大家不要忘了,時間是不斷流逝的,說不準哪天就是我們替換圖片加載庫的時候了呢,所以優良的代碼架構設計和業務功能框架的封裝就顯得有味必要了。這里 Glide 新版本給我們能提供更為便利的操作。

2個全局設置基類 AppGlideModule / LibraryGlideModule

首先我們需要按照如下這樣,在我們的 app 中聲明一個類

@GlideModule
public final class MyAppGlideModule extends AppGlideModule {
    ......
}

這個類需要繼承 AppGlideModule 這個 Glide 提供給我們的類,注意關于這個類有以下要求:

  • Application 級別的 module 也就是我們常用的 app module中,我們要繼承 AppGlideModule 這個類
  • 而 Library 級別的 module 我們就要繼承 LibraryGlideModule 這個類
  • 注意上面2個類不要用混,要不無法正常使用代碼的
  • 必須使用 @GlideModule 這個注解來修飾我們的這個類,@GlideModule 這個注解是 Glide 用來遍歷代碼,查詢我們定義的全局配置類的關鍵
  • 我們定義的全局配置類可以是空的實現,雖然這個要求不是必須的,但是官方還是辦強制的要求我們這么做
  • 從上面的2個不同的基類來看,其實我們可以舍棄 AppGlideModule 而使用 LibraryGlideModule 的,我們會把 app 根據業務和功能拆分成業務和基礎功能 module 的,在所有的 module 中,圖片加載的配置應該是統一的,這樣我們就要把圖片加載也做成一個單獨的功能 module ,或是抽象到業務基礎功能庫 module 中,這樣在 Library 級別的 module 中,我們就只想使用 LibraryGlideModule 這個父類了。

然后我們就可以使用 Glide 的統一入口來調用 Glide 的 API 了。上面說過來了 Glide 的設計思路是很棒的,使用 bulider 建造者模式來統一存儲,設置相關的各種 option 。使用全局靜態單例的方式來給我們提供統一的公共入口,這樣的 API 非常之簡潔,是我們編寫基礎功能庫的最優秀方案。

Glide 的公共入口有2個類,按照不同的配置情況來使用:

  • Glide :我們若是不配置全局配置類的話,就用 Glide 這個類
  • GlideApp :我們若是配置了全局配置類的話,就用 GlideApp 這個類

這2個類的區別除了全局配置以外,就是 API 的使用上了,Glide 在 API 使用上是鏈式調用的寫法,很方便。使用 GlideApp 這個類,對我們使用 API 沒影響,但是 Glide 這個類,我們只能鏈式使用最基礎的2個 API:

GlideApp.with(context)
             .load("圖片地址")
             .into(getImage());

所用從使用上來看,官方是半強制的讓我們使用 GlideApp 這個類,也就是讓我們進行全局配置。

全局配置類中我們可以做什么

還是得從 2個全局設置基類 AppGlideModule / LibraryGlideModule 開始,畢竟這是全局配置的入口,先來看看2個類的定義:

public abstract class AppGlideModule extends LibraryGlideModule implements AppliesOptions {
  
  public boolean isManifestParsingEnabled() {
    return true;
  }

  @Override
  public void applyOptions(Context context, GlideBuilder builder) {
    // Default empty impl.
  }
}
public abstract class LibraryGlideModule implements RegistersComponents {

  @Override
  public void registerComponents(Context context, Glide glide, Registry registry) {
    // Default empty impl.
  }
}

可以看到 AppGlideModule 也是擴展自 LibraryGlideModule 的,LibraryGlideModule 只有一個注冊組件的方法,AppGlideModule 則實現了 AppliesOptions 接口,在 applyOptions 這個方法里可以進行我們對 Glide 的全局配置

看到這里各位看官是不是有疑問了,上文說到我們要把圖片加載放到單獨的基礎功能 module 中,那么在 module 里就不能 AppGlideModule ,而只能使用 LibraryGlideModule ,但是 LibraryGlideModule 不能支持我們配置全局參數啊,這不就是矛盾了嘛,我做了下測試:

  • 基本設計:
    寫3個 module :app / BitmapActivity / BitmapLoader 。 app 是主工程;BitmapActivity 提供一個頁面,用來測試加載圖片;BitmapLoader 提供公共圖片加載 API ,在這 module 中聲明一個 AppGlideModule 的子類出來。
  • 測試流程:
    主功能有個按鈕,點擊一下跳轉到 BitmapActivity 提供的頁面去加載圖片,然后點擊一個按鈕加載圖片
  • 測試結果:
    正常沒問題,可以加載出圖片來
  • 結論猜測:
    可能官方文檔的中文描述有點歧義,Glide 使用的注解在編譯時掃描指定注解的方式查找 AppGlideModule 的子類,Glide 可以做到掃描 aar 包,那么這個 AppGlideModule 的子類我們寫在哪個 module 就不重要了,只要 AppGlideModule 的子類唯一,注解可以掃描的到就行了,LibraryGlideModule 的意義應該是在 module 中優先級高于全局的 AppGlideModule 。上面這是我的猜測。
好了啰嗦了好半天,我們看看都有哪些可以設置的

配置入口在 AppGlideModule 的 applyOptions 方法,配置選項如下:

  • MemoryCache :內存緩存策略
  • BitmapPool : Bitmap 池緩存策略
  • DiskCache :磁盤緩存策略
  • DefaultRequestOptions :默認請求選項
  • 未捕獲異常策略
  • LogLevel :日志級別

Glide 緩存使用LRU 算法,內存緩存使用的是 LruResourceCache ,磁盤緩存使用的是 DiskLruCacheWrapper ,位圖池使用的是 LruBitmapPool 。各級緩存都可以指定大小,也可以替換成自己的實現,但是官方不建議這么做,可能在資源回收這里造成位置錯誤。需要說的是默認磁盤大小為 250 MB,具體的還是推薦大家去看官方文檔:配置 | Glide最新版V4使用指南

默認請求選項這個是比較有用的,這里我們可以設置很多東西,具體有啥后面說,下面的官方文檔的例子

@GlideModule
public class YourAppGlideModule extends AppGlideModule {
  @Override
  public void applyOptions(Context context, GlideBuilder builder) {
    builder.setDefaultRequestOptions(
        new RequestOptions()
          .format(DecodeFormat.RGB_565) // 解碼格式
          .disallowHardwareBitmaps());
  }
}

要說的是 V 3.X 版本默認解碼格式是 RGB_565,對于有透明通道的圖片來說可能會造成色差問題,從 V 4.X 版本開始,默認解碼格式改成了 ARGB_8888

日志這塊沒什么東西,就是調整一下日志的級別罷了

@GlideModule
public class YourAppGlideModule extends AppGlideModule {
  @Override
  public void applyOptions(Context context, GlideBuilder builder) {
    builder.setLogLevel(Log.DEBUG);
  }
}

基本 api

GlideApp.with(activity)
.load(url) // 圖片地址
.override(200,200) // 設置圖片尺寸
.priority(Priority.HIGH) // 加載優先級
.into(imageView); // 目標 view

Glide的基本使用還是很簡單的,上面幾個方法都很簡單,沒有復雜的參數,也是一看就都懂的

占位符

這個簡單,一看都懂,不多說

  • placeholder
    加載完成之前顯示的
  • error
    請求失敗時顯示的
  • fallback
    url 為null 時顯示的,但是優先級低于 error,此時有設置了 error 就優先顯示 error,沒有時才顯示 fallback

注意有一種寫法,在請求失敗時也就是 error 時,可以再請求一次 error 的圖片,注意下面的寫法:

Glide.with(fragment)
  .load(primaryUrl)
  .error(Glide.with(fragment)
      .load(fallbackUrl))
  .into(imageView);

這是 error 中需要傳入的是一個 RequestBuilder<T> 對象,T 的樂星跟著上面走,我們平時寫的 Glide.with(fragment).load(primaryUrl).into(imageView) 這個鏈式調用本質就是生成一個 RequestBuilder<T> 對象,所以可以作為參數傳進去。很有意思把,這樣的寫法,我也是第一次見,后面還有這樣的應用。

Thumbnail 縮略圖

有一些優秀的后臺設計是可以讓我們再請求大圖之前給我們提供一張專屬的縮略圖,而不是大陸貨色,這時 Glide 的 Thumbnail 縮略圖就發揮作用了。Glide 的 Thumbnail 縮略圖和大圖其實是2個平發的請求,一般縮略圖比大圖返回的要快,所有先顯示縮略圖,但大圖的優先級比縮略圖高,要是大圖先回來,那么就不會顯示縮略圖了。縮略圖我們可以配置單獨的一個 url 地址,也可以用大圖的地址,指定縮略圖的尺寸或是縮放比例。

 GlideApp.with(activity)
                .load(url)
                // 請求獨立的縮略圖
                .thumbnail(GlideApp.with(activity)
                        .load(thumbnailUrl))
                // 請求原圖的縮略圖,指定大小,長寬不同
                .thumbnail(GlideApp.with(activity)
                        .load(url).override(200, 300))
                // 請求原圖的縮略圖,指定大小,長寬相同
                .thumbnail(GlideApp.with(activity)
                        .load(url).override( 300 ))
                // 請求原圖的縮略圖,指定大小,指定縮放比例
                .thumbnail(0.25f)

下面的 API 就要講究一個先來后到的順序問題了,下面說2個東西,Transformation 變換,transition 過度。圖片加載本質也是一個網絡請求,一個請求出去后,過一回就會回來,這時 Glide 的 API 會先跑 Transformation 變換的設置,因為這時我們拿到原始資源了,我們可以做任何我們想干的事,裁剪,轉換類型等。然后我們按照我們秀娥想法處理完原始資源過后就要傳遞給 view 去顯示了,這時 Glide 的 API 會跑 transition 過度的設置了,過度其實就是動畫,目的是為了讓 view 的圖像切換顯得不是很突兀,要貼近自然,一般都是做一個漸變動畫。

那么接下來我們按照順序,先說 Transformation 變換的 API ,再來說 transition 過度的 API

Transformation 變換

Transformation 變換的目的是對原始資源進行定制化處理,我們在這里可以干一下幾種事情:

  • 類型變換,默認返回的是 Drawable 類型的對象
  • 對圖片按照 imageview scaleType 進行裁剪
  • 裁剪呈圓形,圓角或其他任何形狀,這是最常見的需求
  • 自定義變換
  • 沒有變換
  • 多個 Transformation 效果疊加,Transformation 默認只能用一個,想同時用多個,得打個集合包進去

類型變換 API

GlideApp.with(activity)
                .asBitmap() 
                .asDrawable()
                .asFile()
                .asGif()

不用說了吧,很直白的,你懂的,默認的是 asDrawable()

scaleType 裁剪

  GlideApp.with(activity)
                .load(url)
                .centerCrop()
                .fitCenter()
                .circleCrop()
                .dontTransform

具體效果我說一下,centerCrop 和 fitCenter 都是他們原本的效果,什么變化,circleCrop 是裁剪成圓形,這里我找了下沒找到圓角的裁剪,這里系統自帶了圓形裁剪還是很方便的,其他圓形裁剪后面會介紹。最后一個是沒有變化

自定義變換需要繼承 BitmapTransformation 這個類,具體的去看管飯發個文檔吧,不重復寫了: Glide - 變換

多個 Transformation 效果疊加

 GlideApp.with(activity)
                .load(url)
                .transforms(new FitCenter(), new CircleCrop())

transforms 里面傳入的是可變參數,可以傳入多個,說實話,代碼寫到現在,真的感覺這個可慘參數真是秒啊,要不就要多寫一個集合對象,代碼上就不能像現在這么簡潔,流暢,舒心了。

transition 過度

承接上文,我們獲取完原始數據,然后按照我們定制的處理過后,就該是我們去如何顯示的步奏了,目前都是采用一個漸變動畫做過度的,這個 transition 的意思就是這樣的一個動畫了。

過渡動畫執行時機:

  • 磁盤緩存
  • 本地資源
  • 遠程資源
  • 內存資源時不會觸發過度動畫

過渡動畫效果:

  • 淡入
  • 交叉淡入
  • 不過渡
  • V4.X 版本默認是沒有動畫效果的
// 淡入
.transition( DrawableTransitionOptions.withCrossFade() )
// 交叉淡入
.transition(DrawableTransitionOptions.withCrossFade(new DrawableCrossFadeFactory.Builder().setCrossFadeEnabled(true)))
或者
DrawableTransitionOptions options = new DrawableTransitionOptions().crossFade(new DrawableCrossFadeFactory.Builder().setCrossFadeEnabled(true));
.transition(options)
// 沒有動畫
TransitionOptions.dontTransition()
或者
.dontAnimate()

根據圖片資源類型的不同,TransitionOptions 有3種:

  • BitmapTransitionOptions
  • DrawableTransitionOptions
  • GenericTransitionOptions 通用型

動畫也可以自定義:
1.實現TransitionFactory
2.重寫build()
可以控制圖片在內存緩存上是否執行動畫。

具體寫法參考DrawableCrossFadeFactory,然后調用

TransitionOptions的with(TransitionFactory transitionFactory)加載。

有一點要說啊,淡入的效果是給加載出來的圖片一個漸變進入的動畫,然后遮擋占位圖。若是使用圓形變換裁剪圖片,而使用全尺寸占位圖的話,那么圖片邊緣就會把占位圖露出來,所以占位圖的形狀一定要和圖片裁剪成的形狀相同徐熬過才好。


添加監聽器

GlideApp.with(context)
                .load(url)
                .placeholder(R.mipmap.ic_launcher)
                .error(R.mipmap.ic_launcher)
                .centerCrop()
                .listener(new RequestListener<Drawable>() {
                    @Override
                    public boolean onLoadFailed(@Nullable GlideException e, Object o, Target<Drawable> target, boolean b) {
                        Log.i(TAG, "圖片加載失敗 ");
                        return false;
                    }

                    @Override
                    public boolean onResourceReady(Drawable drawable, Object o, Target<Drawable> target, DataSource dataSource, boolean b) {
                        Log.i(TAG, "圖片加載完成: ");
                        return false;
                    }
                })
                .into(imageView);

API 是很簡單的, 但是我也沒用過啊,也許是讓我們實現圖片加載進度顯示的。


緩存策略

Glide 有6種緩存策略:

  • DiskCacheStrategy.AUTOMATIC
    自動緩存,默認的緩存策略,效果 = SOURCE + RESULT
  • DiskCacheStrategy.NONE
    跳過磁盤緩存
  • DiskCacheStrategy.SOURCE
    僅僅只緩存原來的全分辨率的圖像,改變尺寸或是變換過后的都不緩存
  • DiskCacheStrategy.RESULT
    僅僅緩存最終的圖像,即,降低分辨率后的(或者是轉換后的)
  • DiskCacheStrategy.ALL
    緩存所有版本的圖像(默認行為)
  • skipMemoryCache(true)
    跳過內存緩存, 加載的圖片不存入內存中,也不從內存中查詢緩存

對于我們經常要進行變換的圖片來說,變換過后的圖片可能對你有很打的影響,那么忽略內存緩存是很有必要的,以此類推,這幾種緩存策略還是不難理解的,大部分情況下默認緩存模式就夠我們用了。但是要注意默認緩存模式中,磁盤緩存和內存緩存是一樣的,內存緩存了什么都會同步到磁盤緩存中,比如變換的圖片也會進行磁盤緩存。


統一參數配置

還記得 AppGlideModule 這個類吧,我們可以在這個類的子類中進行統一配置,可以配置的屬性如下:

  • Placeholders 占位符
  • Transformations 變換
  • Caching Strategies 緩存策略
  • 組件特定參數:編碼質量,解碼參數等。
@GlideModule
public class MyLibraryGlideModule extends AppGlideModule {

    @Override
    public void applyOptions(Context context, GlideBuilder builder) {
        super.applyOptions(context, builder);

        RequestOptions requestOptions = new RequestOptions();
        requestOptions.placeholder(R.drawable.ic_launcher_xx)
                // 占位符
                .error(R.drawable.ic_launcher_xx)
                .format(DecodeFormat.PREFER_ARGB_8888)
                // transform 變換,裁剪等
                .circleCrop();

        // 過度動畫參數
        DrawableTransitionOptions transitionOptions =            DrawableTransitionOptions.withCrossFade(new DrawableCrossFadeFactory.Builder().setCrossFadeEnabled(true));

        builder.setDefaultRequestOptions(requestOptions);
        builder.setDefaultTransitionOptions(Drawable.class, transitionOptions);
    }
}

資源清理

Glide 的資源清理要小心,所有的在 Glide 相關 API 中獲取的 Drawable,Bitmap 都是由 Glide 統一負責回收,重用,所以我們不能私自進行 recycle 等回收操作,有可能會引起錯誤。

但是我們還是可以進行資源回收操作的,有3個操作:

  • clean
    清除正在顯示的 view 和圖片資源的關聯,注意這時 view 會返回顯示占位圖或是空白圖片
GlideApp.with(activity).clear(image);
  • clearMemory
    清除內存緩存,官方不建議我們這么做,但是自行判斷下在用喲見大量圖片的頁面關閉時,要不要手動回收下內存資源,我覺得這樣還是有必要的。Glide 在頁面關閉時做的也只是停止這個頁面當前的所有請求。這個方法是同步方法,在主線程運行
GlideApp.get(activity).clearMemory();
  • clearDiskCache
    清楚磁盤緩存,這個我覺得意義不大, 默認的磁盤緩存是 250M 大小,而且磁盤會緩存圖片的原始數據,變換,裁剪過后的數據都會緩存,這樣其實是很方便我們的,一般情況下我們是不會觸發這個選項的,除了有一些 app 在設置里可以清楚緩存。這個方法注意是需要異步運行的
AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
            @Override
            protected Void doInBackground(Void... voids) {
                GlideApp.get(activity).clearDiskCache();
                return null;
            }
        };
        task.execute();
    }

參考資料:

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。