三方庫源碼筆記(10)-Glide 你可能不知道的知識點

前陣子定了個小目標,打算來深入瞭解下幾個常用的開源庫,看下其源碼和實現原理,進行總結並輸出成文章。初定的目標是 EventBus、ARouter、LeakCanary、Retrofit、Glide、Coil、OkHttp 等七個。目前已經完成了九篇關於 EventBus、ARouter、LeakCanary、Retrofit、Glide 的文章,本篇是第十篇,來對 Glide 的一些擴展知識點進行講解,希望對你有所幫助😎😎

一、利用 AppGlideModule 實現默認配置

在大多數情況下 Glide 的默認配置就已經能夠滿足我們的需求了,像緩存池大小,磁盤緩存策略等都不需要我們主動去設置,但 Glide 也提供了 AppGlideModule 讓開發者可以去實現自定義配置。對於一個 App 來說,在加載圖片的時候一般都是使用同一張 placeholder,如果每次加載圖片時都需要來手動設置一遍的話就顯得很多餘了,此時就可以通過 AppGlideModule 來設置默認的 placeholder

首先需要繼承於 AppGlideModule,在 applyOptions方法中設置配置參數,然後爲實現類添加 @GlideModule 註解,這樣在編譯階段 Glide 就可以通過 APT 解析到我們的這一個實現類,然後將我們的配置參數設置爲默認值

/**
 * 作者:leavesC
 * 時間:2020/11/5 23:16
 * 描述:
 * GitHub:https://github.com/leavesC
 */
@GlideModule
class MyAppGlideModule : AppGlideModule() {

    //用於控制是否需要從 Manifest 文件中解析配置文件
    override fun isManifestParsingEnabled(): Boolean {
        return false
    }

    override fun applyOptions(context: Context, builder: GlideBuilder) {
        builder.setDiskCache(
            //配置磁盤緩存目錄和最大緩存
            DiskLruCacheFactory(
                (context.externalCacheDir ?: context.cacheDir).absolutePath,
                "imageCache",
                1024 * 1024 * 50
            )
        )
        builder.setDefaultRequestOptions {
            return@setDefaultRequestOptions RequestOptions()
                .placeholder(android.R.drawable.ic_menu_upload_you_tube)
                .error(android.R.drawable.ic_menu_call)
                .diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
                .format(DecodeFormat.DEFAULT)
                .encodeQuality(90)
        }
    }

    override fun registerComponents(context: Context, glide: Glide, registry: Registry) {

    }

}

在編譯後,我們的工程目錄中就會自動生成 GeneratedAppGlideModuleImpl 這個類,該類就包含了 MyAppGlideModule

@SuppressWarnings("deprecation")
final class GeneratedAppGlideModuleImpl extends GeneratedAppGlideModule {
  private final MyAppGlideModule appGlideModule;

  public GeneratedAppGlideModuleImpl(Context context) {
    appGlideModule = new MyAppGlideModule();
    if (Log.isLoggable("Glide", Log.DEBUG)) {
      Log.d("Glide", "Discovered AppGlideModule from annotation: github.leavesc.glide.MyAppGlideModule");
    }
  }

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

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

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

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

  @Override
  @NonNull
  GeneratedRequestManagerFactory getRequestManagerFactory() {
    return new GeneratedRequestManagerFactory();
  }
}

在運行階段,Glide 就會通過反射生成一個 GeneratedAppGlideModuleImpl 對象,然後根據我們的默認配置項來初始化 Glide 實例

 @Nullable
  @SuppressWarnings({"unchecked", "TryWithIdenticalCatches", "PMD.UnusedFormalParameter"})
  private static GeneratedAppGlideModule getAnnotationGeneratedGlideModules(Context context) {
    GeneratedAppGlideModule result = null;
    try {
      //通過反射來生成一個 GeneratedAppGlideModuleImpl 對象
      Class<GeneratedAppGlideModule> clazz =
          (Class<GeneratedAppGlideModule>)
              Class.forName("com.bumptech.glide.GeneratedAppGlideModuleImpl");
      result =
          clazz.getDeclaredConstructor(Context.class).newInstance(context.getApplicationContext());
    } catch (ClassNotFoundException e) {
      if (Log.isLoggable(TAG, Log.WARN)) {
        Log.w(
            TAG,
            "Failed to find GeneratedAppGlideModule. You should include an"
                + " annotationProcessor compile dependency on com.github.bumptech.glide:compiler"
                + " in your application and a @GlideModule annotated AppGlideModule implementation"
                + " or LibraryGlideModules will be silently ignored");
      }
      // These exceptions can't be squashed across all versions of Android.
    } catch (InstantiationException e) {
      throwIncorrectGlideModule(e);
    } catch (IllegalAccessException e) {
      throwIncorrectGlideModule(e);
    } catch (NoSuchMethodException e) {
      throwIncorrectGlideModule(e);
    } catch (InvocationTargetException e) {
      throwIncorrectGlideModule(e);
    }
    return result;
  }


 private static void initializeGlide(
      @NonNull Context context,
      @NonNull GlideBuilder builder,
      @Nullable GeneratedAppGlideModule annotationGeneratedModule) {
    Context applicationContext = context.getApplicationContext();
    ···
    if (annotationGeneratedModule != null) {
      //調用 MyAppGlideModule 的 applyOptions 方法,對 GlideBuilder 進行設置
      annotationGeneratedModule.applyOptions(applicationContext, builder);
    }
    //根據 GlideBuilder 來生成 Glide 實例
    Glide glide = builder.build(applicationContext);
    ···
    if (annotationGeneratedModule != null) {
        //配置自定義組件
        annotationGeneratedModule.registerComponents(applicationContext, glide, glide.registry);
    }
    applicationContext.registerComponentCallbacks(glide);
    Glide.glide = glide;
  }

