三方庫源碼筆記(13)-可能是全網第一篇 Coil 的源碼分析文章

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

Coil 是我最後一個要來分析的開源庫,本篇也是我 三方庫源碼筆記 這個系列的最後一篇文章了,包含 Coil 的入門介紹和源碼分析。這一整個系列的文章我從國慶寫到現在也是要兩個月了,到今天也就結尾了,原創不易,覺得有用就請給個贊吧😂😂

Coil 這個開源庫我關注了蠻久的,因爲其很多特性在我看來都挺有意思的,Coil 在2020年10月22日才發佈了 1.0.0 版本,還熱乎着呢。我在網上搜了搜 Coil 的資料,看到的文章都只是入門介紹,沒看見到關於源碼層次的分析,而且本文寫好的時候離 1.0.0 版本發佈剛好才隔了一個月時間,應該沒人比我還早了吧?就斗膽給文章起了這麼個標題:可能是全網第一篇 Coil 的源碼分析文章 ~~~

一、Coil 是什麼

Coil 是一個新興的 Android 圖片加載庫,使用 Kotlin 協程來加載圖片,有以下幾個特點:

  • 更快: Coil 在性能上做了很多優化,包括內存緩存和磁盤緩存、對內存中的圖片進行採樣、複用 Bitmap、支持根據生命週期變化自動暫停和取消圖片請求等
  • 更輕量級: Coil 大約會給你的 App 增加兩千個方法(前提是你的 App 已經集成了 OkHttp 和 Coroutines),Coil 的方法數和 Picasso 相當,相比 Glide 和 Fresco 要輕量級很多
  • 更容易使用: Coil's API 充分利用了 Kotlin 語言的新特性,簡化並減少了很多重複代碼
  • 更流行: Coil 首選 Kotlin 語言開發,並且使用包含 Coroutines、OkHttp、Okio 和 AndroidX Lifecycles 在內的更現代化的開源庫

Coil 的首字母由來:Coroutine,Image 和 Loader 得到 Coil

二、引入 Coil

Coil 要求 AndroidX、Min SDK 14+、Java 8+ 環境

要啓用 Java 8,需要在項目的 Gradle 構建腳本中添加如下配置:

Gradle (.gradle):

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = "1.8"
    }

Gradle Kotlin DSL (.gradle.kts):

    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = "1.8"
    }

Coil 一共包含五個組件,可以在 mavenCentral()上獲取到

  • io.coil-kt:coil-base。基礎組件,提供了基本的圖片請求、圖片解碼、圖片緩存、Bitmap 複用等功能
  • io.coil-kt:coil。默認組件,依賴於io.coil-kt:coil-base,提供了 Coil 類的單例對象以及 ImageView 相關的擴展函數
  • io.coil-kt:coil-gif。包含兩個 decoder 用於支持解碼 GIFs,有關更多詳細信息,請參見 GIF
  • io.coil-kt:coil-svg。包含一個 decoder 用於支持解碼 SVG。有關更多詳細信息,請參見 SVG
  • io.coil-kt:coil-video。包含兩個 fetchers 用於支持讀取和解碼 任何 Android 的支持的視頻格式 的視頻幀。有關更多詳細信息,請參見 videos

當前 Coil 最新的 release 版本是 1.0.0,引入如下依賴就包含了 Coil 最基礎的圖片加載功能

implementation("io.coil-kt:coil:1.0.0")

如果想要顯示 Gif、SVG、視頻幀等類型的圖片,則需要額外引入對應的支持庫:

implementation("io.coil-kt:coil-gif:1.0.0")
implementation("io.coil-kt:coil-svg:1.0.0")
implementation("io.coil-kt:coil-video:1.0.0")

三、快速入門

1、load

要將圖片顯示到 ImageView 上,直接使用ImageView的擴展函數load即可

// URL
imageView.load("https://www.example.com/image.jpg")

// Resource
imageView.load(R.drawable.image)

// File
imageView.load(File("/path/to/image.jpg"))

// And more...

使用可選的 lambda 塊來添加配置項

imageView.load("https://www.example.com/image.jpg") {
    crossfade(true) //淡入淡出
    placeholder(R.drawable.image) //佔位圖
    transformations(CircleCropTransformation()) //圖片變換,將圖片轉爲圓形
}

2、ImageRequest

如果要將圖片加載到自定義的 target 中,可以通過 ImageRequest.Builder 來構建 ImageRequest 實例,並將請求提交給 ImageLoader

        val request = ImageRequest.Builder(context)
            .data("https://www.example.com/image.jpg")
            .target { drawable ->
                // Handle the result.
            }
            .build()
        context.imageLoader.enqueue(request)

3、ImageLoader

imageView.load使用單例對象 imageLoader 來執行 ImageRequest,可以使用 Context 的擴展函數來訪問 ImageLoader

val imageLoader = context.imageLoader

可選地,你也可以構建自己的 ImageLoader 實例,並賦值給 Coil 來實現全局使用

       Coil.setImageLoader(
            ImageLoader.Builder(application)
                .placeholder(ActivityCompat.getDrawable(application, R.drawable.icon_loading))
                .error(ActivityCompat.getDrawable(application, R.drawable.icon_error))
                .build()
        )

4、execute

如果想直接拿到目標圖片,可以調用 ImageLoader 的execute方法來實現

val request = ImageRequest.Builder(context)
    .data("https://www.example.com/image.jpg")
    .build()
val drawable = imageLoader.execute(request).drawable

5、R8 / Proguard

Coil 開箱即用,與 R8 完全兼容,不需要添加任何額外規則

如果你使用了 Proguard,你可能需要添加對應的混淆規則:CoroutinesOkHttp and Okio

6、License

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

四、大體框架

Coil 在我看來是一個比較“激進”的開源庫,熱衷於使用當前最爲流行的技術,包括 Coroutines、OkHttp、Okio,以及 Google 官方的 Jetpack Lifecycles、AndroidX 等,代碼不僅完全由 Kotlin 語言來實現,連 gradle 腳本也是全部使用 kts,而且 gradle 版本也升級得很快,我一開始由於使用的 Android Studio 不是 4.x 版本,連 Coil 代碼都跑不起來 =_=

如果你的項目中已經大面積使用到了這些開源庫和組件的話,那麼 Coil 會更加契合你的項目

當前在 Android 端最爲流行的圖片加載框架應該是 Glide 了,Coil 作爲一個後起之秀相對 Glide 也有着一些獨特的優勢。例如,爲了監聽 UI 層的生命週期變化,Glide 是通過向 Activity 或者 Fragment 注入一個無 UI 界面的 Fragment 來實現間接監聽的,而 Coil 則只需要直接監聽 Lifecycle 即可,在實現方式上 Coil 會更加簡單高效。此外,在聯網請求圖片的時候,Glide 需要通過線程池和多個回調來完成最終圖片的顯示,而 Coil 由於使用了 Kotlin 協程,可以很簡潔地完成異步加載和線程切換,在流程上 Coil 會清晰很多。但實際上 Coil 也是借鑑了一些優秀開源庫的實現思路,所以我看 Coil 的源碼的時候就總會發現一些 Glide 和 OkHttp 的影子😅😅

這裏就先來對 Coil 的各個特性和 Glide 做下簡單的對比,先讓讀者有個大體的印象

  1. 實現語言
    • Glide 全盤使用 Java 語言來實現,對於 Java 和 Kotlin 語言的友好程度差不多
    • Coil 全盤使用 Kotlin 語言來實現,爲 ImageView 聲明瞭多個用於加載圖片的擴展函數,對 Kotlin 語言的友好程度會更高很多
  2. 網絡請求
    • Glide 默認是使用 HttpURLConnection,但也提供了更換網絡請求實現途徑的入口
    • Coil 默認是使用 OkHttp,但也提供了更換網絡請求實現途徑的入口
  3. 生命週期監聽
    • Glide 通過向 Activity 或者 Fragment 注入一個無 UI 界面的 Fragment 來實現監聽
    • Coil 直接通過 Lifecycle 來實現監聽
  4. 內存緩存
    • Glide 的內存緩存分爲 ActiveResources 和 MemoryCache 兩級
    • Coil 的內存緩存分爲 WeakMemoryCache 和 StrongMemoryCache 兩級,本質上和 Glide 一樣
  5. 磁盤緩存
    • Glide 在加載到圖片後通過 DiskLruCache 來進行磁盤緩存,且提供了是否緩存、是否緩存原始圖片、是否緩存轉換過後的圖片等多個選擇
    • Coil 通過 OkHttp 的網絡請求緩存機制來實現磁盤緩存,且磁盤緩存只對通過網絡請求加載到的原始圖片生效,不緩存其它來源的圖片和轉換過後的圖片
  6. 網絡緩存
    • Glide 不存在這個概念
    • Coil 相比 Glide 多出了一層網絡緩存,可用於實現不進行網絡加載,而是強制使用本地緩存(當然,如果本地緩存不存在的話就會報錯)
  7. 線程框架
    • Glide 使用原生的 ThreadPoolExecutor 來完成後臺任務,通過 Handler 來實現線程切換
    • Coil 使用 Coroutines 來完成後臺任務及線程切換

