Glide 系列-1:預熱、Glide 的常用配置方式及其原理

WngShhng發表於2019-01-06

在接下來的幾篇文章中,我們會對 Android 中常用的圖片載入框架 Glide 進行分析。在本篇文章中,我們先通過介紹 Glide 的幾種常用的配置方式來了解 Glide 的部分原始碼。後續的文中,我們會對 Glide 的原始碼進行更詳盡的分析。

對於 Glide,相信多數 Android 開發者並不陌生,在本文中,我們不打算對其具體使用做介紹,你可以通過檢視官方文件進行學習。Glide 的 API 設計非常人性化,上手也很容易。

在這篇文中中我們主要介紹兩種常用的 Glide 的配置方式,並以此為基礎來分析 Glide 的工作原理。在本文中我們將會介紹的內容有:

  1. 通過自定義 GlideModule 指定 Glide 的快取路徑和快取空間的大小;
  2. 帶有時間戳的圖片的快取命中問題的解決;
  3. 在 Glide 中使用 OkHttp 作為網路中的圖片資源載入方式的實現。

1、自定義圖片載入方式

有時候,我們需要對 Glide 進行配置來使其能夠對特殊型別的圖片進行載入和快取。考慮這麼一個場景:圖片路徑中帶有時間戳。這種情形比較場景,即有時候我們通過為圖片設定時間戳來讓圖片連結在指定的時間過後失效,從而達到資料保護的目的。

在這種情況下,我們需要解決幾個問題:1).需要配置快取的 key,不然快取無法命中,每次都需要從網路中進行獲取;2).根據正確的連結,從網路中獲取圖片並展示。

我們可以使用自定義配置 Glide 的方式來解決這個問題。

1.1 帶時間戳圖片載入的實現

1.1.1 MyAppGlideModule

首先,按照下面的方式自定義 GlideModule

    @GlideModule
    public class MyAppGlideModule extends AppGlideModule {

        /**
        * 配置圖片快取的路徑和快取空間的大小
        */
        @Override
        public void applyOptions(Context context, GlideBuilder builder) {
            builder.setDiskCache(new InternalCacheDiskCacheFactory(context, Constants.DISK_CACHE_DIR, 100 << 20));
        }

        /**
        * 註冊指定型別的源資料,並指定它的圖片載入所使用的 ModelLoader
        */
        @Override
        public void registerComponents(Context context, Glide glide, Registry registry) {
            glide.getRegistry().append(CachedImage.class, InputStream.class, new ImageLoader.Factory());
        }

        /**
        * 是否啟用基於 Manifest 的 GlideModule,如果沒有在 Manifest 中宣告 GlideModule,可以通過返回 false 禁用
        */
        @Override
        public boolean isManifestParsingEnabled() {
            return false;
        }
    }
複製程式碼

在上面的程式碼中,我們通過覆寫 registerComponents() 方法,並呼叫 Glide 的 Registryappend() 方法來向 Glide 增加我們的自定義圖片型別的載入方式。(如果替換某種資源載入方式則需要使用 replace() 方法,此外 Registry 還有其他的方法,可以通過檢視原始碼進行了解。)

在上面的方法中,我們新定義了兩個類,分別是 CachedImageImageLoaderCachedImage 就是我們的自定義資源型別,ImageLoader 是該資源型別的載入方式。當進行圖片載入的時候,會根據資源的型別找到該圖片載入方式,然後使用它來進行圖片載入。

1.1.2 CachedImage

我們通過該類的構造方法將原始的圖片的連結傳入,並通過該類的 getImageId() 方法來返回圖片快取的鍵,在該方法中我們從圖片連結中過濾掉時間戳:

    public class CachedImage {

        private final String imageUrl;

        public CachedImage(String imageUrl) {
            this.imageUrl = imageUrl;
        }

        /**
        * 原始的圖片的 url,用來從網路中載入圖片
        */
        public String getImageUrl() {
            return imageUrl;
        }

        /**
        * 提取時間戳之前的部分作為圖片的 key,這個 key 將會被用作快取的 key,並用來從快取中找快取資料
        */
        public String getImageId() {
            if (imageUrl.contains("?")) {
                return imageUrl.substring(0, imageUrl.lastIndexOf("?"));
            } else {
                return imageUrl;
            }
        }
    }
複製程式碼

1.1.3 ImageLoader

