Coil 源码分析 一. 加载流程分析

2,493 阅读9分钟

集成

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 多个扩展函数分别对应不同的加载资源类型

  • String
  • HttpUrl
  • Uri
  • File
  • Int
  • Drawable
  • Bitmap 但是最终都会调用 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 的唯一实现类 RealImageLoaderenqueue 函数

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)
    }
}

可以大致看到这里完成了两个工作

  • 启动协程加载资源 (包含资源的获取和解码)称之为
  • 将协程 jobtarget 绑定 jobtarget 绑定的目的是为了让外部可以控制加载任务的执行与停止

再来看资源处理任务- 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 中实际的请求发起的 interceptorCallServerInterceptor,那么 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 判断是否可处理, 处理函数来处理具体的数据。

默认情况下如果我们不传 fetcherCoil 会使用内置的 fetcher 集合,Coil 内置了以下几种 mapperfetcherdecoder

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 的获取与上文 mapperfetcher 的获取方式相同不再赘述。

最后 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