關於 Glide 的源碼解析可以看我的這兩篇文章:

我看源碼時習慣從最基礎的使用方式來入手,分析其整個調用鏈關係和會涉及到的模塊,這裏也不例外,就從 Coil 加載一張網絡圖片來入手

最簡單的加載方式只需要調用一個load方法即可,比 Glide 還簡潔,想要添加配置項的話就在 lambda 塊中添加

            //直接加載圖片,不添加任何配置項
            imageView.load(imageUrl)

            //在 lambda 塊中添加配置項
            imageView.load(imageUrl) {
                crossfade(true) //淡入淡出
                placeholder(android.R.drawable.presence_away) //佔位圖
                error(android.R.drawable.stat_notify_error) //圖片加載失敗時顯示的圖
                transformations(
                    CircleCropTransformation() //將圖片顯示爲圓形
                )
            }

Coil 爲 ImageView 聲明瞭多個用於加載圖片的擴展函數,均命名爲 load,默認情況下我們只需要傳一個圖片來源地址即可,支持 String、HttpUrl、Uri、File、Int、Drawable、Bitmap 等多種入參類型

/** @see ImageView.loadAny */
@JvmSynthetic
inline fun ImageView.load(
    uri: String?,
    imageLoader: ImageLoader = context.imageLoader,
    builder: ImageRequest.Builder.() -> Unit = {}
): Disposable = loadAny(uri, imageLoader, builder)

不管傳入的是什麼類型的參數,最終都會中轉調用到 loadAny 方法,通過 Builder 模式構建出本次的請求參數 ImageRequest,然後將 ImageRequest 提交給 ImageLoader,由其來完成圖片的加載,最終返回一個 Disposable 對象

@JvmSynthetic
inline fun ImageView.loadAny(
    data: Any?,
    imageLoader: ImageLoader = context.imageLoader,
    builder: ImageRequest.Builder.() -> Unit = {}
): Disposable {
    val request = ImageRequest.Builder(context)
        .data(data)
        .target(this)
        .apply(builder)
        .build()
    return imageLoader.enqueue(request)
}

所以,一個簡單的 load 方法就已經使用到了以下幾個類:

  1. ImageRequest。圖片的請求參數
  2. Disposable。用於取消圖片加載或者等待圖片加載完成
  3. ImageLoader。向其提交 ImageRequest ,由其完成圖片的加載

下面就來分析下這一整個流程

五、ImageRequest

ImageRequest 基於 Builder 模式來構建,包含了加載圖片時的各個配置項,其配置項很多,重點看前九個

        private val context: Context //外部傳入的 Context,例如 ImageView 包含的 Context
        private var data: Any? //圖片地址
        private var target: Target? //圖片加載成功後的接收類
        private var lifecycle: Lifecycle? //ImageView 關聯的生命週期
        private var memoryCachePolicy: CachePolicy? //內存緩存配置
        private var diskCachePolicy: CachePolicy? //磁盤緩存配置
        private var networkCachePolicy: CachePolicy? //網絡緩存配置
        private var fetcher: Pair<Fetcher<*>, Class<*>>? //完成圖片加載的處理器
        private var decoder: Decoder? //完成圖片轉碼的轉換器

        private var defaults: DefaultRequestOptions
        private var listener: Listener?
        private var memoryCacheKey: MemoryCache.Key?
        private var placeholderMemoryCacheKey: MemoryCache.Key?
        private var colorSpace: ColorSpace? = null
        private var transformations: List<Transformation>
        private var headers: Headers.Builder?
        private var parameters: Parameters.Builder?
        private var sizeResolver: SizeResolver?
        private var scale: Scale?
        private var dispatcher: CoroutineDispatcher?
        private var transition: Transition?
        private var precision: Precision?
        private var bitmapConfig: Bitmap.Config?
        private var allowHardware: Boolean?
        private var allowRgb565: Boolean?
        @DrawableRes private var placeholderResId: Int?
        private var placeholderDrawable: Drawable?
        @DrawableRes private var errorResId: Int?
        private var errorDrawable: Drawable?
        @DrawableRes private var fallbackResId: Int?
        private var fallbackDrawable: Drawable?
        private var resolvedLifecycle: Lifecycle?
        private var resolvedSizeResolver: SizeResolver?
        private var resolvedScale: Scale?

1、Target

Target 即最終圖片的接收載體,ImageRequest 提供了 target 方法用於把 ImageView 包裝爲 Target 。如果最終圖片的接收載體不是 ImageView 的話,就需要開發者自己來實現 Target 接口

fun target(imageView: ImageView) = target(ImageViewTarget(imageView))

fun target(target: Target?) = apply {
    this.target = target
    resetResolvedValues()
}

Target 接口提供了圖片開始加載、圖片加載失敗、圖片加載成功的事件回調,主要是爲了顯示佔位圖、錯誤圖、目標圖等幾個

interface Target {
    /**
     * Called when the request starts.
     */
    @MainThread
    fun onStart(placeholder: Drawable?) {}

    /**
     * Called if an error occurs while executing the request.
     */
    @MainThread
    fun onError(error: Drawable?) {}

    /**
     * Called if the request completes successfully.
     */
    @MainThread
    fun onSuccess(result: Drawable) {}
}

ImageViewTarget 就是通過調用 setImageDrawable 來顯式各個狀態的圖片,同時也實現了 DefaultLifecycleObserver 接口,意味着 ImageViewTarget 本身就具備了監聽生命週期事件的能力

/** A [Target] that handles setting images on an [ImageView]. */
open class ImageViewTarget(
    override val view: ImageView
) : PoolableViewTarget<ImageView>, TransitionTarget, DefaultLifecycleObserver {

    private var isStarted = false

    override val drawable: Drawable? get() = view.drawable

    override fun onStart(placeholder: Drawable?) = setDrawable(placeholder)

    override fun onError(error: Drawable?) = setDrawable(error)

    override fun onSuccess(result: Drawable) = setDrawable(result)
    
    /** Replace the [ImageView]'s current drawable with [drawable]. */
    protected open fun setDrawable(drawable: Drawable?) {
        (view.drawable as? Animatable)?.stop()
        view.setImageDrawable(drawable)
        updateAnimation()
    }

    ···
}

2、Lifecycle

每個 ImageRequest 都會關聯一個 Context 對象,如果外部傳入的是 ImageView,則會取 ImageView 內部的 Context。Coil 會判斷 Context 是否屬於 LifecycleOwner 類型,是的話則可以拿到和 Activity 或者 Fragment 關聯的 Lifecycle,否則最終取 GlobalLifecycle

和 Activity 或者 Fragment 關聯的 Lifecycle 才具備有生命週期感知能力,這樣 Coil 纔可以在 Activity 處於後臺或者已經銷燬的時候暫停任務或者停止任務。而 GlobalLifecycle 會默認且一直處於 RESUMED 狀態,這樣任務就會一直運行直到最終結束,這可能導致內存泄露

private fun resolveLifecycle(): Lifecycle {
    val target = target
    val context = if (target is ViewTarget<*>) target.view.context else context
    //context 屬於 LifecycleOwner 類型則返回對應的 Lifecycle,否則返回 GlobalLifecycle
    return context.getLifecycle() ?: GlobalLifecycle
}

internal object GlobalLifecycle : Lifecycle() {

    private val owner = LifecycleOwner { this }

    override fun addObserver(observer: LifecycleObserver) {
        require(observer is DefaultLifecycleObserver) {
            "$observer must implement androidx.lifecycle.DefaultLifecycleObserver."
        }

        // Call the lifecycle methods in order and do not hold a reference to the observer.
        observer.onCreate(owner)
        observer.onStart(owner)
        observer.onResume(owner)
    }

    override fun removeObserver(observer: LifecycleObserver) {}

    override fun getCurrentState() = State.RESUMED

    override fun toString() = "coil.request.GlobalLifecycle"
}

3、CachePolicy

和 Glide 一樣,Coil 也具備了多級緩存的能力,即內存緩存 memoryCachePolicy、磁盤緩存 diskCachePolicy、網絡緩存 networkCachePolicy。這些緩存功能是否開啓都是通過 CachePolicy 來定義,默認三級緩存全部可讀可寫

enum class CachePolicy(
    val readEnabled: Boolean,
    val writeEnabled: Boolean
) {
    ENABLED(true, true), //可讀可寫
    READ_ONLY(true, false), //只讀
    WRITE_ONLY(false, true), //只寫
    DISABLED(false, false) //不可讀不可寫,即禁用
}

4、Fetcher

Fetcher 是根據圖片來源地址轉換爲目標數據類型的轉換器。例如,我們傳入了 Int 類型的 drawableResId,想要以此拿到 Drawable,那麼這裏的 Class<*>Class<Int>Fetcher<*>Fetcher<Drawable>

    /** @see Builder.fetcher */
    val fetcher: Pair<Fetcher<*>, Class<*>>?,

Fetcher 接口包含三個方法