二、自定義網絡請求組件

默認情況下,Glide 是通過 HttpURLConnection 來進行聯網請求圖片的,這個過程就由 HttpUrlFetcher 類來實現。HttpURLConnection 相對於我們常用的 OkHttp 來說比較原始低效,我們可以通過使用 Glide 官方提供的okhttp3-integration來將網絡請求交由 OkHttp 完成

dependencies {
    implementation "com.github.bumptech.glide:okhttp3-integration:4.11.0"
}

如果想方便後續修改的話,我們也可以將okhttp3-integration內的代碼複製出來,通過 Glide 開放的 Registry 來註冊一個自定義的 OkHttpStreamFetcher,這裏我也提供一份 kotlin 版本的示例代碼

首先需要繼承於 DataFetcher,在拿到 GlideUrl 後完成網絡請求,並將請求結果通過 DataCallback 回調出去

/**
 * 作者:leavesC
 * 時間:2020/11/5 23:16
 * 描述:
 * GitHub:https://github.com/leavesC
 */
class OkHttpStreamFetcher(private val client: Call.Factory, private val url: GlideUrl) :
    DataFetcher<InputStream>, Callback {

    companion object {
        private const val TAG = "OkHttpFetcher"
    }

    private var stream: InputStream? = null

    private var responseBody: ResponseBody? = null

    private var callback: DataFetcher.DataCallback<in InputStream>? = null

    @Volatile
    private var call: Call? = null

    override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
        val requestBuilder = Request.Builder().url(url.toStringUrl())
        for ((key, value) in url.headers) {
            requestBuilder.addHeader(key, value)
        }
        val request = requestBuilder.build()
        this.callback = callback
        call = client.newCall(request)
        call?.enqueue(this)
    }

    override fun onFailure(call: Call, e: IOException) {
        if (Log.isLoggable(TAG, Log.DEBUG)) {
            Log.d(TAG, "OkHttp failed to obtain result", e)
        }
        callback?.onLoadFailed(e)
    }

    override fun onResponse(call: Call, response: Response) {
        if (response.isSuccessful) {
            responseBody = response.body()
            val contentLength = Preconditions.checkNotNull(responseBody).contentLength()
            stream = ContentLengthInputStream.obtain(responseBody!!.byteStream(), contentLength)
            callback?.onDataReady(stream)
        } else {
            callback?.onLoadFailed(HttpException(response.message(), response.code()))
        }
    }

    override fun cleanup() {
        try {
            stream?.close()
        } catch (e: IOException) {
            // Ignored
        }
        responseBody?.close()
        callback = null
    }

    override fun cancel() {
        call?.cancel()
    }

    override fun getDataClass(): Class<InputStream> {
        return InputStream::class.java
    }

    override fun getDataSource(): DataSource {
        return DataSource.REMOTE
    }

}

之後還需要繼承於 ModelLoader,提供構建 OkHttpUrlLoader 的入口

/**
 * 作者:leavesC
 * 時間:2020/11/5 23:16
 * 描述:
 * GitHub:https://github.com/leavesC
 */
class OkHttpUrlLoader(private val client: Call.Factory) : ModelLoader<GlideUrl, InputStream> {

    override fun buildLoadData(
        model: GlideUrl,
        width: Int,
        height: Int,
        options: Options
    ): LoadData<InputStream> {
        return LoadData(
            model,
            OkHttpStreamFetcher(client, model)
        )
    }

