Coil是Android平台上又一个开源的图片加载库,尽管Android平台已经有诸如Picasso,Glide以及Fresco等非常成熟且优秀的图片加载库了,但Coil最主要的特色就是融合了当下Android开发界最主流的技术和趋势,采用Kotlin为开发语言,将协程、OKHttp、OKIO和AndroidX作为一等公民,以期打造成一个更加轻快、现代化的图片加载库。具体而言包含以下几个方面:
- 发挥Kotlin的语言特性,利用扩展函数、内联、lambda参数以及密封类来创建简单优雅的API。
- 利用了Kotlin协程强大的 可取消的非阻塞式异步编程和对线程最大化利用的特性。
- 使用现代化的依赖库:OKHttp、OKIO基本上已经是目前大部分app的事实“标准”库,它们强大的特性让Coil避免了重复实现磁盘缓存和缓冲流;类似的,AndroidX-LifeCycle也是官方推荐的,Coil目前是唯一一个对其支持的图片加载库。
- 轻量:Coil项目的代码量几乎只有Glide的1/8,更是远远小于Fresco;并且对APK仅增加了大约1500个方法(对于那些已经依赖的OKHttp和协程的app来说),和Picasso相当并显著低于Glide和Fresco。
- 支持扩展:Coil的image-pipline主要由 Mappers , Fetchers , 和 Decoders 三个类组成,可以方便地用于自定义:扩展或覆盖默认行为,或增加对新的文件类型的支持。
- 测试友好化:Coil的基础服务类是 ClassLoader ,它是一个接口,可以方便地编写对应的实现类进行测试;并且Coil同时提供了单例和非单例对象来支持依赖注入。
- 没有annotation processing:annotation processing一般会降低编译速度,Coil通过Kotlin扩展函数来避免。
Coil目前支持其它图片加载库所包含的所有功能,除此之外它还有一个独特的特性:动态采样(Dynamic image sampling),简而言之就是可以在内存中只缓存了一个低质量的图片而此时需要显示同一个高质量的图片时,Coil可以先把低质量的图片作为 ImageView 的 placeHolder 并同时去磁盘缓存中读取对应的高质量图片最后以“渐进式”的方式替换并最终显示到视图中,例如最常见的从图片列表到预览大图的场景。 以上就是Coil目前的大致介绍,下面我们对Coil的API进行一个简单的使用预览和介绍。
集成
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
使用
①Coil 可以在 mavenCentral() 下载
implementation("io.coil-kt:coil:1.2.1")
1.
②可以使用 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"))
1.2.3.4.5.6.
可以使用 lambda 语法轻松配置请求选项:
imageView.load("https://www.example.com/image.jpg") {
crossfade(true)
placeholder(R.drawable.image)
transformations(CircleCropTransformation())
}
1.2.3.4.5.
error
imageView.load(url) {
error(R.drawable.error)
}
1.2.3.
placeholder预置展位图
imageView.load(url) {
placeholder(R.drawable.placeholder)
crossfade(3000)
}
1.2.3.4.
crossfade的动画时间
imageView.load(url) {
crossfade(3000)
}
1.2.3.
③transformations
Coil默认提供了四种变换:模糊变换(BlurTransformation)、圆形变换(CircleCropTransformation)、灰度变换(GrayscaleTransformation)和圆角变换(RoundedCornersTransformation)
基础用法:
imageView.load(IMAGE_URL){
transformations(GrayscaleTransformation())
}
1.2.3.
直接加入变换就可以, 同时可支持多种变换:
imageView.load(IMAGE_URL) {
transformations(GrayscaleTransformation(),
RoundedCornersTransformation(topLeft = 2f, topRight =
2f,bottomLeft = 40f, bottomRight = 40f))
}
1.2.3.4.5.
④Gif加载
Coil基础包中是不支持Gif加载的,需要添加extend包:
implementation("io.coil-kt:coil-gif:0.9.5")
1.
此时需要更改一下代码的方式,在imageLoader中注册Gif组件:
val gifImageLoader = ImageLoader(this) {
componentRegistry {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
add(ImageDecoderDecoder())
} else {
add(GifDecoder())
}
}
}
1.2.3.4.5.6.7.8.9.
使用本组件之后,ImageView可直接使用:
id_image_gif.load(GIF_IMAGE_URL, gifImageLoader)
1.
⑤SVG加载
Coil也可以进行SVG加载的,同gif一样,也是需要添加extend包的:
implementation("io.coil-kt:coil-svg:0.9.5")
1.
代码如下:
val svgImageLoader = ImageLoader(this){
componentRegistry {
add(SvgDecoder(this@MainActivity))
}
}
id_image_svg.load(R.drawable.ic_directions_bus_black_24dp, svgImageLoader)
1.2.3.4.5.6.
加载流程
从构建 ImageRequest开始
ImageView.loadImageView.loadAnyRealImageLoader.enqueueRealImageLoader.executeMainRealImageLoader.executeChainImageView.setImageDrawable
loadAny` 时会构建加载需要的 `ImageRequest
enqueue` 会启一个***协程***调用 `executeMain
然后最终调用 executeChain 一层层调用 interceptor 最终来到 EngineInterceptor
资源获取数据解码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 的唯一实现类 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
Coil可以配合Kotlin协程实现图片加载:
● 加载性能好:缓存管理(MemCache、DiskCache)、动态采样(Dynamic image sampling)、加载中暂停/终止等功能有助于提高图片加载效率 ● 体积小:其包体积与Picasso相当,显著低于Glide和Fresco,仅仅只有1500个方法,但是在功能上却不输于其他同类库; ● 简单易用:配合Kotlin扩展方法等语法优势,API简单易用; ● 技术先进:基于Coroutine、OkHttp、Okio、AndroidX等先端技术开发,确保了技术上的先进性;