携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第11天,点击查看活动详情
介绍
Coil是一个使用Kotlin开发并使用协程(Coroutines)的图片加载库。得益于Kotlin的语法特性,使得Coil在使用上以及轻量化上优于常见的图片加载库(Glide、Fresco等)。但也正是由于依赖于Kotlin的语法特性,Java开发者可能享受不到Coil带来的优势。
基本用法
利用Kotlin的扩展方法语法特性,使得使用Coil将图片加载到ImageView上变得异常简单:
ImageView.load(url)
ImageView.load()
对于还在继续看这篇文章的读者,笔者默认聪明的你们已经认识了扩展方法这一Kotlin特性,就不作展开了。
直接上代码!
inline fun ImageView.load(
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)
}
因为第三个入参builder是一个方法,所以load方法使用了inline关键字,标记为内联方法以提升性能表现。
- data:图片资源的所在地(插播一个笔者OS:这个入参命名为data有些令人混淆,私以为取名source、location等较为贴切)。类型为Any,意味着该方法可以同时处理多种图片源,可选类型具体为:Uri(Android资源、Content、File、HTTP(S) schemes)、String(与Uri类似)、HttpUrl、File、DrawableRes、Drawable、Bitmap、ByteArray、ByteBuffer。
- imageLoader:类型即为ImageLoader。ImageLoader是一个接口,负责ImageRequest管理、缓存、获取数据、图片解码、内存管理等。Coil利用扩展属性为Context内置了一个默认实现。
- builder:ImageRequest的构造器。类型为以ImageRequest.Builder为接收者的无参无返回方法。用于配置ImageRequest。
load方法很简单,利用ImageRequest的构造器构造出一个Request并入队到ImageLoader。我们逐个分析。
ImageRequest.Builder()
创建ImageRequest的构造器,初始化参数。
data()
将data赋值给Builder中的data属性。
target()
fun target(imageView: ImageView) = target(ImageViewTarget(imageView))
fun target(target: Target?) = apply {
this.target = target
resetResolvedValues()
}
将ImageView包装成ImageViewTarget,然后将其赋值给Builder中的target属性。target的类型为Target,数据目的地抽象出来的接口。ImageViewTarget为Target的一个实现类,以ImageView作为目的地。
resetResolvedValues()则是重置一些属性,确保每一次调用build都能重新计算这些属性的值。
apply()
apply是Kotlin的作用域方法之一,没啥好说的,就是将传入的Builder配置应用到实际的Builder中。
build()
构建ImageRequest。
ImageLoader.enqueue()
由于使用的是Coil默认提供的ImageLoader,所以我们先来看看Coil是怎么提供ImageLoader的:
sequenceDiagram
Context ->> Coil: .imageLoader
Coil ->> Coil : newImageLoader
Coil ->> ImageLoader.Builder:build()
ImageLoader.Builder ->> Context :RealImageLoader
从上面的流程图可以看出,实际上构建的是RealImageLoader。值得注意的是,这个RealImageLoader是internal标记的,所以我们无法直接构建出来,但可以使用ImageLoader.Buidler(context).build()来构建自己的RealImageLoader。
接下来看看RealImageLoader的enqueue方法:
override fun enqueue(request: ImageRequest): Disposable {
//构建协程异步任务Deferred<ImageResult>
val job = scope.async {
executeMain(request, REQUEST_TYPE_ENQUEUE).also { result ->
if (result is ErrorResult) logger?.log(TAG, result.throwable)
}
}
//根据Target的类型创建不用的Disposable
return if (request.target is ViewTarget<*>) {
//①
request.target.view.requestManager.getDisposable(job)
} else {
//②
OneShotDisposable(job)
}
}
- 首先构建一个协程的异步Job,执行的内容为executeMain()。不管从什么线程调用,都会被协程调度到主线程上。可能大家有疑惑,加载图片不是耗时操作吗?为什么在主线程上去执行?那是因为在真正需要执行耗时操作的时候,协程会将主线程“挂起”,然后将耗时操作调度到子线程上执行,执行完毕后再自动切换回主线程。而这时候的主线程“挂起”是不阻塞的。不了解协程的读者可能不太能理解,但这里就不展开了,请自行恶补!
- 然后构建一个Disposable并返回。Disposable是Coil定义的一个接口,用于dispose(读者理解为丢弃)某个任务。Disposable有两个实现类:ViewTargetDisposable和OneShotDisposable。分别对应Target为ViewTarget(①处代码)和非ViewTarget(②处代码)的情况。三者关系如下图:
classDiagram
class Disposable{
+ Deferred<ImageResult> job
+ Boolean isDisposed
+ dispose()
}
<<interface>> Disposable
class OneShotDisposable
class ViewTargetDisposable{
+ View view
}
Disposable <|.. OneShotDisposable:实现
Disposable <|.. ViewTargetDisposable:实现
重点是executeMain方法:
private suspend fun executeMain(initialRequest: ImageRequest, type: Int): ImageResult {
//①
val requestDelegate = requestService.requestDelegate(
initialRequest,
coroutineContext.job
).apply { assertActive() }
//②
val request = initialRequest.newBuilder().defaults(defaults).build()
//③
val eventListener = eventListenerFactory.create(request)
try {
if (request.data == NullRequestData) throw NullRequestDataException()
//④
requestDelegate.start()
//⑤
if (type == REQUEST_TYPE_ENQUEUE) request.lifecycle.awaitStarted()
//⑥
val placeholderBitmap = memoryCache?
.get(request.placeholderMemoryCacheKey)?
.bitmap
val placeholder = placeholderBitmap?.toDrawable(request.context)
?: request.placeholder
request.target?.onStart(placeholder)
eventListener.onStart(request)
request.listener?.onStart(request)
//⑦
eventListener.resolveSizeStart(request)
val size = request.sizeResolver.size()
eventListener.resolveSizeEnd(request, size)
val result = withContext(request.interceptorDispatcher) {
//⑧
RealInterceptorChain(
initialRequest = request,
interceptors = interceptors,
index = 0,
request = request,
size = size,
eventListener = eventListener,
isPlaceholderCached = placeholderBitmap != null
).proceed(request)
}
//⑨
when (result) {
is SuccessResult -> onSuccess(result, request.target, eventListener)
is ErrorResult -> onError(result, request.target, eventListener)
}
return result
} catch (throwable: Throwable) {
if (throwable is CancellationException) {
onCancel(request, eventListener)
throw throwable
} else {
val result = requestService.errorResult(request, throwable)
onError(result, request.target, eventListener)
return result
}
} finally {
requestDelegate.complete()
}
}
-
①处代码。将Request和当前的协程Job包装成RequestDelegate,以便能够自动感应Lifecycle来执行Request的取消和重启。RequestDelegate实现了DefaultLifecycleObserver(可以观察LifecycleOwner的生命周期)。RequestDelegate有两个子类:BaseRequestDelegate和ViewTargetRequestDelegate。由于我们的Target是ViewTarget,所以使用的是ViewTargetRequestDelegate,我们来看看。
internal class ViewTargetRequestDelegate( private val imageLoader: ImageLoader, private val initialRequest: ImageRequest, private val target: ViewTarget<*>, private val lifecycle: Lifecycle, private val job: Job ) : RequestDelegate() { @MainThread fun restart() { imageLoader.enqueue(initialRequest) } //目标View需要已经AttachedToWindow才允许执行 override fun assertActive() { if (!target.view.isAttachedToWindow) { //每一个view都会有一个独立的RequestManger,用于管理该View的Request。 //setRequest会将现有的Request丢弃后再赋值,保证一个View只有一个Request。 target.view.requestManager.setRequest(this) throw CancellationException("'ViewTarget.view' must be attached to a window.") } } //开启后代理会观察Lifecycle override fun start() { lifecycle.addObserver(this) if (target is LifecycleObserver) lifecycle.removeAndAddObserver(target) target.view.requestManager.setRequest(this) } //丢弃则取消协程Job并移除Lifecycle监听。 override fun dispose() { job.cancel() if (target is LifecycleObserver) lifecycle.removeObserver(target) lifecycle.removeObserver(this) } //Lifecycle走到Destroy生命周期时,自动丢弃该view的Request。 override fun onDestroy(owner: LifecycleOwner) { target.view.requestManager.dispose() } } -
②处代码。将默认的RequestOptions传入Request
-
③处代码。创建EventListener,由于本例没有指定,所以对应的是EventListener.NONE对象,是一个空实现。虽然如此,但还是可以看一下EventListener的结构:
classDiagram class EventListener{ +onStart() +resolveSizeStart() +resolveSizeEnd() +mapStart() +mapEnd() +keyStart() +keyEnd() +fetchStart() +fetchEnd() +decodeStart() +decodeEnd() +transformEnd() +transformStart() +transitionStart() +transitionEnd() +onError() +onCancel() +onSuccess() +NONE EventListener } class Factory{ +Factory NONE +create() } <<interface>> Factory <<interface>> EventListener EventListener --> Factory可以看到,EventListener几乎将Coil的整个工作流程都包含了,假如有精细粒度控制的需求可以构建自己的EventListener。
-
④处代码。启动RequestDelegate,开始感知Lifecycle。
-
⑤处代码。如果是enqueue调用该方法,则需要等待Lifecycle处于Started状态才继续往下执行。(对于这里的处理方式笔者有点疑问,站在响应速度的角度来说,是不是——先请求了数据,再等待Lifecycle处于Started状态后然后执行显示——会比这里的处理方式好呢?)
-
⑥处代码。获取占位图(如果有)。通知Target请求开始并传递占位图(Target自行处理占位图的显示逻辑);通知EventListener请求开始;通知Request的Listener请求开始。
-
⑦处代码。计算Size。如无指定Size,宽高则为Undefined。
-
⑧处代码。在interceptorDispatcher指定的线程进行拦截器链执行并得到结果。interceptorDispatcher如果未指定则为主线程。
-
⑨处代码。判断result是成功结果还是失败结果,并通知各种Listener。
看到⑧处代码,相信不少人都会心一笑,心里默念:还得是你啊!
没错,这个InterceptorChain跟大名鼎鼎的OKhttp的设计如出一辙!
既然气氛都烘托到这了,那就来复习(也可能是预习)一下这种设计的结构吧。
classDiagram
class Interceptor{
<<interface>>
intercept(Chain chain) Result
}
class Chain{
<<interface>>
proceed(Request request) Result
Request request
}
sequenceDiagram
Invoker ->> Chain : proceed()
Chain ->> First Interceptor : intercept()
First Interceptor ->> Chain : proceed()
Chain ->> Second Interceptor : intercept()
Second Interceptor ->> Chain : proceed()
Chain ->> ... : intercept()
... ->> Chain : proceed()
Chain ->> Last Interceptor : intercept()
Last Interceptor ->> ... : Result
... ->> Second Interceptor : Result
Second Interceptor ->> First Interceptor : Result
First Interceptor ->> Chain : Result
Chain ->> Invoker : Result
看完了InterceptorChain,很自然就会想到,核心的内容就在各个拦截器的Intercept方法中。正当我满怀希望地找寻“各个”拦截器的时候,我发现我只找到了一个拦截器……
也就是说Coil设计这个拦截器链自己没用上,只是为了让调用者使用的!(内心OS:其实我认为可能只是因为Coil作者仰慕OKhttp的设计而做出的致敬行为,纯属脱裤子放屁)
既然只有一个拦截器,那所有的操作都在那一个拦截器里,下一篇我们就从这个拦截器讲起!