    override fun handles(model: GlideUrl): Boolean {
        return true
    }

    class Factory(private val client: Call.Factory) : ModelLoaderFactory<GlideUrl, InputStream> {

        override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<GlideUrl, InputStream> {
            return OkHttpUrlLoader(client)
        }

        override fun teardown() {
            // Do nothing, this instance doesn't own the client.
        }

    }

}

最後註冊 OkHttpUrlLoader ,之後 GlideUrl 類型的請求都會交由其處理

/**
 * 作者:leavesC
 * 時間:2020/11/5 23:16
 * 描述:
 * GitHub:https://github.com/leavesC
 */
@GlideModule
class MyAppGlideModule : AppGlideModule() {

    override fun isManifestParsingEnabled(): Boolean {
        return false
    }

    override fun applyOptions(context: Context, builder: GlideBuilder) {

    }

    override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
        registry.replace(
            GlideUrl::class.java,
            InputStream::class.java,
            OkHttpUrlLoader.Factory(OkHttpClient())
        )
    }

}

三、實現圖片加載進度監聽

對於某些高清圖片來說,可能一張就是十幾MB甚至上百MB大小了,如果沒有進度條的話用戶可能就會等得有點難受了,這裏我就提供一個基於 OkHttp 攔截器實現的監聽圖片加載進度的方法

首先需要對 OkHttp 原始的 ResponseBody 進行一層包裝,在內部根據 contentLength已讀取到的流字節數來計算當前進度值,然後向外部提供通過 imageUrl 來註冊 ProgressListener 的入口

/**
 * 作者:leavesC
 * 時間:2020/11/6 21:58
 * 描述:
 * GitHub:https://github.com/leavesC
 */
internal class ProgressResponseBody constructor(
    private val imageUrl: String,
    private val responseBody: ResponseBody?
) : ResponseBody() {

    interface ProgressListener {

        fun update(progress: Int)

    }

    companion object {

        private val progressMap = mutableMapOf<String, WeakReference<ProgressListener>>()

        fun addProgressListener(url: String, listener: ProgressListener) {
            progressMap[url] = WeakReference(listener)
        }

        fun removeProgressListener(url: String) {
            progressMap.remove(url)
        }

        private const val CODE_PROGRESS = 100

        private val mainHandler by lazy {
            object : Handler(Looper.getMainLooper()) {
                override fun handleMessage(msg: Message) {
                    if (msg.what == CODE_PROGRESS) {
                        val pair = msg.obj as Pair<String, Int>
                        val progressListener = progressMap[pair.first]?.get()
                        progressListener?.update(pair.second)
                    }
                }
            }
        }

    }

    private var bufferedSource: BufferedSource? = null

    override fun contentType(): MediaType? {
        return responseBody?.contentType()
    }

    override fun contentLength(): Long {
        return responseBody?.contentLength() ?: -1
    }

    override fun source(): BufferedSource {
        if (bufferedSource == null) {
            bufferedSource = source(responseBody!!.source()).buffer()
        }
        return bufferedSource!!
    }

    private fun source(source: Source): Source {
        return object : ForwardingSource(source) {

            var totalBytesRead = 0L

            @Throws(IOException::class)
            override fun read(sink: Buffer, byteCount: Long): Long {
                val bytesRead = super.read(sink, byteCount)
                totalBytesRead += if (bytesRead != -1L) {
                    bytesRead
                } else {
                    0
                }
                val contentLength = contentLength()
                val progress = when {
                    bytesRead == -1L -> {
                        100
                    }
                    contentLength != -1L -> {
                        ((totalBytesRead * 1.0 / contentLength) * 100).toInt()
                    }
                    else -> {
                        0
                    }
                }
                mainHandler.sendMessage(Message().apply {
                    what = CODE_PROGRESS
                    obj = Pair(imageUrl, progress)
                })
                return bytesRead
            }
        }
    }

}

然後在 Interceptor 中使用 ProgressResponseBody 對原始的 ResponseBody 多進行一層包裝,將我們的 ProgressResponseBody 作爲一個代理,之後再將 ProgressInterceptor 添加給 OkHttpClient 即可

/**
 * 作者:leavesC
 * 時間:2020/11/6 22:08
 * 描述:
 * GitHub:https://github.com/leavesC
 */
class ProgressInterceptor : Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        val originalResponse = chain.proceed(request)
        val url = request.url.toString()
        return originalResponse.newBuilder()
            .body(ProgressResponseBody(url, originalResponse.body))
            .build()
    }

}

最終實現的效果:

四、自定義磁盤緩存 key