interface Fetcher<T : Any> {

    /**
     * 如果能處理 data 則返回 true
     */
    fun handles(data: T): Boolean = true

    /**
     * 根據 data 來計算用於內存緩存時的唯一 key
     * 具有相同 key 的緩存將被 MemoryCache 視爲相同的數據
     * 如果返回 null 則不會將 fetch 後的數據緩存到內存中
     */
    fun key(data: T): String?

    /**
     * 根據 data 將目標圖片加載到內存中
     */
    suspend fun fetch(
        pool: BitmapPool,
        data: T,
        size: Size,
        options: Options
    ): FetchResult
}

Coil 默認提供了以下八種類型的 Fetcher,分別用於處理 HttpUriUri、HttpUriUrl、File、Asset、ContentUri、Resource、Drawable、Bitmap 等類型的圖片來源地址

    private val registry = componentRegistry.newBuilder()
        ···
        // Fetchers
        .add(HttpUriFetcher(callFactory))
        .add(HttpUrlFetcher(callFactory))
        .add(FileFetcher(addLastModifiedToFileCacheKey))
        .add(AssetUriFetcher(context))
        .add(ContentUriFetcher(context))
        .add(ResourceUriFetcher(context, drawableDecoder))
        .add(DrawableFetcher(drawableDecoder))
        .add(BitmapFetcher())
        ···
        .build()

5、Decoder

Decoder 接口用於提供將 BufferedSource 轉碼爲 Drawable 的能力,BufferedSource 就對應着不同類型的圖片資源

Coil 提供了以下幾個 Decoder 實現類

  • BitmapFactoryDecoder。用於實現 Bitmap 轉碼
  • GifDecoder、ImageDecoderDecoder。用於實現 Gif、Animated WebPs、Animated HEIFs 轉碼
  • SvgDecoder。用於實現 Svg 轉碼
interface Decoder {
    //如果此 Decoder 能夠處理 source 則返回 true
    fun handles(source: BufferedSource, mimeType: String?): Boolean
    
    //用於將 source 解碼爲 Drawable
    suspend fun decode(
        pool: BitmapPool,
        source: BufferedSource,
        size: Size,
        options: Options
    ): DecodeResult
}

六、Disposable

Disposable 是我們調用 load 方法後的返回值,爲外部提供用於取消圖片加載或者等待圖片加載完成的方法

interface Disposable {
    //如果任務已經完成或者取消的話,則返回 true
    val isDisposed: Boolean
    
    //取消正在進行的任務並釋放與此任務關聯的所有資源
    fun dispose()
    
    //非阻塞式地等待任務結束
    @ExperimentalCoilApi
    suspend fun await()
}

由於 Coil 是使用協程來加載圖片,所以每個任務都會對應一個 Job

如果 ImageRequest 包含的 Target 對應着某個 View(即屬於 ViewTarget 類型),那麼返回的 Disposable 即 ViewTargetDisposable。而 View 可能需要先後請求多張圖片(例如 RecyclerView 的每個 Item 都是 ImageView),那麼當啓動新任務後舊任務就應該被取消,所以 ViewTargetDisposable 就包含了一個 UUID 來唯一標識每個請求。其它情況就都是返回 BaseTargetDisposable

internal class BaseTargetDisposable(private val job: Job) : Disposable {

    override val isDisposed
        get() = !job.isActive

    override fun dispose() {
        if (isDisposed) return
        job.cancel()
    }

    @ExperimentalCoilApi
    override suspend fun await() {
        if (isDisposed) return
        job.join()
    }
}

internal class ViewTargetDisposable(
    private val requestId: UUID,
    private val target: ViewTarget<*>
) : Disposable {

    override val isDisposed
        get() = target.view.requestManager.currentRequestId != requestId

    override fun dispose() {
        if (isDisposed) return
        target.view.requestManager.clearCurrentRequest()
    }

    @ExperimentalCoilApi
    override suspend fun await() {
        if (isDisposed) return
        target.view.requestManager.currentRequestJob?.join()
    }
}

七、ImageLoader

上面有說過,loadAny方法最終是會通過調用 imageLoader.enqueue(request)來發起一個圖片加載請求的,那麼重點就是要來看 ImageLoader 是如何實現的

@JvmSynthetic
inline fun ImageView.loadAny(
    data: Any?,
    imageLoader: ImageLoader = context.imageLoader,
    builder: ImageRequest.Builder.() -> Unit = {}
): Disposable {
    val request = ImageRequest.Builder(context)
        .data(data)
        .target(this)
        .apply(builder)
        .build()
    return imageLoader.enqueue(request)
}

ImageLoader 是一個接口,是承載了所有圖片加載任務和實現緩存複用的加載器

interface ImageLoader {
    //用於提供 ImageRequest 的默認配置項
    val defaults: DefaultRequestOptions
    //內存緩存
    val memoryCache: MemoryCache
    //Bitmap緩存池
    val bitmapPool: BitmapPool
    //異步加載圖片
    fun enqueue(request: ImageRequest): Disposable
    //同步加載圖片
    suspend fun execute(request: ImageRequest): ImageResult
    //停止全部任務
    fun shutdown()
}

ImageLoader 的唯一實現類是 RealImageLoader,其enqueue方法會啓動一個協程,在 job 裏執行 executeMain 方法得到 ImageResult,ImageResult 就包含了最終得到的圖片。同時,job 會被包含在返回的 Disposable 對象裏,這樣外部才能取消圖片加載或者等待圖片加載完成

    override fun enqueue(request: ImageRequest): Disposable {
        // Start executing the request on the main thread.
        val job = scope.launch {
            val result = executeMain(request, REQUEST_TYPE_ENQUEUE)
            if (result is ErrorResult) throw result.throwable
        }

        // Update the current request attached to the view and return a new disposable.
        return if (request.target is ViewTarget<*>) {
            val requestId = request.target.view.requestManager.setCurrentRequestJob(job)
            ViewTargetDisposable(requestId, request.target)
        } else {
            BaseTargetDisposable(job)
        }
    }

executeMain 方法的邏輯也比較簡單,可以概括爲:

  1. 爲 target 和 request 創建一個代理類,用於支持 Bitmap 緩存和 Lifecycle 監聽
  2. 如果外部發起的是異步請求的話(即 REQUEST_TYPE_ENQUEUE),那麼就需要等到 Lifecycle 至少處於 Started 狀態之後才能繼續執行,這樣當 Activity 還處於後臺時就不會發起請求了
  3. 獲取佔位圖並傳給 target
  4. 獲取 target 需要的圖片尺寸大小,以便按需加載,對於 ImageViewTarget 來說,即獲取 ImageView 的寬高屬性
  5. 調用 executeChain 方法拿到 ImageResult,判斷是否成功,調用 target 對應的成功或者失敗的方法
    @MainThread
    private suspend fun executeMain(initialRequest: ImageRequest, type: Int): ImageResult {
        ···

        // Apply this image loader's defaults to this request.
        val request = initialRequest.newBuilder().defaults(defaults).build()

        //target 代理,用於支持Bitmap池
        val targetDelegate = delegateService.createTargetDelegate(request.target, type, eventListener)

        //request 代理,用於支持 lifecycle
        val requestDelegate = delegateService.createRequestDelegate(request, targetDelegate, coroutineContext.job)

        try {
            //如果 data 爲 null,那麼就拋出異常
            if (request.data == NullRequestData) throw NullRequestDataException()

            //如果是異步請求的話,那麼就需要等到 Lifecycle 至少處於 Started 狀態之後才能繼續執行
            if (type == REQUEST_TYPE_ENQUEUE) request.lifecycle.awaitStarted()

            //獲取展位圖傳給 target,從內存緩存中加載或者從全新加載
            val cached = memoryCacheService[request.placeholderMemoryCacheKey]?.bitmap
            try {
                targetDelegate.metadata = null
                targetDelegate.start(cached?.toDrawable(request.context) ?: request.placeholder, cached)
                eventListener.onStart(request)
                request.listener?.onStart(request)
            } finally {
                referenceCounter.decrement(cached)
            }

            //獲取 target 需要的圖片尺寸大小,按需加載
            eventListener.resolveSizeStart(request)
            val size = request.sizeResolver.size()
            eventListener.resolveSizeEnd(request, size)

            // Execute the interceptor chain.
            val result = executeChain(request, type, size, cached, eventListener)

            // Set the result on the target.
            //判斷 result 成功與否,調用相應的方法
            when (result) {
                is SuccessResult -> onSuccess(result, targetDelegate, eventListener)
                is ErrorResult -> onError(result, targetDelegate, eventListener)
            }
            return result
        } catch (throwable: Throwable) {
            if (throwable is CancellationException) {
                onCancel(request, eventListener)
                throw throwable
            } else {
                // Create the default error result if there's an uncaught exception.
                val result = requestService.errorResult(request, throwable)
                onError(result, targetDelegate, eventListener)
                return result
            }
        } finally {
            requestDelegate.complete()
        }
    }