CachedImage 的載入通過 ImageLoader 實現。正如上面所說的,我們將 CachedImagegetImageId() 方法得到的字串作為快取的鍵,然後使用預設的 HttpUrlFetcher 作為圖片的載入方式。

    public class ImageLoader implements ModelLoader<CachedImage, InputStream> {

        /**
        * 在這個方法中,我們使用 ObjectKey 來設定圖片的快取的鍵
        */
        @Override
        public LoadData<InputStream> buildLoadData(CachedImage cachedImage, int width, int height, Options options) {
            return new LoadData<>(new ObjectKey(cachedImage.getImageId()),
                    new HttpUrlFetcher(new GlideUrl(cachedImage.getImageUrl()), 15000));
        }

        @Override
        public boolean handles(CachedImage cachedImage) {
            return true;
        }

        public static class Factory implements ModelLoaderFactory<CachedImage, InputStream> {

            @Override
            public ModelLoader<CachedImage, InputStream> build(MultiModelLoaderFactory multiFactory) {
                return new ImageLoader();
            }

            @Override
            public void teardown() { /* no op */ }
        }
    }
複製程式碼

1.1.4 使用

當我們按照上面的方式配置完畢之後就可以在專案中使用 CachedImage 來載入圖片了:

    GlideApp.with(getContext())
        .load(new CachedImage(user.getAvatarUrl()))
        .into(getBinding().ivAccount);
複製程式碼

這裡,當有載入圖片需求的時候,都會把原始的圖片連結使用 CachedImage 包裝一層之後再進行載入,其他的步驟與 Glide 的基本使用方式一致。

1.2 原理分析

當我們啟用了 @GlideModule 註解之後會在編譯期間生成 GeneratedAppGlideModuleImpl。從下面的程式碼中可以看出,它實際上就是對我們自定義的 MyAppGlideModule 做了一層包裝。這麼去做的目的就是它可以通過反射來尋找 GeneratedAppGlideModuleImpl,並通過呼叫 GeneratedAppGlideModuleImpl 的方法來間接呼叫我們的 MyAppGlideModule。本質上是一種代理模式的應用:

    final class GeneratedAppGlideModuleImpl extends GeneratedAppGlideModule {
        private final MyAppGlideModule appGlideModule;

        GeneratedAppGlideModuleImpl() {
            appGlideModule = new MyAppGlideModule();
        }

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

        @Override
        public void registerComponents(Context context, Glide glide, Registry registry) {
            appGlideModule.registerComponents(context, glide, registry);
        }

        @Override
        public boolean isManifestParsingEnabled() {
            return appGlideModule.isManifestParsingEnabled();
        }

        @Override
        public Set<Class<?>> getExcludedModuleClasses() {
            return Collections.emptySet();
        }

        @Override
        GeneratedRequestManagerFactory getRequestManagerFactory() {
            return new GeneratedRequestManagerFactory();
        }
    }
複製程式碼

下面就是 GeneratedAppGlideModuleImpl 被用到的地方:

當我們例項化單例的 Glide 的時候,會呼叫下面的方法來通過反射獲取該實現類(所以對生成類的混淆就是必不可少的):

    Class<GeneratedAppGlideModule> clazz = (Class<GeneratedAppGlideModule>)
            Class.forName("com.bumptech.glide.GeneratedAppGlideModuleImpl");
複製程式碼

當得到了之後會呼叫 GeneratedAppGlideModule 的各個方法。這樣我們的自定義 GlideModule 的方法就被觸發了。(下面的方法比較重要,我們自定義 Glide 的時候許多的配置都能夠從下面的原始碼中尋找到答案,後文中我們仍然會提到這個方法)

  private static void initializeGlide(Context context, GlideBuilder builder) {
    Context applicationContext = context.getApplicationContext();
    // 利用反射獲取 GeneratedAppGlideModuleImpl
    GeneratedAppGlideModule annotationGeneratedModule = getAnnotationGeneratedGlideModules();
    // 從 Manifest 中獲取 GlideModule
    List<com.bumptech.glide.module.GlideModule> manifestModules = Collections.emptyList();
    if (annotationGeneratedModule == null || annotationGeneratedModule.isManifestParsingEnabled()) {
      manifestModules = new ManifestParser(applicationContext).parse();
    }

    // 獲取被排除掉的 GlideModule
    if (annotationGeneratedModule != null
        && !annotationGeneratedModule.getExcludedModuleClasses().isEmpty()) {
      Set<Class<?>> excludedModuleClasses = annotationGeneratedModule.getExcludedModuleClasses();
      Iterator<com.bumptech.glide.module.GlideModule> iterator = manifestModules.iterator();
      while (iterator.hasNext()) {
        com.bumptech.glide.module.GlideModule current = iterator.next();
        if (!excludedModuleClasses.contains(current.getClass())) {
          continue;
        }
        iterator.remove();
      }
    }

    // 應用 GlideModule,我們自定義 GlideModuel 的方法會在這裡被呼叫
    RequestManagerRetriever.RequestManagerFactory factory = annotationGeneratedModule != null
        ? annotationGeneratedModule.getRequestManagerFactory() : null;
    builder.setRequestManagerFactory(factory);
    for (com.bumptech.glide.module.GlideModule module : manifestModules) {
      module.applyOptions(applicationContext, builder);
    }
    if (annotationGeneratedModule != null) {
      annotationGeneratedModule.applyOptions(applicationContext, builder);
    }
    // 構建 Glide 物件
    Glide glide = builder.build(applicationContext);
    for (com.bumptech.glide.module.GlideModule module : manifestModules) {
      module.registerComponents(applicationContext, glide, glide.registry);
    }
    if (annotationGeneratedModule != null) {
      annotationGeneratedModule.registerComponents(applicationContext, glide, glide.registry);
    }
    applicationContext.registerComponentCallbacks(glide);
    Glide.glide = glide;
  }
