天天看點

glide加載網絡圖檔_三方庫源碼筆記(10)-Glide 你可能不知道的知識點

glide加載網絡圖檔_三方庫源碼筆記(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 {
            [email protected] 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()
    }

}
           

最終實作的效果:

glide加載網絡圖檔_三方庫源碼筆記(10)-Glide 你可能不知道的知識點

四、自定義磁盤緩存 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​github.com