executeChain方法就比較有意思了,有看過 OkHttp 源碼的同學應該會對 RealInterceptorChain 有點印象,OkHttp 的攔截器就是通過該同名類來實現的,很顯然 Coil 借鑑了 OkHttp 的實現思路,極大方便了後續功能擴展,也給了外部控制整個圖片加載流程的入口,可擴展性 +100

不瞭解 OkHttp 的 RealInterceptorChain 實現思路的可以看我的這篇文章,這裏不再贅述:三方庫源碼筆記(11)-OkHttp 源碼詳解

    private val interceptors = registry.interceptors + EngineInterceptor(registry, bitmapPool, referenceCounter,
        strongMemoryCache, memoryCacheService, requestService, systemCallbacks, drawableDecoder, logger)

    private suspend inline fun executeChain(
        request: ImageRequest,
        type: Int,
        size: Size,
        cached: Bitmap?,
        eventListener: EventListener
    ): ImageResult {
        val chain = RealInterceptorChain(request, type, interceptors, 0, request, size, cached, eventListener)
        return if (launchInterceptorChainOnMainThread) {
            chain.proceed(request)
        } else {
            withContext(request.dispatcher) {
                chain.proceed(request)
            }
        }
    }

所以說,重點就還是要來看 EngineInterceptor 的 intercept 方法,其邏輯可以概括爲:

  1. 找到能處理本次請求的 fetcher,執行下一步
  2. 計算本次要加載的圖片在內存中的緩存 key,如果內存緩存可用的話就直接使用緩存,結束流程
  3. 如果存在內存緩存但是不可用(可能是由於硬件加速配置不符或者是本次不允許使用緩存),那麼就更新該緩存在內存中的可用狀態並更新引用計數,執行下一步
  4. 調用 execute 方法完成圖片加載,得到 drawable,結束流程

execute 方法的邏輯可以概括爲:

  1. 通過 fetcher 來執行磁盤加載或者網絡請求,得到 fetchResult,執行下一步
  2. 如果 fetchResult 屬於 DrawableResult 的話,那麼就已經拿到目標圖片類型 Drawable 了,那麼直接返回,結束流程
  3. 如果 fetchResult 屬於 SourceResult 類型,即拿到的數據類型是 BufferedSource,此時還需要轉碼爲 Drawable,執行下一步
  4. 先判斷本次請求是否屬於預加載,即可能外部現在不需要使用到該圖片,只是想先將圖片緩存到本地磁盤,方便後續能夠快速加載。預加載的判斷標準就是:異步請求 + target 爲null + 不允許緩存到內存中。屬於預加載的話就不需要將加載到的圖片進行轉碼了,此時就使用 EmptyDecoder,否則就還是需要去找能進行實際轉碼的 Decoder。拿到 Decoder 後就執行下一步
  5. 通過 Decoder 完成圖片轉碼,得到 Drawable,結束流程
/** The last interceptor in the chain which executes the [ImageRequest]. */
internal class EngineInterceptor(
    private val registry: ComponentRegistry,
    private val bitmapPool: BitmapPool,
    private val referenceCounter: BitmapReferenceCounter,
    private val strongMemoryCache: StrongMemoryCache,
    private val memoryCacheService: MemoryCacheService,
    private val requestService: RequestService,
    private val systemCallbacks: SystemCallbacks,
    private val drawableDecoder: DrawableDecoderService,
    private val logger: Logger?
) : Interceptor {

    override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
        try {
            // This interceptor uses some internal APIs.
            check(chain is RealInterceptorChain)

            val request = chain.request
            val context = request.context
            val data = request.data
            val size = chain.size
            val eventListener = chain.eventListener

            // Perform any data mapping.
            eventListener.mapStart(request, data)
            val mappedData = registry.mapData(data)
            eventListener.mapEnd(request, mappedData)

            //找到能處理本次請求的 fetcher
            val fetcher = request.fetcher(mappedData) ?: registry.requireFetcher(mappedData)
            //計算本次要加載的圖片在內存中的緩存 key
            val memoryCacheKey = request.memoryCacheKey ?: computeMemoryCacheKey(request, mappedData, fetcher, size)
            //如果本次允許使用內存緩存的話,那麼就嘗試從 memoryCacheService 中獲取緩存
            val value = if (request.memoryCachePolicy.readEnabled) memoryCacheService[memoryCacheKey] else null

            // Ignore the cached bitmap if it is hardware-backed and the request disallows hardware bitmaps.
            val cachedDrawable = value?.bitmap
                ?.takeIf { requestService.isConfigValidForHardware(request, it.safeConfig) }
                ?.toDrawable(context)

            //如果緩存可用,則直接返回緩存
            if (cachedDrawable != null && isCachedValueValid(memoryCacheKey, value, request, size)) {
                return SuccessResult(
                    drawable = value.bitmap.toDrawable(context),
                    request = request,
                    metadata = Metadata(
                        memoryCacheKey = memoryCacheKey,
                        isSampled = value.isSampled,
                        dataSource = DataSource.MEMORY_CACHE,
                        isPlaceholderMemoryCacheKeyPresent = chain.cached != null
                    )
                )
            }

            // Fetch, decode, transform, and cache the image on a background dispatcher.
            return withContext(request.dispatcher) {
                //如果 request.data 屬於 BitmapDrawable 或者 Bitmap 類型
                //會執行到這裏說明 data 不符合本次的使用條件,那麼就在內存中將其標記爲不可用狀態
                invalidateData(request.data)

                //存在緩存但是沒用上,引用計數減一
                if (value != null) referenceCounter.decrement(value.bitmap)

                // Fetch and decode the image.
                val (drawable, isSampled, dataSource) =
                    execute(mappedData, fetcher, request, chain.requestType, size, eventListener)

                // Mark the drawable's bitmap as eligible for pooling.
                validateDrawable(drawable)
                
                //嘗試將獲取到的 bitmap 緩存到內存中
                val isCached = writeToMemoryCache(request, memoryCacheKey, drawable, isSampled)

                // Return the result.
                SuccessResult(
                    drawable = drawable,
                    request = request,
                    metadata = Metadata(
                        memoryCacheKey = memoryCacheKey.takeIf { isCached },
                        isSampled = isSampled,
                        dataSource = dataSource,
                        isPlaceholderMemoryCacheKeyPresent = chain.cached != null
                    )
                )
            }
        } catch (throwable: Throwable) {
            if (throwable is CancellationException) {
                throw throwable
            } else {
                return requestService.errorResult(chain.request, throwable)
            }
        }
    }
    
    /** Load the [data] as a [Drawable]. Apply any [Transformation]s. */
    private suspend inline fun execute(
        data: Any,
        fetcher: Fetcher<Any>,
        request: ImageRequest,
        type: Int,
        size: Size,
        eventListener: EventListener
    ): DrawableResult {
        val options = requestService.options(request, size, systemCallbacks.isOnline)

        eventListener.fetchStart(request, fetcher, options)
        val fetchResult = fetcher.fetch(bitmapPool, data, size, options)
        eventListener.fetchEnd(request, fetcher, options, fetchResult)

        val baseResult = when (fetchResult) {
            is SourceResult -> {
                val decodeResult = try {
                    // Check if we're cancelled.
                    coroutineContext.ensureActive()

                    //判斷本次請求是否屬於預加載,即可能外部只是想先將圖片加載到本地磁盤,方便後續使用
                    //預加載的判斷標準就是:異步請求 + target爲null + 不緩存到內存中
                    //屬於預加載的話就不需要將加載到的圖片進行轉碼了,就會使用 EmptyDecoder
                    //否則就還是需要去找能進行轉碼的 Decoder
                    val isDiskOnlyPreload = type == REQUEST_TYPE_ENQUEUE &&
                        request.target == null &&
                        !request.memoryCachePolicy.writeEnabled
                    val decoder = if (isDiskOnlyPreload) {
                        // Skip decoding the result if we are preloading the data and writing to the memory cache is
                        // disabled. Instead, we exhaust the source and return an empty result.
                        EmptyDecoder
                    } else {
                        request.decoder ?: registry.requireDecoder(request.data, fetchResult.source, fetchResult.mimeType)
                    }

                    // Decode the stream.
                    eventListener.decodeStart(request, decoder, options)
                    //進行轉碼,得到目標類型 Drawable
                    val decodeResult = decoder.decode(bitmapPool, fetchResult.source, size, options)
                    eventListener.decodeEnd(request, decoder, options, decodeResult)
                    decodeResult
                } catch (throwable: Throwable) {
                    // Only close the stream automatically if there is an uncaught exception.
                    // This allows custom decoders to continue to read the source after returning a drawable.
                    fetchResult.source.closeQuietly()
                    throw throwable
                }

                // Combine the fetch and decode operations' results.
                DrawableResult(
                    drawable = decodeResult.drawable,
                    isSampled = decodeResult.isSampled,
                    dataSource = fetchResult.dataSource
                )
            }
            is DrawableResult -> fetchResult
        }

        // Check if we're cancelled.
        coroutineContext.ensureActive()

        // Apply any transformations and prepare to draw.
        val finalResult = applyTransformations(baseResult, request, size, options, eventListener)
        (finalResult.drawable as? BitmapDrawable)?.bitmap?.prepareToDraw()
        return finalResult
    }
    
}