複製程式碼

再回到之前的自定義 GlideModule 部分程式碼中:

public void applyOptions(Context context, GlideBuilder builder) {
    builder.setDiskCache(new InternalCacheDiskCacheFactory(context, Constants.DISK_CACHE_DIR, 100 << 20));
}
複製程式碼

這裡的 applyOptions() 方法允許我們對 Glide 進行自定義。從 initializeGlide() 方法中,我們也看出,這裡的 GlideBuilder 也就是 initializeGlide() 方法中傳入的 GlideBuilder。這裡使用了構建者模式,GlideBuilder 是構建者的例項。所以,我們可以通過呼叫 GlideBuilder 的方法來對 Glide 進行自定義。

在上面的自定義 GlideModule 中,我們通過構建者來指定了 Glide 的快取大小和快取路徑。 GlideBuilder 還提供了一些其他的方法,我們可以通過檢視原始碼瞭解,並呼叫這些方法來自定義 Glide.

2、在 Glide 中使用 OkHttp

Glide 預設使用 HttpURLConnection 實現網路當中的圖片的載入。我們可以通過對 Glide 進行配置來使用 OkHttp 進行網路圖片載入。

首先,我們需要引用如下依賴:

    api ('com.github.bumptech.glide:okhttp3-integration:4.8.0') {
        transitive = false
    }
複製程式碼

該類庫中提供了基於 OkHttp 的 ModelLoaderDataFetcher 實現。它們是 Glide 圖片載入環節中的重要組成部分,我們會在後面介紹原始碼和 Glide 的架構的時候介紹它們被設計的意圖及其作用。

然後,我們需要在自定義的 GlideModule 中註冊網路圖片載入需要的元件,即在 registerComponents() 方法中替換 GlideUrl 的載入的預設實現:

    @GlideModule
    @Excludes(value = {com.bumptech.glide.integration.okhttp3.OkHttpLibraryGlideModule.class})
    public class MyAppGlideModule extends AppGlideModule {

        private static final String DISK_CACHE_DIR = "Glide_cache";

        private static final long DISK_CACHE_SIZE = 100 << 20; // 100M

        @Override
        public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) {
            builder.setDiskCache(new InternalCacheDiskCacheFactory(context, DISK_CACHE_DIR, DISK_CACHE_SIZE));
        }

        @Override
        public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
            OkHttpClient okHttpClient = new OkHttpClient.Builder()
                    .connectTimeout(10, TimeUnit.SECONDS)
                    .writeTimeout(10, TimeUnit.SECONDS)
                    .readTimeout(15, TimeUnit.SECONDS)
                    .eventListener(new EventListener() {
                        @Override
                        public void callStart(Call call) {
                            // 輸出日誌,用於確認使用了我們配置的 OkHttp 進行網路請求
                            LogUtils.d(call.request().url().toString());
                        }
                    })
                    .build();
            registry.replace(GlideUrl.class, InputStream.class, new Factory(okHttpClient));
        }

        @Override
        public boolean isManifestParsingEnabled() {
            // 不使用 Manifest 中的 GlideModule
            return false;
        }
    }
複製程式碼

這樣我們通過自己的配置指定網路中圖片載入需要使用 OkHttp. 並且自定義了 OkHttp 的超時時間等引數。按照上面的方式我們可以在 Glide 中使用 OkHttp 來載入網路中的圖片了。

