Android kotlin图片库Coil源码详解

25 阅读5分钟

Coil 源码分析

一、整体架构概览

Coil(Coroutine Image Loader)是 2019 年发布的 Kotlin 协程原生图片库,整体设计借鉴 OkHttp 的拦截器模式,全程协程驱动。

核心组件

ImageLoader          图片加载器,全局单例,管理所有请求
ImageRequest         单次请求的配置对象
RequestDelegate      生命周期管理,绑定 View/Lifecycle
InterceptorChain     拦截器链,串联所有处理环节
Fetcher              数据获取(网络/文件/资源)
Decoder              图片解码(Bitmap/GIF/SVG)

二、完整加载流程

imageView.load(url)
  → imageLoader.enqueue(request)        启动协程
  → executeMain()                        主线程初始化
  → requestDelegate.start()             绑定生命周期
  → 拦截器链 proceed()
      MemoryCacheInterceptor             查内存缓存
      DiskCacheInterceptor               查磁盘缓存
      FetcherInterceptor                 网络请求
      DecoderInterceptor                 图片解码
  → target.onSuccess(drawable)          主线程显示

三、拦截器链设计(照搬 OkHttp)

接口对比

// OkHttp 拦截器
interface Interceptor {
    fun intercept(chain: Chain): Response
    interface Chain {
        fun request(): Request
        fun proceed(request: Request): Response
    }
}

// Coil 拦截器(只多了 suspend)
interface Interceptor {
    suspend fun intercept(chain: Chain): ImageResult
    interface Chain {
        val request: ImageRequest
        suspend fun proceed(request: ImageRequest): ImageResult
    }
}

内置 5 个拦截器

用户自定义 Interceptor(插在最前)
  → EngineInterceptor       引擎,负责调度
  → MemoryCacheInterceptor  查内存缓存
  → DiskCacheInterceptor    查磁盘缓存
  → FetcherInterceptor      获取原始数据
  → DecoderInterceptor      解码图片

拦截器链执行原理

// RealInterceptorChain.kt
override suspend fun proceed(request: ImageRequest): ImageResult {
    val interceptor = interceptors[index]
    val next = copy(index = index + 1)   // 指向下一个
    return interceptor.intercept(next)   // 传入 next,拦截器调 next.proceed() 继续链
}

执行路径(递归调用):

proceed(0) → MemoryCache.intercept()
  → chain.proceed(1) → DiskCache.intercept()
    → chain.proceed(2) → Fetcher.intercept()
      → chain.proceed(3) → Decoder.intercept()
        → 返回结果
      ← Decoder 写缓存
    ← Fetcher 返回
  ← DiskCache 写磁盘
← MemoryCache 写内存

四、协程在各环节的运用

4.1 enqueue 启动协程

override fun enqueue(request: ImageRequest): Disposable {
    val job = imageLoaderScope.launch(request.coroutineContext) {
        val result = executeMain(request, REQUEST_TYPE_ENQUEUE)
        when (result) {
            is SuccessResult -> target.onSuccess(result.image.toDrawable())
            is ErrorResult   -> target.onError(result.image?.toDrawable())
        }
    }
    return CoroutineDisposable(job)   // 封装 Job,支持 dispose() 取消
}

4.2 executeMain 线程切换

private suspend fun executeMain(...): ImageResult =
    withContext(Dispatchers.Main.immediate) {  // 确保主线程
        val requestDelegate = requestService.requestDelegate(
            request = initialRequest,
            job = coroutineContext.job          // 把当前协程 Job 传给 delegate
        )
        requestDelegate.start()                // 注册生命周期监听

        withContext(request.interceptorDispatcher) {  // 切到 IO 线程
            RealInterceptorChain(...).proceed(request)
        }
    }

4.3 HttpUriFetcher — 回调桥接成挂起函数(最核心)

override suspend fun fetch(): FetchResult {
    val call = okHttpClient.newCall(request)

    val response = suspendCancellableCoroutine<Response> { continuation ->

        // 协程取消 → 自动取消 OkHttp 请求
        continuation.invokeOnCancellation {
            call.cancel()
        }

        call.enqueue(object : Callback {
            override fun onResponse(call: Call, response: Response) {
                continuation.resume(response)           // 恢复协程,返回结果
            }
            override fun onFailure(call: Call, e: IOException) {
                continuation.resumeWithException(e)    // 恢复协程,抛异常
            }
        })
    }
    // 挂起等待 OkHttp 回调,回调后协程恢复继续执行

    return SourceResult(source = response.body!!.source().toImageSource(), ...)
}

4.4 DecoderInterceptor 线程切换

override suspend fun intercept(chain: Chain): ImageResult {
    val bitmap = withContext(request.decoderDispatcher) {
        // Dispatchers.Default:CPU 密集,解码切到 Default 线程池
        decoder.decode()
    }

    val transformed = withContext(request.transformationDispatcher) {
        // 变换处理
        request.transformations.fold(bitmap) { b, t -> t.transform(b, size) }
    }

    return SuccessResult(image = transformed.toImage(), ...)
}

线程流转总图

主线程
  imageView.load(url)
  └── imageLoaderScope.launch
        │
        └── withContext(Main.immediate)        ← executeMain
              ├── requestDelegate.start()       绑定 Lifecycle
              ├── target.onStart()              显示 placeholder
              │
              └── withContext(IO)               ← 拦截器链
                    ├── MemoryCache             同步查,无切换
                    ├── DiskCache               IO 线程读磁盘
                    ├── Fetcher
                    │   └── suspendCancellableCoroutine
                    │         OkHttp enqueue → continuation.resume
                    └── Decoder
                        └── withContext(Default)  解码(CPU密集)

              ← 结果回到 Main
              target.onSuccess(drawable)        显示图片