Fetcher 是根據圖片來源地址轉換爲目標數據類型的轉換器。Coil 默認提供了以下八種類型的 Fetcher,分別用於處理 HttpUri、HttpUrl、File、Asset、ContentUri、Resource、Drawable、Bitmap 等類型的圖片來源地址

    private val registry = componentRegistry.newBuilder()
        ···
        // Fetchers
        .add(HttpUriFetcher(callFactory))
        .add(HttpUrlFetcher(callFactory))
        .add(FileFetcher(addLastModifiedToFileCacheKey))
        .add(AssetUriFetcher(context))
        .add(ContentUriFetcher(context))
        .add(ResourceUriFetcher(context, drawableDecoder))
        .add(DrawableFetcher(drawableDecoder))
        .add(BitmapFetcher())
        ···
        .build()

所以,如果我們外部要加載的是一張網絡圖片,且傳入的是 String 類型的 ImageUrl,那麼最終對應上的就是 HttpUriFetcher,其父類 HttpFetcher 就會通過 OkHttp 來進行網絡請求了。至此,整個圖片加載流程就結束了

internal class HttpUriFetcher(callFactory: Call.Factory) : HttpFetcher<Uri>(callFactory) {

    override fun handles(data: Uri) = data.scheme == "http" || data.scheme == "https"

    override fun key(data: Uri) = data.toString()

    override fun Uri.toHttpUrl(): HttpUrl = HttpUrl.get(toString())
}

internal abstract class HttpFetcher<T : Any>(private val callFactory: Call.Factory) : Fetcher<T> {

    /**
     * Perform this conversion in a [Fetcher] instead of a [Mapper] so
     * [HttpUriFetcher] can execute [HttpUrl.get] on a background thread.
     */
    abstract fun T.toHttpUrl(): HttpUrl

    override suspend fun fetch(
        pool: BitmapPool,
        data: T,
        size: Size,
        options: Options
    ): FetchResult {
        val url = data.toHttpUrl()
        val request = Request.Builder().url(url).headers(options.headers)

        val networkRead = options.networkCachePolicy.readEnabled
        val diskRead = options.diskCachePolicy.readEnabled
        when {
            !networkRead && diskRead -> {
                request.cacheControl(CacheControl.FORCE_CACHE)
            }
            networkRead && !diskRead -> if (options.diskCachePolicy.writeEnabled) {
                request.cacheControl(CacheControl.FORCE_NETWORK)
            } else {
                request.cacheControl(CACHE_CONTROL_FORCE_NETWORK_NO_CACHE)
            }
            !networkRead && !diskRead -> {
                // This causes the request to fail with a 504 Unsatisfiable Request.
                request.cacheControl(CACHE_CONTROL_NO_NETWORK_NO_CACHE)
            }
        }

        val response = callFactory.newCall(request.build()).await()
        if (!response.isSuccessful) {
            response.body()?.close()
            throw HttpException(response)
        }
        val body = checkNotNull(response.body()) { "Null response body!" }

        return SourceResult(
            source = body.source(),
            mimeType = getMimeType(url, body),
            dataSource = if (response.cacheResponse() != null) DataSource.DISK else DataSource.NETWORK
        )
    }
    
}

八、緩存機制

Glide 的緩存機制是分爲內存緩存磁盤緩存兩層,Coil 在這兩個的基礎上還增加了網絡緩存這一層,這可以從 ImageRequest 的參數看出來,默認情況下,這三層緩存機制是全部啓用的,即全部可讀可寫

    //內存緩存
    val memoryCachePolicy: CachePolicy,
    //磁盤緩存
    val diskCachePolicy: CachePolicy,
    //網絡緩存
    val networkCachePolicy: CachePolicy,

enum class CachePolicy(
    val readEnabled: Boolean,
    val writeEnabled: Boolean
) {
    ENABLED(true, true),
    READ_ONLY(true, false),
    WRITE_ONLY(false, true),
    DISABLED(false, false)
}

在請求圖片的時候,我們可以在 lambda 塊中配置本次請求的緩存策略

            imageView.load(imageUrl) {
                memoryCachePolicy(CachePolicy.ENABLED)
                diskCachePolicy(CachePolicy.ENABLED)
                networkCachePolicy(CachePolicy.ENABLED)
            }

下面來看看 Coil 的緩存機制具體是如何定義和實現的

1、內存緩存

Coil 的內存緩存機制集中在 EngineInterceptor 中生效,有兩個時機會來判斷是否可以寫入和讀取內存緩存

  1. 如果本次請求允許從內存中讀取緩存的話,即 request.memoryCachePolicy.readEnabled 爲 true,那麼就嘗試從 memoryCacheService 讀取緩存
  2. 如果本次請求允許將圖片緩存到內存的話,即 request.memoryCachePolicy.writeEnabled 爲 true,那麼就將圖片存到 strongMemoryCache 中
internal class EngineInterceptor(
    private val registry: ComponentRegistry,
    private val bitmapPool: BitmapPool,
    private val referenceCounter: BitmapReferenceCounter,
    private val strongMemoryCache: StrongMemoryCache,
    private val memoryCacheService: MemoryCacheService,
    private val requestService: RequestService,
    private val systemCallbacks: SystemCallbacks,
    private val drawableDecoder: DrawableDecoderService,
    private val logger: Logger?
) : Interceptor {
    
    override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
        try {
            val request = chain.request           
            ··· 
            //如果本次允許使用內存緩存的話,那麼就嘗試從 memoryCacheService 中獲取緩存
            val value = if (request.memoryCachePolicy.readEnabled) memoryCacheService[memoryCacheKey] else null
            ···
            return withContext(request.dispatcher) {
                ···
                //嘗試將獲取到的 bitmap 緩存到 strongMemoryCache 中
                val isCached = writeToMemoryCache(request, memoryCacheKey, drawable, isSampled)
                ···
            }
        } catch (throwable: Throwable) {
            ···
        }
    }
    
    private fun writeToMemoryCache(
        request: ImageRequest,
        key: MemoryCache.Key?,
        drawable: Drawable,
        isSampled: Boolean
    ): Boolean {
        if (!request.memoryCachePolicy.writeEnabled) {
            return false
        }

        if (key != null) {
            val bitmap = (drawable as? BitmapDrawable)?.bitmap
            if (bitmap != null) {
                strongMemoryCache.set(key, bitmap, isSampled)
                return true
            }
        }
        return false
    }
    
}

MemoryCacheService 相當於一個工具類,會先後嘗試從 StrongMemoryCache 和 WeakMemoryCache 取值,取得到的話會同時通過 BitmapReferenceCounter 將其引用計數 +1

internal class MemoryCacheService(
    private val referenceCounter: BitmapReferenceCounter,
    private val strongMemoryCache: StrongMemoryCache,
    private val weakMemoryCache: WeakMemoryCache
) {

    operator fun get(key: MemoryCache.Key?): RealMemoryCache.Value? {
        key ?: return null
        val value = strongMemoryCache.get(key) ?: weakMemoryCache.get(key)
        if (value != null) referenceCounter.increment(value.bitmap)
        return value
    }
}

Coil 的內存緩存機制實際上是分爲兩級:

  1. WeakMemoryCache
  2. StrongMemoryCache

在默認情況下,Coil 的這兩級內存緩存都是開啓的,這兩者的關係是:

  1. RealWeakMemoryCache。通過弱引用來保存曾經加載到內存中的 Bitmap
  2. RealBitmapPool。Bitmap 緩存池,用於在內存中緩存當前不再被使用的 Bitmap,可用於後續複用
  3. RealBitmapReferenceCounter。RealBitmapReferenceCounter 也通過弱引用來保存 Bitmap,用於對當前處於使用狀態的 Bitmap 進行引用標記,計算每個 Bitmap 當前的引用次數及可用狀態。例如,當 EngineInterceptor 在 StrongMemoryCache 中找到了可以複用的 Bitmap 後,該 Bitmap 的引用計數就會 +1。當 StrongMemoryCache 由於容量限制需要移除某個 Bitmap 時,該 Bitmap 的引用計數就會 -1。當 Bitmap 的引用次數變爲 0 且處於不可用狀態時,就會將其從 RealWeakMemoryCache 中移除並存到 BitmapPool 中
  4. RealStrongMemoryCache。RealStrongMemoryCache 通過最近最少使用算法 LruCache 來緩存 Bitmap,並且是通過強引用的方式來保存。當 EngineInterceptor 加載到一個 Bitmap 後,就會將其存到 RealStrongMemoryCache 的 LruCache 中,並同時將 RealBitmapReferenceCounter 的引用計數 +1,在移除元素時也會相應減少引用計數

這兩級緩存的設計初衷是什麼呢?或者說,將內存緩存設計爲這兩層是因爲什麼呢?