不過,當我們在專案中引用了 okhttp3-integration 的依賴之後,不進行上述配置一樣可以使用 OkHttp 來進行網路圖片載入的。這是因為上述依賴的包中已經提供了一個自定義的 GlideModule,即 OkHttpLibraryGlideModule。該類使用了 @GlideModule 註解,並且已經指定了網路圖片載入使用 OkHttp。所以,當我們不自定義 GlideModule 的時候,只使用它一樣可以在 Glide 中使用 OkHttp.

如果我們使用了自定義的 GlideModule,當我們編譯的時候會看到 GeneratedAppGlideModuleImpl 中的 registerComponents() 方法定義如下:

  @Override
  public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
    new OkHttpLibraryGlideModule().registerComponents(context, glide, registry);
    appGlideModule.registerComponents(context, glide, registry);
  }
複製程式碼

這裡先呼叫了 OkHttpLibraryGlideModuleregisterComponents() 方法,然後呼叫了我們自定義的 GlideModule 的 registerComponents() 方法,只是,我們的 GlideModule 的 registerComponents() 方法會覆蓋掉 OkHttpLibraryGlideModule 中的實現。(因為我們的 GlideModule 的 registerComponents() 方法中呼叫的是 Registryreplace() 方法,會替換之前的效果。)

如果不希望多此一舉,我們可以直接在自定義的 GlideModule 中使用 @Excludes 註解,並指定 OkHttpLibraryGlideModule 來直接排除該類。這樣 GeneratedAppGlideModuleImpl 中的 registerComponents() 方法將只使用我們自定義的 GlideModule. 以下是排除之後生成的類中 registerComponents() 方法的實現:

  @Override
  public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
    appGlideModule.registerComponents(context, glide, registry);
  }
複製程式碼

3、總結

在本文中,我們通過介紹 Glide 的兩種常見的配置方式來分析了 Glide 的部分原始碼實現。在這部分中,我們重點介紹了初始化 Glide 的並獲取 GlideModule 的過程,以及與圖片資源的時候相關的 ModelLoader 等的原始碼。瞭解這部分內容是比較重要的,因為它們是暴露給使用者的 API 介面,比較常用;並且對這些類簡單瞭解之後能夠不至於在隨後分析 Glide 整個載入流程的時候迷路。

這裡我們對上面兩種配置方式中涉及到的類進行一個分析。如下圖所示

Glide原始碼

當我們初始化 Glide 的時候會使用 Registryappend() 等一系列的方法構建資源型別-載入方式-輸出型別 的一個對映,然後當我們使用 Glide 進行記載的時候,會先根據資源型別找到對應的載入方式,然後使用該載入方式從指定的資料來源中載入資料,並將其轉換成指定的輸出型別。

以上面我們自定義圖片載入方式的過程為例,這裡我們自定義了一個資源型別 CacheImage,並通過自定義 GlideModule 指定了它的載入實現是我們自定義的 ImageLoader 類。然後,在我們自定義的 ImageLoader 中,我們指定了獲取該資源的快取的鍵的方式和從資料來源中記載資料的具體實現 HttpUrlFetcher。這樣,當 Glide 要載入某個 CacheImage 的時候,會先使用該快取的鍵嘗試從快取中獲取,拿不到結果之後使用 HttpUrlFetcher 從網路當中獲取資料。從網路中獲取資料的時候會得到 InputStream,最後,再呼叫一個回撥類,使用 BitmapFactory 從 InputStream 中獲取 Bitmap 並將其顯示到 ImageView 上面,這樣就完成了整個圖片載入的流程。

從上文的分析中,我們可以總結出 Glide 的幾個設計人性的地方:

  1. 使用代理類包裝自定義 GlideModule,然後可以使用發射獲取該代理類,並通過呼叫代理類的方法來間接呼叫我們的 GlideModuel;
  2. 構建資源型別-載入方式-輸出型別對映的時候使用工廠方法而不是通過某個類建立一對一對映。

上面我們通過 Glide 的幾種配置方式簡單介紹了 Glide 的圖片載入流程。其實際的執行過程遠比我們上述過程更加複雜。在下文中我們會對 Glide 的圖片載入的主流程進行分析。歡迎繼續關注和閱讀!

  1. Glide 系列-1:預熱、Glide 的常用配置方式及其原理
  2. Glide 系列-2:主流程原始碼分析(4.8.0)
  3. Glide 系列-3:Glide 快取的實現原理(4.8.0)

如果您喜歡我的文章,可以在以下平臺關注我:

更多文章:Gihub: Android-notes

相關文章