在某些時候,我們拿到的圖片 Url 可能是帶有時效性的,需要在 Url 的尾部加上一個 token 值,在指定時間後 token 就會失效,防止圖片被盜鏈。這種類型的 Url 在一定時間內就需要更換 token 才能拿到圖片,可是 Url 的變化就會導致 Glide 的磁盤緩存機制完全失效

https://images.pexels.com/photos/1425174/pexels-photo-1425174.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260&token=tokenValue

從我的上篇文章內容可以知道,一張圖片在進行磁盤緩存時必定會同時對應一個唯一 Key,這樣 Glide 在後續加載同樣的圖片時才能複用已有的緩存文件。對於一張網絡圖片來說,其唯一 Key 的生成就依賴於 GlideUrl 類的 getCacheKey()方法,該方法會直接返回網絡圖片的 Url 字符串。如果 Url 的 token 值會一直變化,那麼 Glide 就無法對應上同一張圖片了,導致磁盤緩存完全失效

/**
 * @Author: leavesC
 * @Date: 2020/11/6 15:13
 * @Desc:
 * GitHub:https://github.com/leavesC
 */
public class GlideUrl implements Key {
    
  @Nullable private final String stringUrl;
    
  public GlideUrl(String url) {
    this(url, Headers.DEFAULT);
  }

  public GlideUrl(String url, Headers headers) {
    this.url = null;
    this.stringUrl = Preconditions.checkNotEmpty(url);
    this.headers = Preconditions.checkNotNull(headers);
  }
    
  public String getCacheKey() {
    return stringUrl != null ? stringUrl : Preconditions.checkNotNull(url).toString();
  }
    
}

想要解決這個問題,就需要來手動定義磁盤緩存時的唯一 Key。這可以通過繼承 GlideUrl,修改getCacheKey()方法的返回值來實現,將 Url 移除 token 鍵值對後的字符串作爲緩存 Key 即可

/**
 * @Author: leavesC
 * @Date: 2020/11/6 15:13
 * @Desc:
 * GitHub:https://github.com/leavesC
 */
class TokenGlideUrl(private val selfUrl: String) : GlideUrl(selfUrl) {

    override fun getCacheKey(): String {
        val uri = URI(selfUrl)
        val querySplit = uri.query.split("&".toRegex())
        querySplit.forEach {
            val kv = it.split("=".toRegex())
            if (kv.size == 2 && kv[0] == "token") {
                //將包含 token 的鍵值對移除
                return selfUrl.replace(it, "")
            }
        }
        return selfUrl
    }

}

然後在加載圖片的時候使用 TokenGlideUrl 來傳遞圖片 Url 即可

      Glide.with(Context).load(TokenGlideUrl(ImageUrl)).into(ImageView)

五、如何直接拿到圖片

如果想直接取得 Bitmap 而非顯示在 ImageView 上的話,可以用以下同步請求的方式來獲得 Bitmap。需要注意的是,submit()方法就會觸發 Glide 去請求圖片,此時請求操作還是運行於 Glide 內部的線程池的,但 get()操作就會直接阻塞所在線程,直到圖片加載結束(不管成功與否)纔會返回

            thread {
                val futureTarget = Glide.with(this)
                    .asBitmap()
                    .load(url)
                    .submit()
                val bitmap = futureTarget.get()
                runOnUiThread {
                    iv_tokenUrl.setImageBitmap(bitmap)
                }
            }

也可以用類似的方式來拿到 File 或者 Drawable

            thread {
                val futureTarget = Glide.with(this)
                    .asFile()
                    .load(url)
                    .submit()
                val file = futureTarget.get()
                runOnUiThread {
                    showToast(file.absolutePath)
                }
            }

Glide 也提供了以下的異步加載方式

            Glide.with(this)
                .asBitmap()
                .load(url)
                .into(object : CustomTarget<Bitmap>() {
                    override fun onLoadCleared(placeholder: Drawable?) {
                        showToast("onLoadCleared")
                    }

                    override fun onResourceReady(
                        resource: Bitmap,
                        transition: Transition<in Bitmap>?
                    ) {
                        iv_tokenUrl.setImageBitmap(resource)
                    }
                })

六、Glide 如何實現網絡監聽

在上篇文章我有講到,RequestTracker 就用於存儲所有加載圖片的任務,並提供了開始、暫停和重啓所有任務的方法,一個常見的需要重啓任務的情形就是用戶的網絡從無信號狀態恢復正常了,此時就應該自動重啓所有未完成的任務

ConnectivityMonitor  connectivityMonitor =
        factory.build(
            context.getApplicationContext(),
            new RequestManagerConnectivityListener(requestTracker));  