我們都知道,弱引用是不會阻止內存回收的,一個對象如果只具備弱引用,那麼在 GC 過後該對象就會被回收,所以 RealWeakMemoryCache 的存在不會導致 Bitmap 被泄漏。而 RealStrongMemoryCache 是通過強引用和 LruCache 來存儲 Bitmap 的,由於 LruCache 具有固定容量,那麼就存在由於容量不足導致用戶當前正在使用的 Bitmap 被移出 LruCache 的可能,如果之後又需要加載同一個 Bitmap 的話,就還可以通過 RealWeakMemoryCache 來取值,儘量複用已經加載在內存中的 Bitmap。所以說,RealStrongMemoryCache 和 RealWeakMemoryCache 的存在意義都是爲了儘量複用 Bitmap

此外,BitmapPool 的存在意義是爲了儘量避免頻繁創建 Bitmap。在使用 Transformation 的時候需要用到 Bitmap 來作爲載體,如果頻繁創建 Bitmap 可能會造成內存抖動,所以即使當一個 Bitmap 不再被使用,也會將之存到 RealBitmapPool 中緩存起來,方便後續複用。RealBitmapReferenceCounter 會保存 Bitmap 的引用次數和可用狀態,當引用次數小於等於 0 且處於不可用狀態時,就會將其從 RealWeakMemoryCache 中移除並存到 BitmapPool 中

2、磁盤緩存、網絡緩存

Coil 的磁盤緩存網絡緩存可以合在一起講,因爲 Coil 的磁盤緩存其實是通過 OkHttp 本身的網絡緩存功能來間接實現的。RealImageLoader 在初始化的時候,默認構建了一個包含 cache 的 OkHttpClient,即默認支持緩存網絡請求結果

        private fun buildDefaultCallFactory() = lazyCallFactory {
            OkHttpClient.Builder()
                .cache(CoilUtils.createDefaultCache(applicationContext))
                .build()
        }

而且,Coil 的磁盤緩存和網絡緩存這兩個配置也只會在 HttpFetcher 這裏讀取,即只在進行網絡請求的時候生效,所以說,Coil 只會磁盤緩存通過網絡請求得到的原始圖片,而不緩存其它尺寸大小的圖片

HttpFetcher 的網絡緩存和磁盤緩存策略是通過修改 Request 的 cacheControl 來實現的,每種緩存策略可以分別配置是否可讀可寫,一共有以下幾種可能:

  1. 不允許網絡請求,允許磁盤讀緩存。那麼就強制使用本地緩存,如果本地緩存不存在的話就報錯,加載失敗
  2. 允許網絡請求,不允許磁盤讀緩存
    1. 允許磁盤寫緩存。那麼就強制去網絡請求,且將請求結果緩存到本地磁盤
    2. 不允許磁盤寫緩存。那麼就強制去網絡請求,且不將請求結果緩存到本地磁盤
  3. 不允許網絡請求,不允許磁盤讀緩存。這會導致請求失敗,Http 報 504 錯誤,加載失敗
  4. 允許網絡請求,也允許磁盤讀緩存和磁盤寫緩存。那麼就會優先使用本地緩存,本地緩存不存在的話再去網絡請求,並將網絡請求結果緩存到本地磁盤
internal abstract class HttpFetcher<T : Any>(private val callFactory: Call.Factory) : Fetcher<T> {

    /**
     * Perform this conversion in a [Fetcher] instead of a [Mapper] so
     * [HttpUriFetcher] can execute [HttpUrl.get] on a background thread.
     */
    abstract fun T.toHttpUrl(): HttpUrl

    override suspend fun fetch(
        pool: BitmapPool,
        data: T,
        size: Size,
        options: Options
    ): FetchResult {
        val url = data.toHttpUrl()
        val request = Request.Builder().url(url).headers(options.headers)

        val networkRead = options.networkCachePolicy.readEnabled
        val diskRead = options.diskCachePolicy.readEnabled
        when {
            //1、不允許網絡請求,允許磁盤讀緩存
            //那麼就強制使用本地緩存,如果不存在本地緩存的話就報錯
            !networkRead && diskRead -> {
                request.cacheControl(CacheControl.FORCE_CACHE)
            }
            //2、允許網絡請求,不允許磁盤讀緩存
            networkRead && !diskRead ->
                if (options.diskCachePolicy.writeEnabled) {
                    //2.1、允許磁盤寫緩存
                    //那麼就強制去網絡請求,且將請求結果緩存到本地磁盤
                    request.cacheControl(CacheControl.FORCE_NETWORK)
                } else {
                    //2.2、不允許磁盤寫緩存
                    //那麼就強制去網絡請求,且不將請求結果緩存到本地磁盤
                    request.cacheControl(CACHE_CONTROL_FORCE_NETWORK_NO_CACHE)
                }
            !networkRead && !diskRead -> {
                //3、不允許網絡請求,不允許磁盤讀緩存
                //這會導致請求失敗,就會導致請求失敗,報 504 錯誤
                request.cacheControl(CACHE_CONTROL_NO_NETWORK_NO_CACHE)
            }
        }

        val response = callFactory.newCall(request.build()).await()
        if (!response.isSuccessful) {
            response.body()?.close()
            throw HttpException(response)
        }
        val body = checkNotNull(response.body()) { "Null response body!" }

        return SourceResult(
            source = body.source(),
            mimeType = getMimeType(url, body),
            dataSource = if (response.cacheResponse() != null) DataSource.DISK else DataSource.NETWORK
        )
    }

    ···
}

從以上邏輯也可以看出,networkCachePolicy 的 writeEnabled 屬性並沒有被用到,因爲網絡請求本身只有發起不發起兩種選擇,用 readEnabled 就足夠表示了,所以 writeEnabled 對於 networkCachePolicy 來說沒有意義

此外,爲了在無網絡信號的時候可以快速結束整個流程,避免無意義的網絡請求,RequestService 會在當前處於離線的時候(即 isOnline 爲 false),將 networkCachePolicy 修改爲完全禁用狀態(CachePolicy.DISABLED)

internal class RequestService(private val logger: Logger?) {
    
    @WorkerThread
    fun options(
        request: ImageRequest,
        size: Size,
        isOnline: Boolean
    ): Options {
        ···
        // Disable fetching from the network if we know we're offline.
        val networkCachePolicy = if (isOnline) request.networkCachePolicy else CachePolicy.DISABLED
        ···
        return Options(
            context = request.context,
            config = config,
            colorSpace = request.colorSpace,
            scale = request.scale,
            allowInexactSize = request.allowInexactSize,
            allowRgb565 = allowRgb565,
            premultipliedAlpha = request.premultipliedAlpha,
            headers = request.headers,
            parameters = request.parameters,
            memoryCachePolicy = request.memoryCachePolicy,
            diskCachePolicy = request.diskCachePolicy,
            networkCachePolicy = networkCachePolicy
        )
    }

    
}

九、生命週期監聽

前文有提到,每個 ImageRequest 都會關聯一個 Context 對象,如果外部傳入的是 ImageView,則會自動取 ImageView 內部的 Context。Coil 會判斷 Context 是否屬於 LifecycleOwner 類型,是的話則可以拿到和 Activity 或者 Fragment 關聯的 Lifecycle,否則最終取 GlobalLifecycle

和 Activity 或者 Fragment 關聯的 Lifecycle 才具備有生命週期感知能力,這樣 Coil 纔可以在 Activity 處於後臺或者已經銷燬的時候暫停或者停止任務。而 GlobalLifecycle 會默認且一直會處於 RESUMED 狀態,這樣任務就會一直運行直到最終結束,這可能導致內存泄露

那麼,該 Lifecycle 對象具體是在什麼地方起了作用呢?

這個主要看 RealImageLoader 的 executeMain 方法。在發起圖片加載請求前,後先創建 request 的代理對象 requestDelegate,requestDelegate 中就包含了對 Lifecycle 的處理邏輯。此外,如果是異步請求的話,會等到 Lifecycle 至少處於 Started 狀態之後才能發起請求,這樣當 Activity 還處於後臺時就不會發起請求了

    @MainThread
    private suspend fun executeMain(initialRequest: ImageRequest, type: Int): ImageResult {
        ···
        
        //創建 request 的代理對象
        val requestDelegate = delegateService.createRequestDelegate(request, targetDelegate, coroutineContext.job)

        try {
            ···

            //如果是異步請求的話,那麼就需要等到 Lifecycle 至少處於 Started 狀態之後才能繼續執行
            if (type == REQUEST_TYPE_ENQUEUE) request.lifecycle.awaitStarted()

            ···
            return result
        } catch (throwable: Throwable) {
            if (throwable is CancellationException) {
                onCancel(request, eventListener)
                throw throwable
            } else {
                // Create the default error result if there's an uncaught exception.
                val result = requestService.errorResult(request, throwable)
                onError(result, targetDelegate, eventListener)
                return result
            }
        } finally {
            requestDelegate.complete()
        }
    }