五、生命周期管理

imageLoaderScope

private val imageLoaderScope = CoroutineScope(
    SupervisorJob() +                  // 子请求失败互不影响
    Dispatchers.Main.immediate +
    CoroutineExceptionHandler { ... }
)

imageLoaderScope 生命周期 = ImageLoader 实例生命周期,调用 shutdown() 才取消。

requestDelegate 两种实现

BaseRequestDelegateexecute 调用,无 View)

internal class BaseRequestDelegate(
    private val lifecycle: Lifecycle,
    private val job: Job,
) : RequestDelegate, DefaultLifecycleObserver {

    override fun start() {
        lifecycle.addObserver(this)
    }

    override fun onDestroy(owner: LifecycleOwner) {
        finish()
        job.cancel()   // onDestroy 取消,不重启
    }
}

ViewTargetRequestDelegateimageView.load(),有 View)

internal class ViewTargetRequestDelegate(
    private val imageLoader: ImageLoader,
    private val initialRequest: ImageRequest,
    private val target: ViewTarget<*>,
    private val lifecycle: Lifecycle,
    private val job: Job,
) : RequestDelegate, DefaultLifecycleObserver, View.OnAttachStateChangeListener {

    override fun start() {
        target.view.addOnAttachStateChangeListener(this)  // View 维度
        lifecycle.addObserver(this)                        // Lifecycle 维度
    }

    // ── View 维度 ──────────────────────────
    override fun onViewDetachedFromWindow(v: View) {
        job.cancel()                        // RecyclerView 滑走,取消
    }
    override fun onViewAttachedToWindow(v: View) {
        imageLoader.enqueue(initialRequest) // 滑回来,重新加载
    }

    // ── Lifecycle 维度 ─────────────────────
    override fun onStop(owner: LifecycleOwner) {
        job.cancel()                        // 切后台,取消
    }
    override fun onStart(owner: LifecycleOwner) {
        imageLoader.enqueue(initialRequest) // 切回前台,重新加载(命中缓存瞬间完成)
    }
    override fun onDestroy(owner: LifecycleOwner) {
        finish()
        job.cancel()                        // 页面销毁,取消,不再重启
    }
}

触发时机选择

ViewTargetRequestDelegate:
  imageView.load(url) 或 .target(imageView) → target 是 ViewTarget

BaseRequestDelegate:
  imageLoader.execute(request)、没有 target、自定义非 View Target

取消完整路径

请求被取消的 6 种情况:

1. disposable.dispose()              调用方主动取消
2. imageView.load(newUrl)            新请求替换旧请求
3. onViewDetachedFromWindow          RecyclerView 滑走
4. lifecycle.onStop                  切后台
5. lifecycle.onDestroy               页面销毁
6. imageLoader.shutdown()            ImageLoader 整体关闭

取消传播链

任意取消触发
  → job.cancel()
  → 取消 executeMain 协程
  → 取消所有子协程(整条拦截器链)
      → 如果在 suspendCancellableCoroutine 挂起
            → invokeOnCancellation 触发
            → okHttpCall.cancel()        网络连接断开
      → 如果在 withContext(Default) 解码
            → 收到 CancellationException  解码停止
  → 所有资源释放,无内存泄漏

六、缓存设计

两级缓存

级别实现存储内容命中后
内存缓存WeakReference + LruCache解码后的 Image直接显示
磁盘缓存OkHttp DiskCache原始压缩数据解码后显示

缓存 Key 构成

// 内存缓存 Key = url + size + transformations + ...
// 同一张图不同尺寸是两份缓存
val memoryCacheKey = MemoryCache.Key(
    key = url,
    extras = mapOf("width" to "100", "height" to "100")
)

七、与 Glide/Fresco 生命周期方案对比

FrescoGlideCoil
方案onAttachToWindow 内嵌 View偷塞无界面 Fragment + OnAttachStateChangeListenerLifecycle Observer + OnAttachStateChangeListener
侵入性高(必须换 DraweeView低(原生 ImageView低(原生 ImageView
依赖无需 Jetpack无需 Lifecycle 组件需要 Lifecycle
诞生时间201520132019

Glide 为何不用 Lifecycle:2013 年发布时 Lifecycle 组件尚未存在(2017 年才随 Jetpack 推出),Fragment 方案是当时唯一选择,历史包袱导致难以重构。


八、Coil 用到的协程技术汇总

技术用在哪里目的
scope.launchenqueue 启动加载异步执行,不阻塞主线程
withContext(Main)executeMain、显示图片UI 操作必须主线程
withContext(IO)拦截器链、磁盘读写IO 操作切到 IO 线程
withContext(Default)图片解码、TransformationCPU 密集切到 Default
suspendCancellableCoroutineHttpUriFetcher把 OkHttp 回调变成挂起函数
invokeOnCancellationOkHttp 请求取消协程取消自动取消 HTTP 请求
coroutineContext.job传给 requestDelegate页面销毁取消协程
SupervisorJobimageLoaderScope单个请求失败不影响其他
结构化并发整个加载链取消自动传播,无泄漏
CoroutineDisposableenqueue 返回值封装 Job,给调用方取消入口