集成
Coil 一共有 7 个依赖
io.coil-kt:coil
io.coil-kt:coil-base
io.coil-kt:coil-compose
io.coil-kt:coil-compose-base
io.coil-kt:coil-gif
io.coil-kt:coil-svg
io.coil-kt:coil-video
Compose 项目使用
io.coil-kt:coil-compose
或者
io.coil-kt:coil-compose-base
区别在于 base 依赖:
- 不包含
ImageViews.kt相关扩展函数 - 不包含
ImageLoader的单例实现
非 Compose 项目使用
io.coil-kt:coil
或者
io.coil-kt:coil-base
最后三个依赖包含了一些解码器以及 fetchers
基本使用
要将图片显示到 ImageView 上,直接使用 ImageView 的扩展函数 load 即可
// URL
imageView.load("https://www.example.com/image.jpg")
// Resource
imageView.load(R.drawable.image)
// File
imageView.load(File("/path/to/image.jpg"))
// And more...
进一步的配置
imageView.load("https://www.example.com/image.jpg") {
crossfade(true)
placeholder(R.drawable.image)
transformations(CircleCropTransformation())
}
全局配置
Coil.setImageLoader(imageLoader: ImageLoader)
或者
Coil.setImageLoader(factory: ImageLoaderFactory)
加载流程
从构建 ImageRequest开始
graph TD
ImageView.load --> ImageView.loadAny --> RealImageLoader.enqueue --> RealImageLoader.executeMain --> RealImageLoader.executeChain --> ImageView.setImageDrawable
loadAny 时会构建加载需要的 ImageRequest
enqueue 会启一个协程调用 executeMain
然后最终调用 executeChain 一层层调用 interceptor 最终来到 EngineInterceptor
graph TD
资源获取 --> 数据解码 --> DrawableResult
ImageLoader
ImageViews.kt 定义的 扩展方法
inline fun ImageView.load(
uri: String?,
imageLoader: ImageLoader = context.imageLoader,
builder: ImageRequest.Builder.() -> Unit = {}
): Disposable = loadAny(uri, imageLoader, builder)
ImageLoader 是一个接口,这里 imageLoader 的默认值 context.imageLoader
看一下定义
inline val Context.imageLoader: ImageLoader
@JvmName("imageLoader") get() = Coil.imageLoader(this)
如果全局的 imageloader 还没有定义则 Coil.imageLoader 会生成一个默认的全局 imageloader
fun imageLoader(context: Context): ImageLoader = imageLoader ?: newImageLoader(context)
/** Create and set the new singleton [ImageLoader]. */
@Synchronized
private fun newImageLoader(context: Context): ImageLoader {
// Check again in case imageLoader was just set.
imageLoader?.let { return it }
// Create a new ImageLoader.
val newImageLoader = imageLoaderFactory?.newImageLoader()
?: (context.applicationContext as? ImageLoaderFactory)?.newImageLoader()
?: ImageLoader(context)
imageLoaderFactory = null
imageLoader = newImageLoader
return newImageLoader
}
这里生成 imageLoader 实例可以概括为两种:
ImageLoaderFactory生成ImageLoader()直接生成
先来看第一种,ImageLoaderFactory
fun interface ImageLoaderFactory {
/**
* Return a new [ImageLoader].
*/
fun newImageLoader(): ImageLoader
}
ImageLoaderFactory 是个工厂接口,需要自行实现 newInageLoader() 来生成 Imageloader 实例,实际这里可以自行实现 ImageLoader 接口来生成更加定制化的图片加载器。当然,最基础的方式,我们可以让项目的 Application 来实现 ImageLoaderFactory 接口,然后直接通过 ImageLoader.Builder 方式来构建全局的 ImageLoader 从而达到全局配置 Coil 的目的。
再来看看 ImageLoader(),ImageLoader 是一个接口,那它是怎么实例化的呢
companion object {
/** Create a new [ImageLoader] without configuration. */
@JvmStatic
@JvmName("create")
operator fun invoke(context: Context) = Builder(context).build()
}
其实就是用静态 invoke 函数重载了一下 () 表达式,实际上还是会调用自身的 Builder 类来构建 ImageLoader。
fun build(): ImageLoader {
val memoryCache = memoryCache ?: buildDefaultMemoryCache()
return RealImageLoader(
context = applicationContext,
defaults = defaults,
bitmapPool = memoryCache.bitmapPool,
memoryCache = memoryCache,
callFactory = callFactory ?: buildDefaultCallFactory(),
eventListenerFactory = eventListenerFactory ?: EventListener.Factory.NONE,
componentRegistry = componentRegistry ?: ComponentRegistry(),
options = options,
logger = logger
)
}
从这里可以看到默认是实例化了 ImageLoader 的唯一实现类 RealImageLoader,下文也是基于 RealImageLoader 来展开的。感兴趣的可以自己实现一个 ImageLoader。
当然也可以前置设置全局的 imageLoader, 或者 ImageLoaderFactory, 设置 ImageLoaderFactory 的好处在于可以按需初始化
fun interface ImageLoaderFactory {
/**
* Return a new [ImageLoader].
*/
fun newImageLoader(): ImageLoader
}
回到 load 流程上来, ImageView.load 多个扩展函数分别对应不同的加载资源类型
StringHttpUrlUriFileIntDrawableBitmap但是最终都会调用loadAny
@JvmSynthetic
inline fun ImageView.loadAny(
data: Any?,
imageLoader: ImageLoader = context.imageLoader,
builder: ImageRequest.Builder.() -> Unit = {}
): Disposable {
val request = ImageRequest.Builder(context)
.data(data)
.target(this)
.apply(builder)
.build()
return imageLoader.enqueue(request)
}
loadAny 即整个图片加载的入口, 在这里会构建基于传进来的 ImageRequest.Builder 参数的 ImageRequest 对象, ImageRequest 对象包含了单次图片加载所需要的所有资源与 环境
ImageRequest 中比较重要的几个参数
/**
* 1. 自定义 target 加载图片时用作生成 ImageLoader
* 2. 获取图片资源时使用
*/
val context: Context,
/** 加载的资源 */
val data: Any,
/** 加载目标, 实现了 Target 接口的对象 */
val target: Target?,
/** 自定义 Fetcher, 用于获取图片 Raw Data */
val fetcher: Pair<Fetcher<*>, Class<*>>?,
/**
*自定义 decoder, 用于解码 fetcher 获取的数据,
* 最终转换出 Drawable
*/
val decoder: Decoder?,
/** 加载任务协程所在的 dispatcher */
val dispatcher: CoroutineDispatcher,
/** 图片裁剪方式 */
val precision: Precision,
回到 loadAny
构建出 ImageRequest 后来到 imageLoader.enqueue(request)
enqueue
看一下 ImageLoader 的唯一实现类 RealImageLoader 的 enqueue 函数
override fun enqueue(request: ImageRequest): Disposable {
// Start executing the request on the main thread.
val job = scope.launch {
val result = executeMain(request, REQUEST_TYPE_ENQUEUE)
if (result is ErrorResult) throw result.throwable
}
// Update the current request attached to the view and return a new disposable.
return if (request.target is ViewTarget<*>) {
val requestId = request.target.view.requestManager.setCurrentRequestJob(job)
ViewTargetDisposable(requestId, request.target)
} else {
BaseTargetDisposable(job)
}
}
可以大致看到这里完成了两个工作
- 启动协程加载资源 (包含资源的获取和解码)称之为
- 将协程
job与target绑定job与target绑定的目的是为了让外部可以控制加载任务的执行与停止
再来看资源处理任务- executeMain
@MainThread
private suspend fun executeMain(initialRequest: ImageRequest, type: Int): ImageResult {
val request = initialRequest.newBuilder().defaults(defaults).build()
... ...
try {
... ...
if (type == REQUEST_TYPE_ENQUEUE) request.lifecycle.awaitStarted()
val result = executeChain(request, type, size, cached, eventListener)
when (result) {
is SuccessResult -> onSuccess(result, targetDelegate, eventListener)
is ErrorResult -> onError(result, targetDelegate, eventListener)
}
return result
} catch (throwable: Throwable) {
... ...
}
... ...
}
省略了部分逻辑,只看加载相关的逻辑
可以看到 进来先重新 build 了一份 ImageRequest,为什么要这么做呢,可以看到它重新 build 时加了一句
.defaults(defaults)
defaults 是定义在 ImageLoader 中的全局的一份配置,这里的重新 build ImageRequest 的用意也显现出来了,就是为了让全局配置也生效
然后继续看 executeMain
if (type == REQUEST_TYPE_ENQUEUE) request.lifecycle.awaitStarted()
可以从方法名看出来这是一个挂起函数,跟 lificycle 相关,从名字来看就是 wait util 生命周期处于活跃状态时
/** Suspend until [Lifecycle.getCurrentState] is at least [STARTED] */
@MainThread
internal suspend inline fun Lifecycle.awaitStarted() {
// Fast path: we're already started.
if (currentState.isAtLeast(STARTED)) return
// Slow path: observe the lifecycle until we're started.
observeStarted()
}
/** Cannot be 'inline' due to a compiler bug. There is a test that guards against this bug. */
@MainThread
internal suspend fun Lifecycle.observeStarted() {
var observer: LifecycleObserver? = null
try {
suspendCancellableCoroutine<Unit> { continuation ->
observer = object : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) {
continuation.resume(Unit)
}
}
addObserver(observer!!)
}
} finally {
// 'observer' will always be null if this method is marked as 'inline'.
observer?.let(::removeObserver)
}
}
可以看到这里就是做了一个等待生命周期来到 onStart
只有在加载所处的生命周期 onStart 之后才会继续往下执行。
intercept
然后继续看 executeMain
val result = executeChain(request, type, size, cached, eventListener)
private suspend inline fun executeChain(
request: ImageRequest,
type: Int,
size: Size,
cached: Bitmap?,
eventListener: EventListener
): ImageResult {
val chain = RealInterceptorChain(request, type, interceptors, 0, request, size, cached, eventListener)
return if (options.launchInterceptorChainOnMainThread) {
chain.proceed(request)
} else {
withContext(request.dispatcher) {
chain.proceed(request)
}
}
}
看到这里是不是感觉有点似曾相识? 先看看下面这个代码片段:
Response getResponseWithInterceptorChain() throws IOException {
// Build all other interceptors.
interceptors.add(new CallServerInterceptor(forWebSocket));
... ...
Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
originalRequest, this, eventListener, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());
Response response = chain.proceed(originalRequest);
return response;
}
对,这段是 OKHttp 的代码,RealCall 中最终实际责任链调用获取 response 的地方,实际上 Coil 就是借鉴了 OKHttp 的责任链模式来实现图片资源最终的获取与处理, 我们可以在构建全局 ImageLoader 时传入自定义的 Interceptor,来实现对图片的过滤或者其他处理,极大地增强了可扩展性。
继续回到 RealInterceptorChain, 我们来看看实际的网络请求。
OKHttp 中实际的请求发起的 interceptor 是 CallServerInterceptor,那么 Coil 中对应的 interceptor 是谁呢
private val interceptors = registry.interceptors + EngineInterceptor(registry, bitmapPool,
memoryCache.referenceCounter, memoryCache.strongMemoryCache, memoryCacheService, requestService,
systemCallbacks, drawableDecoder, logger)
在 RealImageLoader 中定义的 interceptors 是我们配置的自定义 interceptors 与一个叫 EngineInterceptor 组成的集合, 那么实际的图片资源获取就是它来完成的了, 来看看他的 intercept 方法:
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
try {
val request = chain.request
val context = request.context
val data = request.data
val size = chain.size
val eventListener = chain.eventListener
val mappedData = registry.mapData(data)
val fetcher = request.fetcher(mappedData) ?: registry.requireFetcher(mappedData)
// Fetch, decode, transform, and cache the image on a background dispatcher.
return withContext(request.dispatcher) {
// Fetch and decode the image.
val (drawable, isSampled, dataSource) =
execute(mappedData, fetcher, request, chain.requestType, size, eventListener)
// Return the result.
SuccessResult(
drawable = drawable,
request = request,
metadata = Metadata(
memoryCacheKey = memoryCacheKey.takeIf { isCached },
isSampled = isSampled,
dataSource = dataSource,
isPlaceholderMemoryCacheKeyPresent = chain.cached != null
)
)
}
} catch (throwable: Throwable) {
if (throwable is CancellationException) {
throw throwable
} else {
return requestService.errorResult(chain.request, throwable)
}
}
}
先对图片数据源进行预处理,预处理的目的是将数据源的类型转换为实际使用的类型,举个例子比如 String 类型需要转换为 Uri,对应的 StringMapper:
internal class StringMapper : Mapper<String, Uri> {
override fun map(data: String) = data.toUri()
}
这里数据源预处理转换的 mapper 是由
RealImageLoader初始化时添加- 调用者在初始化
imageLoader时配置的mapper
val mappedData = registry.mapData(data)
interface Mapper<T : Any, V : Any> {
/** Return true if this can convert [data]. */
fun handles(data: T): Boolean = true
/** Convert [data] into [V]. */
fun map(data: T): V
}
handles 函数用来判断数据源类型是否匹配
map 函数对数据源进行处理。
val fetcher = request.fetcher(mappedData) ?: registry.requireFetcher(mappedData)
这一行是用来获取拉取图片资源用到的 fetcher,如果调用者提前在 ImageRequest 中传入了自定义的 fetcher,则本次加载优先使用这个 fetcher,但是请注意,如果调用者传入的 fetcher 与数据源不匹配的话,整个加载流程就此中断并抛出一个 IllegalStateException,设计意图不言而喻,就是你传入的 fetcher 必须跟你传入的 数据源类型要匹配。
interface Fetcher<T : Any> {
fun handles(data: T): Boolean = true
fun key(data: T): String?
suspend fun fetch(
pool: BitmapPool,
data: T,
size: Size,
options: Options
): FetchResult
}
可以看到 fetcher 接口的结构,跟 mapper 是类似的,后文的 deceder 接口也是类似的结构,handle 判断是否可处理, 处理函数来处理具体的数据。
默认情况下如果我们不传 fetcher,Coil 会使用内置的 fetcher 集合,Coil 内置了以下几种 mapper,fetcher,decoder
private val registry = componentRegistry.newBuilder()
// Mappers
.add(StringMapper())
.add(FileUriMapper())
.add(ResourceUriMapper(context))
.add(ResourceIntMapper(context))
// Fetchers
.add(HttpUriFetcher(callFactory))
.add(HttpUrlFetcher(callFactory))
.add(FileFetcher(options.addLastModifiedToFileCacheKey))
.add(AssetUriFetcher(context))
.add(ContentUriFetcher(context))
.add(ResourceUriFetcher(context, drawableDecoder))
.add(DrawableFetcher(drawableDecoder))
.add(BitmapFetcher())
// Decoders
.add(BitmapFactoryDecoder(context))
.build()
registry.requireFetcher
internal fun <T : Any> ComponentRegistry.requireFetcher(data: T): Fetcher<T> {
val result = fetchers.findIndices { (fetcher, type) ->
type.isAssignableFrom(data::class.java) && (fetcher as Fetcher<Any>).handles(data)
}
checkNotNull(result) { "Unable to fetch data. No fetcher supports: $data" }
return result.first as Fetcher<T>
}
同样的,如果 Coil 内部 fetcher 与传入的数据源不匹配的话,加载也会终止。
// Fetch and decode the image.
val (drawable, isSampled, dataSource) =
execute(mappedData, fetcher, request, chain.requestType, size, eventListener)
最后的数据拉取是在 EngineIntercetor.execute 函数中。
private suspend inline fun execute(
data: Any,
fetcher: Fetcher<Any>,
request: ImageRequest,
type: Int,
size: Size,
eventListener: EventListener
): DrawableResult {
val options = requestService.options(request, size, systemCallbacks.isOnline)
val fetchResult = fetcher.fetch(bitmapPool, data, size, options)
val baseResult = when (fetchResult) {
is SourceResult -> {
val decodeResult = try {
val decoder = if (isDiskOnlyPreload) {
EmptyDecoder
} else {
request.decoder ?: registry.requireDecoder(request.data, fetchResult.source, fetchResult.mimeType)
}
val decodeResult = decoder.decode(bitmapPool, fetchResult.source, size, options)
decodeResult
} catch (throwable: Throwable) {
fetchResult.source.closeQuietly()
throw throwable
}
DrawableResult(
drawable = decodeResult.drawable,
isSampled = decodeResult.isSampled,
dataSource = fetchResult.dataSource
)
}
is DrawableResult -> fetchResult
}
(finalResult.drawable as? BitmapDrawable)?.bitmap?.prepareToDraw()
return finalResult
}
这里,最终使用了匹配出来的 fetcher 来对资源进行拉取,这里不同的资源有不同的实现方式,比如如果是个 HttpUrl 资源的话,就会实际去使用 OKHttp 进行相应的网络操作,得到 Raw Data。
val decoder = if (isDiskOnlyPreload) {
EmptyDecoder
} else {
request.decoder ?: registry.requireDecoder(request.data, fetchResult.source, fetchResult.mimeType)
}
val decodeResult = decoder.decode(bitmapPool, fetchResult.source, size, options)
这里 decoder 的获取与上文 mapper 和 fetcher 的获取方式相同不再赘述。
最后 decoder 对 Raw Data 进行解码, 转化为 DecodeResult 对象,包含 Drawable 对象, 加载流程的最后,对 DecodeResult 进行必要的 transformation 操作 (如果有)。
最终回到 executeMain。
when (result) {
is SuccessResult -> onSuccess(result, targetDelegate, eventListener)
is ErrorResult -> onError(result, targetDelegate, eventListener)
}
onSuccess
private suspend inline fun onSuccess(
result: SuccessResult,
targetDelegate: TargetDelegate,
eventListener: EventListener
) {
try {
... ...
targetDelegate.success(result)
......
} finally {
}
}
最终 ImageViewTarget
open class ImageViewTarget(
override val view: ImageView
) : PoolableViewTarget<ImageView>, TransitionTarget, DefaultLifecycleObserver {
override fun onStart(placeholder: Drawable?) = setDrawable(placeholder)
override fun onError(error: Drawable?) = setDrawable(error)
override fun onSuccess(result: Drawable) = setDrawable(result)
}
资源准备完毕之后即显示在 ImageView 之上,整个加载流程即结束。
注意点
dispatcher
可在全局 ImageLoader 构建时配置,也可以在单次请求 ImageRequest 时配置
后续在获取图片资源, 解码等关键耗时步骤协程运行的上下文都会在配置的 dispatcher 下。
Precision
Coil 本身在加载图片时会有一个 优化 操作,在没有明确配置时,Coil 在图片资源加载完成后,对图片资源进行缩放时,默认会朝图片尺寸最小的方向进行裁剪。比如图片资源本身比控件大的话,默认配置下 Coil 会将图片进行缩放至控件大小,二档图片资源比空间本身小的话,默认配置会在控件上展示资源本身大小。
Precision.AUTOMATIC