createRequestDelegate 方法的邏輯可以總結爲:

  1. 如果 target 對象屬於 ViewTarget 類型,那麼說明其包含特定 View
    • 將請求請求參數包裝爲 ViewTargetRequestDelegate 類型,而 ViewTargetRequestDelegate 實現了 DefaultLifecycleObserver 接口,其會在收到 onDestroy 事件的時候主動取消 Job 並清理各類資源。所以向 Lifecycle 添加該 Observer 就可以保證在 Activity 銷燬後也能同時取消圖片加載請求,避免內存泄漏
    • 如果 target 屬於 LifecycleObserver 類型的話,則也向 Lifecycle 添加該 Observer 。ImageViewTarget 就實現了 DefaultLifecycleObserver 接口,這主要是爲了判斷 ImageView 對應的 Activity 或者 Fragment 是否處於前臺,如果處於前臺且存在 Animatable 的話就會自動啓動動畫,否則就自動停止動畫。之所以需要先 removeObserver 再 addObserver,是因爲 target 可能需要先後請求多張圖片,我們不能重複向 Lifecycle 添加同一個 Observer 對象
    • 同時,如果 View 已經 Detached 了的話,那麼就需要主動取消請求
  2. 如果 target 對象不屬於 ViewTarget 類型的話,創建的代理對象是 BaseRequestDelegate 類型,也會在收到 onDestroy 事件的時候主動取消 Job
    /** Wrap [request] to automatically dispose (and for [ViewTarget]s restart) the [ImageRequest] based on its lifecycle. */
    @MainThread
    fun createRequestDelegate(
        request: ImageRequest,
        targetDelegate: TargetDelegate,
        job: Job
    ): RequestDelegate {
        val lifecycle = request.lifecycle
        val delegate: RequestDelegate
        when (val target = request.target) {
            //對應第1點
            is ViewTarget<*> -> {
                //對應第1.1點
                delegate = ViewTargetRequestDelegate(imageLoader, request, targetDelegate, job)
                lifecycle.addObserver(delegate)

                //對應第1.2點
                if (target is LifecycleObserver) {
                    lifecycle.removeObserver(target)
                    lifecycle.addObserver(target)
                }

                target.view.requestManager.setCurrentRequest(delegate)

                //對應第1.3點
                // Call onViewDetachedFromWindow immediately if the view is already detached.
                if (!target.view.isAttachedToWindowCompat) {
                    target.view.requestManager.onViewDetachedFromWindow(target.view)
                }
            }
            //對應第2點
            else -> {
                delegate = BaseRequestDelegate(lifecycle, job)
                lifecycle.addObserver(delegate)
            }
        }
        return delegate
    }

十、Transformation

圖片變換是基本所有的圖片加載庫都會支持的功能,Coil 對這個概念的抽象即 Transformation 接口

注意,key()方法的返回值是用於計算圖片在內存緩存中的唯一 Key 時的輔助參數,所以需要實現該方法,爲 Transformation 生成一個可以唯一標識自身的字符串 Key。transform 方法包含了一個 BitmapPool 參數,我們在實現圖形變換的時候往往是需要一個全新的 Bitmap,此時就應該通過 BitmapPool 來獲取,儘量複用已有的 Bitmap

interface Transformation {

    /**
     * Return a unique key for this transformation.
     *
     * The key should contain any params that are part of this transformation (e.g. size, scale, color, radius, etc.).
     */
    fun key(): String

    /**
     * Apply the transformation to [input].
     *
     * @param pool A [BitmapPool] which can be used to request [Bitmap] instances.
     * @param input The input [Bitmap] to transform. Its config will always be [Bitmap.Config.ARGB_8888] or [Bitmap.Config.RGBA_F16].
     * @param size The size of the image request.
     */
    suspend fun transform(pool: BitmapPool, input: Bitmap, size: Size): Bitmap
}

Coil 默認提供了以下幾個 Transformation 實現類

  • BlurTransformation。用於實現高斯模糊
  • CircleCropTransformation。用於將圖片轉換爲圓形
  • GrayscaleTransformation。用戶實現將圖片轉換爲灰色
  • RoundedCornersTransformation。用於爲圖片添加圓角

我們可以學着官方給的例子,自己來實現兩個 Transformation

1、爲圖片添加水印

爲圖片添加水印的思路也很簡單,只需要對 canvas 稍微坐下旋轉,然後繪製文本即可

/**
 * @Author: leavesC
 * @Date: 2020/11/22 11:32
 * @GitHub:https://github.com/leavesC
 * @Desc: 爲圖片添加水印
 */
class WatermarkTransformation(
    private val watermark: String,
    @ColorInt private val textColor: Int,
    private val textSize: Float
) : Transformation {

    override fun key(): String {
        return "${WatermarkTransformation::class.java.name}-${watermark}-${textColor}-${textSize}"
    }

    override suspend fun transform(pool: BitmapPool, input: Bitmap, size: Size): Bitmap {
        val width = input.width
        val height = input.height
        val config = input.config

        val output = pool.get(width, height, config)

        val canvas = Canvas(output)
        val paint = Paint()
        paint.isAntiAlias = true
        canvas.drawBitmap(input, 0f, 0f, paint)

        canvas.rotate(40f, width / 2f, height / 2f)

        paint.textSize = textSize
        paint.color = textColor

        val textWidth = paint.measureText(watermark)

        canvas.drawText(watermark, (width - textWidth) / 2f, height / 2f, paint)

        return output
    }

}

            imageView.load(imageUrl) {
                transformations(
                    WatermarkTransformation("葉志陳", Color.parseColor("#8D3700B3"), 120f)
                )
            }

2、爲圖片添加蒙層

Android 的 Paint 原生就支持爲 Bitmap 添加一個蒙層,只需要使用其 colorFilter方法即可

/**
 * @Author: leavesC
 * @Date: 2020/11/22 11:17
 * @GitHub:https://github.com/leavesC
 * @Desc: 添加蒙層
 */
class ColorFilterTransformation(
    @ColorInt private val color: Int
) : Transformation {

    override fun key(): String = "${ColorFilterTransformation::class.java.name}-$color"

    override suspend fun transform(pool: BitmapPool, input: Bitmap, size: Size): Bitmap {
        val width = input.width
        val height = input.height
        val config = input.config
        val output = pool.get(width, height, config)

        val canvas = Canvas(output)
        val paint = Paint()
        paint.isAntiAlias = true
        paint.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP)
        canvas.drawBitmap(input, 0f, 0f, paint)

        return output
    }
}

            imageView.load(imageUrl) {
                transformations(
                    WatermarkTransformation("葉志陳", Color.parseColor("#8D3700B3"), 120f),
                    ColorFilterTransformation(Color.parseColor("#9CF44336"))
                )
            }

更多 Transformation 效果看這裏:coil-transformations

十一、實現全局默認配置

如果我們想要設置應用內所有圖片在加載時固定顯示同一張 loading 圖,在加載失敗時固定顯示一張 error 圖, 那麼就需要爲 Coil 設定一個全局的默認配置。Glide 是通過 AppGlideModule 來實現的,那 Coil 是如何來實現這個效果呢?

Coil 默認會在我們第一次觸發圖片加載的時候來初始化 RealImageLoader 的單例對象,而 RealImageLoader 的構造參數就包含了一個 DefaultRequestOptions 用於設置默認配置,所以我們可以通過自定義 RealImageLoader 的初始化邏輯來控制全局的默認請求配置

internal class RealImageLoader(
    context: Context,
    override val defaults: DefaultRequestOptions,
    override val bitmapPool: BitmapPool,
    private val referenceCounter: BitmapReferenceCounter,
    private val strongMemoryCache: StrongMemoryCache,
    private val weakMemoryCache: WeakMemoryCache,
    callFactory: Call.Factory,
    private val eventListenerFactory: EventListener.Factory,
    componentRegistry: ComponentRegistry,
    addLastModifiedToFileCacheKey: Boolean,
    private val launchInterceptorChainOnMainThread: Boolean,
    val logger: Logger?
) 

RealImageLoader 的單例對象就保存在另一個單例對象 Coil 中,Coil 以兩種方式來完成 RealImageLoader 的初始化

  • 如果項目中的 Application 繼承了 ImageLoaderFactory 接口,那麼就通過該接口來完成初始化
  • 通過 ImageLoader(context) 來完成默認初始化
/**
 * A class that holds the singleton [ImageLoader] instance.
 */
object Coil {

    private var imageLoader: ImageLoader? = null
    private var imageLoaderFactory: ImageLoaderFactory? = null

    /**
     * Get the singleton [ImageLoader]. Creates a new instance if none has been set.
     */
    @JvmStatic
    fun imageLoader(context: Context): ImageLoader = imageLoader ?: newImageLoader(context)

    ···

    /** Create and set the new singleton [ImageLoader]. */
    @Synchronized
    private fun newImageLoader(context: Context): ImageLoader {
        // Check again in case imageLoader was just set.
        imageLoader?.let { return it }

        // Create a new ImageLoader.
        val newImageLoader = imageLoaderFactory?.newImageLoader()
            ?: (context.applicationContext as? ImageLoaderFactory)?.newImageLoader()
            ?: ImageLoader(context)
        imageLoaderFactory = null
        imageLoader = newImageLoader
        return newImageLoader
    }
    
}

