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 两种实现
BaseRequestDelegate (execute 调用,无 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 取消,不重启
}
}
ViewTargetRequestDelegate (imageView.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,给调用方取消入口 |