Coil源码分析

1,566 阅读3分钟

Coil简介

Coil 是一个 Android 图片加载库,通过 Kotlin 协程的方式加载图片。特点如下:

  • 更快: Coil 在性能上有很多优化,包括内存缓存和磁盘缓存,把缩略图存保存在内存中,循环利用 bitmap,自动暂停和取消图片网络请求等。
  • 更轻量级: Coil 只有2000个方法(前提是你的 APP 里面集成了 OkHttp 和 Coroutines),Coil 和 Picasso 的方法数差不多,相比 Glide 和 Fresco 要轻量很多。
  • 更容易使用: Coil 的 API 充分利用了 Kotlin 语言的新特性,简化和减少了很多样板代码。
  • 更流行: Coil 首选 Kotlin 语言开发并且使用包含 Coroutines, OkHttp, Okio 和 AndroidX Lifecycles 在内最流行的开源库。 更流行更轻量级没啥好打假的,确实如作者所述,我们在看看Coil另外两个特点

更容易使用

代码调用

Glide用法

Glide.with(this)
    .load("https://imagev2.xmcdn.com/group87/M00/06/BF/wKg5J19WEZiTxF5GAALTgwicAmM788.jpg!op_type=3&columns=290&rows=290&magick=png")
    .placeholder(R.drawable.ic_launcher_foreground)
    .error(R.drawable.ic_launcher_foreground)
    .into(imageView)

Coil普通用法

Coil.imageLoader(this).enqueue(
    ImageRequest.Builder(this@TestActivity)
        .data("https://imagev2.xmcdn.com/group87/M00/06/BF/wKg5J19WEZiTxF5GAALTgwicAmM788.jpg!op_type=3&columns=290&rows=290&magick=png")
        .placeholder(R.drawable.ic_launcher_foreground)
        .error(R.drawable.ic_launcher_foreground)
        .target(imageView)
        .build()
)

看到这里很多人都说哪里简单了,还不如Glide,但是作者利用了kotlin的拓展函数,将上面的代码套了壳子于是有了下面的使用方法

imageView.load("https://imagev2.xmcdn.com/group87/M00/06/BF/wKg5J19WEZiTxF5GAALTgwicAmM788.jpg!op_type=3&columns=290&rows=290&magick=png"){
    placeholder(R.drawable.ic_launcher_foreground)
    error(R.drawable.ic_launcher_foreground)
}

Coil建议在Application初始化imageLoader(后文介绍为啥这么做),然后利用Context拓展属性来获得imageLoader单例,如果所有加载占位图和异常图都一致,可以配置在imageLoader初始化中,那么就使用imageView.load(xxx)完成图片加载,这样来看Coil确实更容易使用

首次加载网络图片源码分析

加载http图片时序图 image.png 1.coil通知imageView加载占位图[5];
2.网络请求获得图片流[10];
3.根据imageView大小,缩放比例,图片要求精度等生成BitmapDrawable[12];
4.最后通知imageView加载网络图片[14];

这个只是网络图片,coil也能支持多种来源,解码多种类型的位图我们接着往下分析

Coil加载多种图片来源,解码多种图片

结合源码我梳理了结构将coil分成如下5个层

  • API:暴露给外部接口调用
  • middle:加载位图的核心逻辑
  • cache:决定是存入LruCache还是WeakReference
  • fetcher:加载各种渠道来源的位图
  • decode: 图片解码库 1628648818866.jpg 我们查看一下ImageRequest的源码,发现data是一个any类型,那coil是如何怎么知道要用哪种Fetcher哪种Decode
/**
 * ImageRequest.kt
 * Set the data to load.
 * The default supported data types are:
 * - [String] (mapped to a [Uri])
 * - [Uri] ("android.resource", "content", "file", "http", and "https" schemes only)
 * - [HttpUrl]
 * - [File]
 * - [DrawableRes]
 * - [Drawable]
 * - [Bitmap]
 */
fun data(data: Any?) = apply {
    this.data = data
}

我们在回到时序图请求拦截方法[8],这里先是校验data类型,如果不支持就会直接抛出异常,然后就去获取fetcher

/**
 *EngineInterceptor.kt
 */
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
val mappedData = registry.mapData(data)
val fetcher = request.fetcher(mappedData) ?: 
registry.requireFetcher(mappedData)
}

再看下registry mapData 和 requireFetcher

/**
 *ComponentRegistries.kt
 */
internal fun ComponentRegistry.mapData(data: Any): Any {
    var mappedData = data
    mappers.forEachIndices { (mapper, type) ->
        if (type.isAssignableFrom(mappedData::class.java) && (mapper as Mapper<Any, *>).handles(mappedData)) {
            mappedData = mapper.map(mappedData)
        }
    }
    return mappedData
}
 
@Suppress("UNCHECKED_CAST")
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>
}

mapData:判断传进来Any类型的data是否是集合中某一个类型,然后生成mappedData对象 requireFetcher:根据mappedData对象找到集合中的某一个fetcher
这两个集合是哪里来的呢?我们在去看下RealImageLoader类

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

RealImageLoader初始化的时候就会创建Mappers和fetchers集合

