Coil KMP 跨端图片库源码分析
1. 概述
1.1 简介
Coil 图片加载库正逐步扩展对 Kotlin Multiplatform (KMP) 的支持,未来将覆盖 Android、iOS 和 Desktop 平台。它基于 KMP 开发,充分利用 Kotlin 协程和跨平台特性,致力于提供统一、高效的图片加载解决方案。
主要目标:
- 提供统一的跨平台图片加载 API
- 高性能的图片加载和缓存
- 支持 Compose Multiplatform
- 轻量级设计
1.2 核心特性
-
跨平台支持
- Android/iOS/Desktop 统一 API
- 针对平台特定优化
-
现代化设计
- Kotlin Coroutines 异步处理
- Compose Multiplatform 支持
- Flow API
-
高性能
- 多级缓存
- 内存优化
- 并发控制
-
扩展性
- 自定义 ImageLoader
- 自定义拦截器、组件
- 灵活的配置项
2. 架构
2.1 整体架构
Coil 采用清晰的三层架构设计,每层职责明确,相互协作又相对独立:
Compose UI 层
作为用户交互的门面,负责:
- 提供 Compose UI 图片组件
- 管理图片加载状态和生命周期
- 处理用户交互和视图更新
请求核心层
作为整个库的核心系统,负责:
- 调度图片加载请求和解码
- 管理内存和磁盘缓存
- 控制并发和队列优先级
平台层
作为底层实现的适配器,负责:
- 处理平台特定的图片请求和解码
- 优化平台特定的性能表现
- 提供原生 API 的桥接能力
架构图
时序图
2.2 UI 层
Coil 在 Compose UI 层提供了图片加载支持,其中核心组件 AsyncImage 专为声明式架构设计,弥补了原生 Image 组件无法直接请求资源的不足。它内置了完善的基于 State 的状态管理系统,通过状态流转机制,能够轻松适配 Compose 的声明式特性,实现图片加载过程中的自动刷新与动态更新。
// 1. AsyncImage Composable
@Composable
fun AsyncImage(
model: Any?,
contentDescription: String?,
modifier: Modifier = Modifier,
transform: (State) -> State = DefaultTransform,
onState: ((State) -> Unit)? = null,
// ... 其他参数
)
AsyncImage 的核心能力依赖于 AsyncImagePainter 实现。作为一个同时继承自 Painter 和 RememberObserver 的类,AsyncImagePainter 不仅能够接入 Image 的绘制能力(Image 接收一个 painter 作为参数),还可以响应 Compose 的重组机制,实现高效的状态刷新。
// 2. rememberAsyncImagePainter
@Stable
class AsyncImagePainter internal constructor(
input: Input,
) : Painter(), RememberObserver {
// ...
// 驱动重组的关键对象:改变时,触发 DrawScope.onDraw 绘制
private var painter: Painter? by mutableStateOf(null)
override fun DrawScope.onDraw() {
// ...
// 绘制当前 Painter
painter?.apply { draw(size, alpha, colorFilter) }
}
// Compose 组件激活时触发
@OptIn(ExperimentalCoroutinesApi::class)
override fun onRemembered() = trace("AsyncImagePainter.onRemembered") {
// 动图起播 比如 Loading 动画
(painter as? RememberObserver)?.onRemembered()
// 开启工作线程
rememberJob = scope.launch {
restartSignal
.flatMapLatest { _input }
.mapLatest { input ->
// 调用 ImageLoader 加载图片
val previewHandler = previewHandler
if (previewHandler != null) {
val request = updateRequest(input.request, isPreview = true)
previewHandler.handle(input.imageLoader, request)
} else {
val request = updateRequest(input.request, isPreview = false)
input.imageLoader.execute(request).toState()
}
}
// 返回结果,更新状态
.collect(::updateState)
}
}
// Compose 组件移除触发
override fun onForgotten() {
rememberJob = null
// 动图停播
(painter as? RememberObserver)?.onForgotten()
}
// 根据请求结果更新状态、Painter
private fun updateState(state: State) {
val previous = _state.value
val current = transform(state)
_state.value = current
// 更新 Painter
painter = maybeNewCrossfadePainter(previous, current, contentScale) ?: current.painter
if (previous.painter !== current.painter) {
// 原状态下的动图停播
(previous.painter as? RememberObserver)?.onForgotten()
(current.painter as? RememberObserver)?.onRemembered()
}
// ...
}
AsyncImagePainter 的重组逻辑主要依赖其内部的另一个 painter 成员,这是一个 State 的对象。当 painter 状态发生变化时,会触发 Compose 重组并调用其绘制函数 onDraw 进行更新。而 painter 的状态更新则由图片加载器 ImageLoader 的加载结果驱动(通过 updateState 函数更新),从而实现图片加载与 UI 绘制的无缝衔接。
2.3 请求核心层
ImageLoader 是 Coil 框架的核心组件,负责发起图片加载流程,并协调各个功能模块,依次完成图片资源的请求、解码、缓存等工作。其设计基于责任链模式,通过模块化的职责分工实现灵活的加载流程。
责任链中的核心职责
ImageLoader 的加载流程由一系列职责模块组成,通过责任链串联起来,主要包括以下功能:
Mapper 转换器
用于统一和转换请求资源的类型。例如,在 Android 平台中,资源 ID 和资源 URI 都可以表示应用资源。通过 Mapper,资源 ID 会被转换为 URI,从而实现两者共享同一个资源请求器。MemoryCache 内存缓存
在生成资源唯一 Key 的同时,负责内存缓存的读取与写入,提升加载速度并减少重复解码。DiskCache 磁盘缓存
用于缓存原始资源(如网络图片的未解码数据),减少重复的网络请求。Decoder 解码器
负责解码不同格式的图片资源(如 JPEG、PNG、WebP 等),将原始数据转换为可用的位图。Fetcher 请求器
用于请求不同来源的图片资源,包括网络图片、应用内资源、本地文件路径等。
EngineInterceptor:责任链的调度核心
尽管上述职责模块看似独立,但它们并未直接封装在责任链的拦截器中。ImageLoader 的责任链内部仅包含一个核心拦截器——EngineInterceptor。顾名思义,EngineInterceptor 的作用是调度整个加载流程,将 map -> memory -> disk -> fetch -> decode
的职责串联起来。
这些职责模块本身并非具体功能的实现体,而是功能组件的调度器。 功能组件则通过 ComponentRegistry 对象进行统一管理和调度。
ComponentRegistry:功能组件的管理中心
ComponentRegistry 是 ImageLoader 的功能组件注册中心,保存了大量负责不同能力的功能组件。通过遍历这些组件,ImageLoader 实现了动态的功能调度。例如:
Fetcher 调度
fetch 操作会遍历 ComponentRegistry 中的请求器工厂列表(fetcherFactories),找到符合当前请求需求的工厂,并通过该工厂实例化具体的请求器来响应请求。Decoder 调度
类似地,decode 操作会遍历解码器工厂列表,动态选择合适的解码器来处理图片资源。
这样设计使得 ImageLoader 的功能高度模块化和可扩展,开发者可以通过自定义组件轻松扩展框架的能力。
2.4 平台层
在大多数情况下,代码是共享的。然而,共享代码在使用平台特性时存在一定的局限性。为弥补跨平台开发的这些劣势,平台层的设计旨在充分利用各平台的特性,通过差异化实现来完善功能的同时提升性能。
其中,最具代表性的功能是解码模块。以移动平台为例,Android 基于原生 Framework 渲染框架,因此在高版本系统中更适合使用支持硬件解码的 ImageDecoder。而 iOS 在 KMM 中依赖 Skia UI 框架,因此使用 Skia 自身的解码能力则更加高效和适配。因此,针对不同平台,Coil 分别采用了 StaticImageDecoder 和 SkiaImageDecoder 作为解码组件,充分发挥了各平台的性能优势。
// 跨平台抽象层
internal expect fun ComponentRegistry.Builder.addAndroidComponents(
options: RealImageLoader.Options,
): ComponentRegistry.Builder
// main
internal actual fun ComponentRegistry.Builder.addAndroidComponents(
options: RealImageLoader.Options,
): ComponentRegistry.Builder = apply {
...
// Decoders
val parallelismLock = Semaphore(options.bitmapFactoryMaxParallelism)
if (enableStaticImageDecoder(options)) {
add(
StaticImageDecoder.Factory(
parallelismLock = parallelismLock,
),
)
}
add(
BitmapFactoryDecoder.Factory(
parallelismLock = parallelismLock,
exifOrientationStrategy = options.bitmapFactoryExifOrientationStrategy,
),
)
}
private fun enableStaticImageDecoder(options: RealImageLoader.Options): Boolean {
// Require API 29 for ImageDecoder support as API 28 has framework bugs:
// https://github.com/element-hq/element-android/pull/7184
return SDK_INT >= 29 &&
options.bitmapFactoryExifOrientationStrategy.let { it == RESPECT_PERFORMANCE || it == RESPECT_ALL }
}
// nonAndroidMain
internal actual fun ComponentRegistry.Builder.addAndroidComponents(
options: RealImageLoader.Options,
): ComponentRegistry.Builder {
return this
// Decoders
.add(SkiaImageDecoder.Factory())
}
3. 核心模块
3.1 核心 API
3.2 请求模块
在图片加载框架中,Fetcher
是负责从不同来源获取图片数据的核心组件。根据资源类型,Fetcher 可分为以下三类:
网络图片请求器
用途:从网络 URL 加载远程图片资源。
-
示例 URL:
"https://www.sample.com/sample.jpg" "http://www.example.com/image.png"
-
实现类:
NetworkFetcher
- 基于
Ktor
实现高效的网络请求。
- 基于
应用内图片资源请求器
用途:加载应用内置的图片资源(如
res/drawable
或assets
目录中的资源)。-
示例资源:
R.drawable.sample sample_image
-
实现类:
-
Android:
-
ResourceUriFetcher
:加载res/drawable
资源。 -
AssetUriFetcher
:加载assets
目录资源。
-
-
iOS:
- 开发中
-
Android:
Coil 确实在向跨平台图片加载库的方向发展,但目前仍主要支持 Android 平台,其他平台的支持正在开发中。
本地路径资源请求器
用途:加载设备存储中的图片文件(如文件系统路径或媒体库资源)。
-
示例路径:
// Android 示例 "content://media/external/images/media/12345" // 媒体库图片 "file:///storage/emulated/0/Download/sample.jpg" // 文件路径 // iOS 示例 "file:///var/mobile/Containers/Data/sample.jpg" // 沙盒文件路径 "assets-library://asset/asset.jpg" // 相册图片
-
实现类:
-
Android:
-
ContentUriFetcher
:处理content://
URI。 -
FileUriFetcher
:处理file://
URI。
-
-
iOS:
- 开发中
-
Android:
3.3 缓存模块
内存缓存
Coil 采用双层内存缓存机制:强引用 LruCache 和弱引用缓存。缓存操作遵循优先级策略:写入时优先存入强引用缓存,当强引用缓存项被移除时会自动降级到弱引用缓存;读取时则优先查找强引用缓存,未命中时再查找弱引用缓存。
磁盘缓存
Coil 在磁盘缓存方面采用了 DiskLruCache 机制,并结合 Okio 实现跨平台的文件读写操作。
internal class RealDiskCache(
override val maxSize: Long,
override val directory: Path,
override val fileSystem: FileSystem, // okio.FileSystem
cleanupDispatcher: CoroutineDispatcher,
) : DiskCache {
private val cache = DiskLruCache(
fileSystem = fileSystem,
directory = directory,
cleanupDispatcher = cleanupDispatcher,
maxSize = maxSize,
appVersion = 3,
valueCount = 2,
)
...
3.4 解码模块
Coil 针对不同平台优化了解码方式:在 Kotlin/JVM 后端使用 Android 原生解码,在 Kotlin/Native 后端则采用 Skia 解码,以适配各平台的性能和功能需求。
静态图
-
Android:
- API > 28:StaticImageDecoder
- API <= 28:BitmapFactoryDecoder
-
iOS:
- SkiaImageDecoder
动态图
-
Android:
- API >= 28:AnimatedImageDecoder
- API < 28:GifDecoder
-
iOS:
- 未实现
虽然 Coil 未直接提供 iOS 平台的动图解码器,但借助 Skia 的解码能力仍可实现。通过使用
Codec.makeFromData
解码动图,并封装一个继承自Painter
和RememberObserver
的SkiaGifPainter
,可以在基于 Compose 的环境中实现动图的解码、播放以及自刷新组件。
4. 可能的潜在问题
-
主线程创建对象问题
-
ImageRequest
的创建过程发生在主线程,在部分平台(如 iOS 和 Harmony)上,这可能导致性能劣化,尤其是在以 Kotlin/Native 为后端的平台上表现更为明显。
-
-
Skia 软解码性能劣势
- 以 Kotlin/Native 为后端的平台依赖 Skia 进行软解码,无法利用硬件解码器,导致解码性能相较硬件解码存在劣势。
5. 总结
Coil KMP 是一个设计优秀的跨平台图片加载框架,虽然目前跨平台功能尚未完全完善,但其灵活的架构和模块化设计,使开发者可以基于它轻松扩展,打造真正的跨平台图片加载库。
compose-imageLoader 是一个真正实现跨平台的图片加载库。从源码来看,该库的实现几乎完全沿用了 Coil 的框架设计,这也从侧面证明了 Coil 框架设计的优秀与通用性。
优势:
- 统一的跨平台 API
- 现代化的协程基础设施
- 优秀的缓存策略
- 良好的扩展性
不足:
- 部分平台亟待完善
- 平台特定优化有限
- 生态相对不够成熟
总的来说,Coil KMP 为跨平台开发提供了一个可靠的图片加载解决方案,值得在实际项目中采用。