private class RequestManagerConnectivityListener
      implements ConnectivityMonitor.ConnectivityListener {
    @GuardedBy("RequestManager.this")
    private final RequestTracker requestTracker;

    RequestManagerConnectivityListener(@NonNull RequestTracker requestTracker) {
      this.requestTracker = requestTracker;
    }

    @Override
    public void onConnectivityChanged(boolean isConnected) {
      if (isConnected) {
        synchronized (RequestManager.this) {
          //重啓未完成的任務
          requestTracker.restartRequests();
        }
      }
    }
  }

可以看出來,RequestManagerConnectivityListener 本身就只是一個回調函數,重點還需要看 ConnectivityMonitor 是如何實現的。ConnectivityMonitor 實現類就在 DefaultConnectivityMonitorFactory 中獲取,內部會判斷當前應用是否具有 NETWORK_PERMISSION 權限,如果沒有的話則返回一個空實現 NullConnectivityMonitor,有權限的話就返回 DefaultConnectivityMonitor,在內部根據 ConnectivityManager 來判斷當前的網絡連接狀態

public class DefaultConnectivityMonitorFactory implements ConnectivityMonitorFactory {
  private static final String TAG = "ConnectivityMonitor";
  private static final String NETWORK_PERMISSION = "android.permission.ACCESS_NETWORK_STATE";

  @NonNull
  @Override
  public ConnectivityMonitor build(
      @NonNull Context context, @NonNull ConnectivityMonitor.ConnectivityListener listener) {
    int permissionResult = ContextCompat.checkSelfPermission(context, NETWORK_PERMISSION);
    boolean hasPermission = permissionResult == PackageManager.PERMISSION_GRANTED;
    if (Log.isLoggable(TAG, Log.DEBUG)) {
      Log.d(
          TAG,
          hasPermission
              ? "ACCESS_NETWORK_STATE permission granted, registering connectivity monitor"
              : "ACCESS_NETWORK_STATE permission missing, cannot register connectivity monitor");
    }
    return hasPermission
        ? new DefaultConnectivityMonitor(context, listener)
        : new NullConnectivityMonitor();
  }
}

DefaultConnectivityMonitor 的邏輯比較簡單,不過多贅述。我覺得比較有價值的一點是:Glide 由於使用人數衆多,有比較多的開發者會反饋 issues,DefaultConnectivityMonitor 內部就對各種可能拋出 Exception 的情況進行了捕獲,這樣相對來說會比我們自己實現的邏輯要考慮周全得多,所以我就把 DefaultConnectivityMonitor 複製出來轉爲 kotlin 以便後續自己複用了

/**
 * @Author: leavesC
 * @Date: 2020/11/7 14:40
 * @Desc:
 */
internal interface ConnectivityListener {
    fun onConnectivityChanged(isConnected: Boolean)
}

internal class DefaultConnectivityMonitor(
    context: Context,
    val listener: ConnectivityListener
) {

    private val appContext = context.applicationContext

    private var isConnected = false

    private var isRegistered = false

    private val connectivityReceiver: BroadcastReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
            val wasConnected = isConnected
            isConnected = isConnected(context)
            if (wasConnected != isConnected) {
                listener.onConnectivityChanged(isConnected)
            }
        }
    }

    private fun register() {
        if (isRegistered) {
            return
        }
        // Initialize isConnected.
        isConnected = isConnected(appContext)
        try {
            appContext.registerReceiver(
                connectivityReceiver,
                IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
            )
            isRegistered = true
        } catch (e: SecurityException) {
            e.printStackTrace()
        }
    }

    private fun unregister() {
        if (!isRegistered) {
            return
        }
        appContext.unregisterReceiver(connectivityReceiver)
        isRegistered = false
    }

    @SuppressLint("MissingPermission")
    private fun isConnected(context: Context): Boolean {
        val connectivityManager =
            context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager
                ?: return true
        val networkInfo = try {
            connectivityManager.activeNetworkInfo
        } catch (e: RuntimeException) {
            return true
        }
        return networkInfo != null && networkInfo.isConnected
    }

    fun onStart() {
        register()
    }

    fun onStop() {
        unregister()
    }

}

七、結尾

關於 Glide 的知識點擴展也介紹完了,上述的所有示例代碼我也都放到 GitHub 了,歡迎 star:AndroidOpenSourceDemo

一個人走得快,一羣人走得遠,寫了文章就只有自己看那得有多孤單,只希望對你有所幫助😂😂😂

查看更多文章請點擊關注:字節數組

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章