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,给调用方取消入口
相关推荐
YM52e3 小时前
鸿蒙Flutter Padding内边距:EdgeInsets详解
android·学习·flutter·华为·harmonyos·鸿蒙
777VG6 小时前
PostgreSQL +martin将多张表输出成一个 MVT
前端·数据库·postgresql
mayaairi6 小时前
JS循环语句深度解析:嵌套for、while与do...while
开发语言·前端·javascript
To_OC6 小时前
啃完流式输出:从一个卡顿的 LLM 接口开始,我搞懂了数据流到底怎么 “流”
前端·javascript·llm
阳光是sunny7 小时前
LangGraph实战教程:defer延迟节点——让收尾工作自动排到最后
前端·人工智能·后端
kyriewen7 小时前
我用了三周Claude Code Skills——总结出5条铁律,第3条最反直觉
前端·ai编程·claude
阳光是sunny7 小时前
LangGraph实战教程:控制流详解
前端·人工智能·后端
Rex叶然7 小时前
ScreenMatch适配安卓屏幕脚本,用于老项目
android·screenmatch·安卓屏幕适配
阿pin7 小时前
Android随笔-Activity启动流程深度分析
android·activity启动流程
格尔曼Noah8 小时前
Safari浏览器中如何只允许指定网站下载
前端·safari