Android kotlin图片库Coil源码详解

Coil 源码分析

一、整体架构概览

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

核心组件

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

二、完整加载流程

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

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

接口对比

kotlin 复制代码
// 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      解码图片

拦截器链执行原理

kotlin 复制代码
// 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() 继续链
}

执行路径(递归调用):

scss 复制代码
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 启动协程

kotlin 复制代码
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 线程切换

kotlin 复制代码
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 --- 回调桥接成挂起函数(最核心)

kotlin 复制代码
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 线程切换

kotlin 复制代码
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(), ...)
}

线程流转总图

scss 复制代码
主线程
  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

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

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

requestDelegate 两种实现

BaseRequestDelegateexecute 调用,无 View)

kotlin 复制代码
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)

kotlin 复制代码
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()                        // 页面销毁,取消,不再重启
    }
}

触发时机选择

scss 复制代码
ViewTargetRequestDelegate:
  imageView.load(url) 或 .target(imageView) → target 是 ViewTarget

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

取消完整路径

markdown 复制代码
请求被取消的 6 种情况:

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

取消传播链

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

六、缓存设计

两级缓存

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

缓存 Key 构成

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

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

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

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


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

技术 用在哪里 目的
scope.launch enqueue 启动加载 异步执行,不阻塞主线程
withContext(Main) executeMain、显示图片 UI 操作必须主线程
withContext(IO) 拦截器链、磁盘读写 IO 操作切到 IO 线程
withContext(Default) 图片解码、Transformation CPU 密集切到 Default
suspendCancellableCoroutine HttpUriFetcher 把 OkHttp 回调变成挂起函数
invokeOnCancellation OkHttp 请求取消 协程取消自动取消 HTTP 请求
coroutineContext.job 传给 requestDelegate 页面销毁取消协程
SupervisorJob imageLoaderScope 单个请求失败不影响其他
结构化并发 整个加载链 取消自动传播,无泄漏
CoroutineDisposable enqueue 返回值 封装 Job,给调用方取消入口
相关推荐
随风一样自由1 小时前
【前端领域】前端开发核心应用场景与落地实践
前端·前端框架
an317422 小时前
弹窗数据流设计的两种高阶架构实践
前端·vue.js·架构
AFinalStone2 小时前
Android 7系统网络(一)全景图与调用链路概览
android·网络·frameworks
谢尔登2 小时前
【React】 状态管理方案
前端·react.js·前端框架
用户2136610035722 小时前
Vue商品详情与放大镜组件
前端·javascript
半个落月2 小时前
从Tapas小Demo理清localStorage、事件与this
前端·javascript
李明卫杭州2 小时前
Vue2 中 v-model 处理不同数据结构的技巧
前端·javascript·vue.js
李明卫杭州2 小时前
使用 computed 处理 v-model 复杂数据结构
前端·javascript·vue.js
用户86022504674722 小时前
Android DEX 内存 Dump 全流程实战:从 APK 提取到无特征内存盲扫
android