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图片时序图
1.coil通知imageView加载占位图[5];
2.网络请求获得图片流[10];
3.根据imageView大小,缩放比例,图片要求精度等生成BitmapDrawable[12];
4.最后通知imageView加载网络图片[14];
这个只是网络图片,coil也能支持多种来源,解码多种类型的位图我们接着往下分析
Coil加载多种图片来源,解码多种图片
结合源码我梳理了结构将coil分成如下5个层
- API:暴露给外部接口调用
- middle:加载位图的核心逻辑
- cache:决定是存入LruCache还是WeakReference
- fetcher:加载各种渠道来源的位图
- decode: 图片解码库 我们查看一下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状态开始/取消任务时序图
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缓存
缓存策略时序图
时序图纸只画了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")
循环利用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
)
)
}