爲了設定默認配置,我們就需要在應用啓動之後,開始圖片加載之前向 Coil 注入自己的 ImageLoader 實例

/**
 * @Author: leavesC
 * @Date: 2020/11/22 13:06
 * @GitHub:https://github.com/leavesC
 * @Desc:
 */
object CoilHolder {

    fun init(application: Application) {
        Coil.setImageLoader(
            ImageLoader.Builder(application)
                .placeholder(ActivityCompat.getDrawable(application, R.drawable.icon_loading)) //佔位符
                .error(ActivityCompat.getDrawable(application, R.drawable.icon_error)) //錯誤圖
                .memoryCachePolicy(CachePolicy.ENABLED) //開啓內存緩存
                .callFactory(createOkHttp(application)) //主動構造 OkHttpClient 實例
                .build()
        )
    }

    private fun createOkHttp(application: Application): OkHttpClient {
        return OkHttpClient.Builder()
            .cache(createDefaultCache(application))
            .build()
    }

    private fun createDefaultCache(context: Context): Cache {
        val cacheDirectory = getDefaultCacheDirectory(context)
        return Cache(cacheDirectory, 10 * 1024 * 1024)
    }

    private fun getDefaultCacheDirectory(context: Context): File {
        return File(context.cacheDir, "image_cache").apply { mkdirs() }
    }

}

十二、自定義網絡請求

在講 Coil 的磁盤緩存網絡緩存這一節內容的時候有提到,Coil 的網絡請求是由 HttpFetcher 來完成的,那麼我們是否有辦法來替換該組件,自己來實現網絡請求呢?先來看下 Coil 是如何實現將外部傳入的圖片地址和特定的 Fetcher 對應上的

RealImageLoader 包含一個 registry 變量,其包含的 Mapper 和 Fetcher 就用於實現數據映射

internal class RealImageLoader(
    context: Context,
    override val defaults: DefaultRequestOptions,
    override val bitmapPool: BitmapPool,
    private val referenceCounter: BitmapReferenceCounter,
    private val strongMemoryCache: StrongMemoryCache,
    private val weakMemoryCache: WeakMemoryCache,
    callFactory: Call.Factory,
    private val eventListenerFactory: EventListener.Factory,
    componentRegistry: ComponentRegistry,
    addLastModifiedToFileCacheKey: Boolean,
    private val launchInterceptorChainOnMainThread: Boolean,
    val logger: Logger?
) : ImageLoader {
    
    private val registry = componentRegistry.newBuilder()
        // Mappers
        .add(StringMapper())
        .add(FileUriMapper())
        .add(ResourceUriMapper(context))
        .add(ResourceIntMapper(context))
        // Fetchers
        .add(HttpUriFetcher(callFactory))
        .add(HttpUrlFetcher(callFactory))
        .add(FileFetcher(addLastModifiedToFileCacheKey))
        .add(AssetUriFetcher(context))
        .add(ContentUriFetcher(context))
        .add(ResourceUriFetcher(context, drawableDecoder))
        .add(DrawableFetcher(drawableDecoder))
        .add(BitmapFetcher())
        // Decoders
        .add(BitmapFactoryDecoder(context))
        .build()
    
}

外部在調用 load 方法時,傳入的 String 參數可能是完全不同的含義,既可能是指向本地資源文件,也可能是指向遠程的網絡圖片,Coil 就依靠 Mapper 和 Fetcher 來區分資源類型

imageView.load("android.resource://example.package.name/drawable/image")

imageView.load("https://www.example.com/image.jpg")

StringMapper 首先會將 String 類型轉換爲 Uri

internal class StringMapper : Mapper<String, Uri> {

    override fun map(data: String) = data.toUri()
}

ResourceUriFetcher 會拿到 Uri,然後判斷 Uri 的 scheme 是否是 android.resource,是的話就知道其指向的是本地的資源文件。HttpUriFetcher 則是判斷 Uri 的 scheme 是否是http或者https,是的話就知道其指向的是遠程網絡圖片

internal class ResourceUriFetcher(
    private val context: Context,
    private val drawableDecoder: DrawableDecoderService
) : Fetcher<Uri> {

    override fun handles(data: Uri) = data.scheme == ContentResolver.SCHEME_ANDROID_RESOURCE
    
}


internal class HttpUriFetcher(callFactory: Call.Factory) : HttpFetcher<Uri>(callFactory) {

    override fun handles(data: Uri) = data.scheme == "http" || data.scheme == "https"

    override fun key(data: Uri) = data.toString()

    override fun Uri.toHttpUrl(): HttpUrl = HttpUrl.get(toString())
    
}

以上這個轉換+判斷+加載的過程就發生在 EngineInterceptor 中

internal class EngineInterceptor(
    private val registry: ComponentRegistry,
    private val bitmapPool: BitmapPool,
    private val referenceCounter: BitmapReferenceCounter,
    private val strongMemoryCache: StrongMemoryCache,
    private val memoryCacheService: MemoryCacheService,
    private val requestService: RequestService,
    private val systemCallbacks: SystemCallbacks,
    private val drawableDecoder: DrawableDecoderService,
    private val logger: Logger?
) : Interceptor {
    
    override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
        ···
        //將外部傳入的數據進行類型轉換
        val mappedData = registry.mapData(data)
        //找到能處理本次請求的 fetcher
        val fetcher = request.fetcher(mappedData) ?: registry.requireFetcher(mappedData)
        
        ···
        
    }
    
}

所以,要自定義網絡請求組件,我們就需要向 ComponentRegistry 添加自己的 HttpFetcher 實現,在拿到 Uri 類型的網絡地址後發起網絡請求。這裏我來寫一個通過 Volley 來完成網絡圖片加載的 VolleyFetcher。需要注意的是,我寫的 VolleyFetcher 並不可靠,因爲我也只是想寫個 Demo 而已,正常來說還是應該使用 OkHttp

/**
 * @Author: leavesC
 * @Date: 2020/11/22 13:52
 * @GitHub:https://github.com/leavesC
 * @Desc:
 */
class VolleyFetcher(private val application: Application) : Fetcher<Uri> {

    override fun handles(data: Uri) = data.scheme == "http" || data.scheme == "https"

    override fun key(data: Uri): String? {
        return data.toString()
    }

    private class ImageRequest(url: String, private val listener: RequestFuture<BufferedSource>) :
        Request<BufferedSource>(Method.GET, url, listener) {
        override fun parseNetworkResponse(response: NetworkResponse): Response<BufferedSource> {
            return Response.success(
                Buffer().write(response.data),
                HttpHeaderParser.parseCacheHeaders(response)
            )
        }

        override fun deliverResponse(response: BufferedSource) {
            listener.onResponse(response)
        }
    }

    override suspend fun fetch(
        pool: BitmapPool,
        data: Uri,
        size: Size,
        options: Options
    ): FetchResult {
        val url = data.toString()
        val newFuture = RequestFuture.newFuture<BufferedSource>()
        val request = ImageRequest(url, newFuture)
        newFuture.setRequest(request)
        Volley.newRequestQueue(application).apply {
            add(request)
            start()
        }
        val get = newFuture.get()
        return SourceResult(
            source = get,
            mimeType = "",
            dataSource = DataSource.NETWORK
        )
    }

}

然後爲 ImageLoader 註冊該 Fetcher 即可

    fun init(application: Application) {
        val okHttpClient = createOkHttp(application)
        Coil.setImageLoader(
            ImageLoader.Builder(application)
                .placeholder(ActivityCompat.getDrawable(application, R.drawable.icon_loading))
                .error(ActivityCompat.getDrawable(application, R.drawable.icon_error))
                .memoryCachePolicy(CachePolicy.ENABLED)
                .callFactory(okHttpClient)
                .componentRegistry(
                    ComponentRegistry.Builder()
                        .add(VolleyFetcher(application))
                        .add(OkHttpFetcher(okHttpClient)).build()
                )
                .build()
        )
    }

十三、Demo 下載

上述的所有示例代碼我都放到 GitHub 了,歡迎 star:AndroidOpenSourceDemo

十四、系列文章導航

三方庫源碼筆記(1)-EventBus 源碼詳解

三方庫源碼筆記(2)-EventBus 自己實現一個?

三方庫源碼筆記(3)-ARouter 源碼詳解

三方庫源碼筆記(4)-ARouter 自己實現一個?

三方庫源碼筆記(5)-LeakCanary 源碼詳解

三方庫源碼筆記(6)-LeakCanary 擴展閱讀

三方庫源碼筆記(7)-超詳細的 Retrofit 源碼解析

三方庫源碼筆記(8)-Retrofit 與 LiveData 的結合使用

三方庫源碼筆記(9)-超詳細的 Glide 源碼詳解

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

三方庫源碼筆記(11)-OkHttp 源碼詳解

三方庫源碼筆記(12)-OkHttp / Retrofit 開發調試利器

三方庫源碼筆記(13)-可能是全網第一篇 Coil 的源碼分析文章

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

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

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