以string类型的url为例:
1.ComponentRegistry.mapData根据String遍历Mappers集合得到StringMapper,调用 map把string url转成Uri对象
2.根据uri遍历fetchers集合得到HttpUriFetcher,然后执行fetch拿到图片流
从这个逻辑就能回答为啥作者期望你在Applcation里面创建单例的ImageLoader,毕竟要初始化这么多能力,用单例能达到性能最优,相比于每次都去创建ImageLoader

更快

我没有看过其他几个库源码,所以不敢妄言coil比其他人更快,Coil 在更快里面介绍自己说了三个特点(1)自动暂停和取消图片网络请求,(2)内存缓存和磁盘缓存,(3)循环利用bitmap我们就来验证一下

自动暂停和取消图片网络请求

细心的同学发现放在ativity onCreate里面执行网络加载图片会打印如下log,会先取消,然后再一次加载

RealImageLoader: 🏗  Cancelled - https://imagev2.xmcdn.com/group87/M00/06/BF/wKg5J19WEZiTxF5GAALTgwicAmM788.jpg!op_type=3&columns=290&rows=290&magick=png
RealImageLoader: ☁️  Successful (NETWORK) - https://imagev2.xmcdn.com/group87/M00/06/BF/wKg5J19WEZiTxF5GAALTgwicAmM788.jpg!op_type=3&columns=290&rows=290&magick=png

根据view状态开始/取消任务时序图

image.png 1.第一次请求加载时DelegateService在方法createRequestDelegate判断view拓展属性isAttachedToWindowCompat时,发现ImageView还没有可见,所以取消了加载job[6];
2.RealImageLoder 捕获到CancellationException打印了第一个日志[7];
3.DelegateService在方法createRequestDelegate通过拓展函数给ImageView添加了View.OnAttachStateChangeListener监听,等到ImageView可见时执行了回调函数[8];
4.最后把任务加到队列里面重新开始执行[9];
5.假设在任务结束之前我们finish调了页面,coil一样会取消job[12]

这里就利用了lifecycle和view可见监听实现了自动暂停和取消图片网络请求

内存缓存和磁盘缓存

这边就是图片加载的重要逻辑了,coil内存缓存分成StrongMemoryCache/LruCache,weakMemoryCache/WeakReference,磁盘缓存利用了OkHttpClient缓存
缓存策略时序图

image.png 时序图纸只画了StrongMemoryCache,还有weakMemoryCache,coil是如何决定用哪种呢?
第一次:
buildDefaultMemoryCache()根据设定的百分比计算LruCache最多使用的内存大小maxSize 根据maxSize,和trackWeakReferences布尔值来决定使用哪种

internal interface StrongMemoryCache {

    companion object {
        operator fun invoke(
            weakMemoryCache: WeakMemoryCache,
            referenceCounter: BitmapReferenceCounter,
            maxSize: Int,
            logger: Logger?
        ): StrongMemoryCache {
            return when {
                maxSize > 0 -> RealStrongMemoryCache(weakMemoryCache, referenceCounter, maxSize, logger)
                weakMemoryCache is RealWeakMemoryCache -> ForwardingStrongMemoryCache(weakMemoryCache)
                else -> EmptyStrongMemoryCache
            }
        }
    }
}

第二次:
如果需要加载的图片大小没法存入lruCache,也会启用weakMemoryCache

@Synchronized
override fun set(key: Key, bitmap: Bitmap, isSampled: Boolean) {
    // If the bitmap is too big for the cache, don't even attempt to store it. Doing so will cause
    // the cache to be cleared. Instead just evict an existing element with the same key if it exists.
    val size = bitmap.allocationByteCountCompat + cache.size()
    if (size > maxSize) {
        val previous = cache.remove(key)
        if (previous == null) {
            // If previous != null, the value was already added to the weak memory cache in LruCache.entryRemoved.
            weakMemoryCache.set(key, bitmap, isSampled, size)
        }
        return
    }
    Log.d("RealImageLoader","RealStrongMemoryCache increment $bitmap")
    referenceCounter.increment(bitmap)
    cache.put(key, InternalValue(bitmap, isSampled, size))
}

磁盘缓存:
coil的磁盘缓存只针对网络资源,创建ImageLoader单例时,okhttpClient会在data分区下面创建10M大小的image_cache文件夹,设置缓存有效期时一年,在有效期内再次网络请求会从disk加载

// To create the an optimized Coil disk cache, use CoilUtils.createDefaultCache(context).
val cacheDirectory = File(filesDir, "image_cache").apply { mkdirs() }
val cache = Cache(cacheDirectory, 10*1024*1024)

// Rewrite the Cache-Control header to cache all responses for a year.
val cacheControlInterceptor = ResponseHeaderInterceptor("Cache-Control", "max-age=31536000,public")

image.png

循环利用bitmap

每次加载之前,先检查内存中是否含有位图,如果有则直接返回

val value = if (request.memoryCachePolicy.readEnabled) memoryCacheService[memoryCacheKey] else null
// Short circuit if the cached bitmap is valid.
if (value != null && isCachedValueValid(memoryCacheKey, value, request, size)) {
    return SuccessResult(
        drawable = value.bitmap.toDrawable(context),
        request = request,
        metadata = Metadata(
            memoryCacheKey = memoryCacheKey,
            isSampled = value.isSampled,
            dataSource = DataSource.MEMORY_CACHE,
            isPlaceholderMemoryCacheKeyPresent = chain.cached != null
        )
    )
}

总结

image.png