Kotlin开发首选!轻量级协程图片加载库—Coil,真香!
作者:互联网
前言:
Coil是Android上的一个全新的图片加载框架,全名叫做 coroutine image loader
,即协程图片加载库。与传统的图片加载库Glide,Picasso或Fresco等相比。该具有轻量(只有大约1500个方法)、快、易于使用、更现代的API等优势。
它支持GIF和SVG,并且可以执行四个默认转换:模糊,圆形裁剪,灰度和圆角。
采用全Kotlin语言编写,如果你是纯Kotlin项目的话,那么这个库应该是你的首选。
本文参考:
https://juejin.cn/post/6897872882051842061(叶志陈)
https://github.com/coil-kt/coil
https://coil-kt.github.io/coil/transitions/
一、Coil 是什么
Coil 是一个新兴的 Android 图片加载库,使用 Kotlin 协程来加载图片,有以下几个特点:
- 更快: Coil 在性能上做了很多优化,包括内存缓存和磁盘缓存、对内存中的图片进行采样、复用 Bitmap、支持根据生命周期变化自动暂停和取消图片请求等
- 更轻量级: Coil 大约会给你的 App 增加两千个方法(前提是你的 App 已经集成了 OkHttp 和 Coroutines),Coil 的方法数和 Picasso 相当,相比 Glide 和 Fresco 要轻量级很多
- 更容易使用: Coil’s API 充分利用了 Kotlin 语言的新特性,简化并减少了很多重复代码
- 更流行: Coil 首选 Kotlin 语言开发,并且使用包含 Coroutines、OkHttp、Okio 和 AndroidX Lifecycles 在内的更现代化的开源库
二、引入 Coil
环境要求
AndroidX
Min SDK 14+
Java 8+
要启用 Java 8,需要在项目的 Gradle 构建脚本中添加如下配置:
Gradle (.gradle
):
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
Gradle Kotlin DSL (.gradle.kts
):
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
Coil 一共包含五个组件,可以在 mavenCentral()
上获取到
io.coil-kt:coil-base
。基础组件,提供了基本的图片请求、图片解码、图片缓存、Bitmap 复用等功能io.coil-kt:coil
。默认组件,依赖于io.coil-kt:coil-base
,提供了 Coil 类的单例对象以及 ImageView 相关的扩展函数io.coil-kt:coil-gif
。包含两个 decoder 用于支持解码 GIFs,有关更多详细信息,请参见 GIFio.coil-kt:coil-svg
。包含一个 decoder 用于支持解码 SVG。有关更多详细信息,请参见 SVGio.coil-kt:coil-video
。包含两个 fetchers 用于支持读取和解码 任何 Android 的支持的视频格式 的视频帧。有关更多详细信息,请参见 videos
当前 Coil 最新的 release 版本是 1.0.0,引入如下依赖就包含了 Coil 最基础的图片加载功能
implementation("io.coil-kt:coil:1.0.0")
如果想要显示 Gif、SVG、视频帧等类型的图片,则需要额外引入对应的支持库:
implementation("io.coil-kt:coil-gif:1.0.0")
implementation("io.coil-kt:coil-svg:1.0.0")
implementation("io.coil-kt:coil-video:1.0.0")
三、快速入门
1、load
要将图片显示到 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...
使用可选的 lambda 块来添加配置项
imageView.load("https://www.example.com/image.jpg") {
crossfade(true) //淡入淡出
placeholder(R.drawable.image) //占位图
transformations(CircleCropTransformation()) //图片变换,将图片转为圆形
}
2、ImageRequest
如果要将图片加载到自定义的 target 中,可以通过 ImageRequest.Builder 来构建 ImageRequest 实例,并将请求提交给 ImageLoader
val request = ImageRequest.Builder(context)
.data("https://www.example.com/image.jpg")
.target { drawable ->
// Handle the result.
}
.build()
context.imageLoader.enqueue(request)
3、ImageLoader
imageView.load
使用单例对象 imageLoader 来执行 ImageRequest,可以使用 Context 的扩展函数来访问 ImageLoader
val imageLoader = context.imageLoader
可选地,你也可以构建自己的 ImageLoader 实例,并赋值给 Coil 来实现全局使用
Coil.setImageLoader(
ImageLoader.Builder(application)
.placeholder(ActivityCompat.getDrawable(application, R.drawable.icon_loading))
.error(ActivityCompat.getDrawable(application, R.drawable.icon_error))
.build()
)
4、execute
如果想直接拿到目标图片,可以调用 ImageLoader 的execute
方法来实现
val request = ImageRequest.Builder(context)
.data("https://www.example.com/image.jpg")
.build()
val drawable = imageLoader.execute(request).drawable
5、R8 / Proguard
Coil 开箱即用,与 R8 完全兼容,不需要添加任何额外规则
如果你使用了 Proguard,你可能需要添加对应的混淆规则:Coroutines、OkHttp and Okio。
6、License
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
四、大体框架
Coil 在我看来是一个比较“激进”的开源库,热衷于使用当前最为流行的技术,包括 Coroutines、OkHttp、Okio,以及 Google 官方的 Jetpack Lifecycles、AndroidX 等,代码不仅完全由 Kotlin 语言来实现,连 gradle 脚本也是全部使用 kts,而且 gradle 版本也升级得很快,我一开始由于使用的 Android Studio 不是 4.x 版本,连 Coil 代码都跑不起来 =_=
如果你的项目中已经大面积使用到了这些开源库和组件的话,那么 Coil 会更加契合你的项目
当前在 Android 端最为流行的图片加载框架应该是 Glide 了,Coil 作为一个后起之秀相对 Glide 也有着一些独特的优势。例如,为了监听 UI 层的生命周期变化,Glide 是通过向 Activity 或者 Fragment 注入一个无 UI 界面的 Fragment 来实现间接监听的,而 Coil 则只需要直接监听 Lifecycle 即可,在实现方式上 Coil 会更加简单高效。此外,在联网请求图片的时候,Glide 需要通过线程池和多个回调来完成最终图片的显示,而 Coil 由于使用了 Kotlin 协程,可以很简洁地完成异步加载和线程切换,在流程上 Coil 会清晰很多。但实际上 Coil 也是借鉴了一些优秀开源库的实现思路,所以我看 Coil 的源码的时候就总会发现一些 Glide 和 OkHttp 的影子
这里就先来对 Coil 的各个特性和 Glide 做下简单的对比,先让读者有个大体的印象
- 实现语言
- Glide 全盘使用 Java 语言来实现,对于 Java 和 Kotlin 语言的友好程度差不多
- Coil 全盘使用 Kotlin 语言来实现,为 ImageView 声明了多个用于加载图片的扩展函数,对 Kotlin 语言的友好程度会更高很多
- 网络请求
- Glide 默认是使用 HttpURLConnection,但也提供了更换网络请求实现途径的入口
- Coil 默认是使用 OkHttp,但也提供了更换网络请求实现途径的入口
- 生命周期监听
- Glide 通过向 Activity 或者 Fragment 注入一个无 UI 界面的 Fragment 来实现监听
- Coil 直接通过 Lifecycle 来实现监听
- 内存缓存
- Glide 的内存缓存分为 ActiveResources 和 MemoryCache 两级
- Coil 的内存缓存分为 WeakMemoryCache 和 StrongMemoryCache 两级,本质上和 Glide 一样
- 磁盘缓存
- Glide 在加载到图片后通过 DiskLruCache 来进行磁盘缓存,且提供了是否缓存、是否缓存原始图片、是否缓存转换过后的图片等多个选择
- Coil 通过 OkHttp 的网络请求缓存机制来实现磁盘缓存,且磁盘缓存只对通过网络请求加载到的原始图片生效,不缓存其它来源的图片和转换过后的图片
- 网络缓存
- Glide 不存在这个概念
- Coil 相比 Glide 多出了一层网络缓存,可用于实现不进行网络加载,而是强制使用本地缓存(当然,如果本地缓存不存在的话就会报错)
- 线程框架
- Glide 使用原生的 ThreadPoolExecutor 来完成后台任务,通过 Handler 来实现线程切换
- Coil 使用 Coroutines 来完成后台任务及线程切换
我看源码时习惯从最基础的使用方式来入手,分析其整个调用链关系和会涉及到的模块,这里也不例外,就从 Coil 加载一张网络图片来入手
最简单的加载方式只需要调用一个load
方法即可,比 Glide 还简洁,想要添加配置项的话就在 lambda 块中添加
//直接加载图片,不添加任何配置项
imageView.load(imageUrl)
//在 lambda 块中添加配置项
imageView.load(imageUrl) {
crossfade(true) //淡入淡出
placeholder(android.R.drawable.presence_away) //占位图
error(android.R.drawable.stat_notify_error) //图片加载失败时显示的图
transformations(
CircleCropTransformation() //将图片显示为圆形
)
}
Coil 为 ImageView 声明了多个用于加载图片的扩展函数,均命名为 load
,默认情况下我们只需要传一个图片来源地址即可,支持 String、HttpUrl、Uri、File、Int、Drawable、Bitmap 等多种入参类型
/** @see ImageView.loadAny */
@JvmSynthetic
inline fun ImageView.load(
uri: String?,
imageLoader: ImageLoader = context.imageLoader,
builder: ImageRequest.Builder.() -> Unit = {}
): Disposable = loadAny(uri, imageLoader, builder)
不管传入的是什么类型的参数,最终都会中转调用到 loadAny
方法,通过 Builder 模式构建出本次的请求参数 ImageRequest,然后将 ImageRequest 提交给 ImageLoader,由其来完成图片的加载,最终返回一个 Disposable 对象
@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)
}
所以,一个简单的 load
方法就已经使用到了以下几个类:
- ImageRequest。图片的请求参数
- Disposable。用于取消图片加载或者等待图片加载完成
- ImageLoader。向其提交 ImageRequest ,由其完成图片的加载
下面就来分析下这一整个流程
五、ImageRequest
ImageRequest 基于 Builder 模式来构建,包含了加载图片时的各个配置项,其配置项很多,重点看前九个
private val context: Context //外部传入的 Context,例如 ImageView 包含的 Context
private var data: Any? //图片地址
private var target: Target? //图片加载成功后的接收类
private var lifecycle: Lifecycle? //ImageView 关联的生命周期
private var memoryCachePolicy: CachePolicy? //内存缓存配置
private var diskCachePolicy: CachePolicy? //磁盘缓存配置
private var networkCachePolicy: CachePolicy? //网络缓存配置
private var fetcher: Pair<Fetcher<*>, Class<*>>? //完成图片加载的处理器
private var decoder: Decoder? //完成图片转码的转换器
private var defaults: DefaultRequestOptions
private var listener: Listener?
private var memoryCacheKey: MemoryCache.Key?
private var placeholderMemoryCacheKey: MemoryCache.Key?
private var colorSpace: ColorSpace? = null
private var transformations: List<Transformation>
private var headers: Headers.Builder?
private var parameters: Parameters.Builder?
private var sizeResolver: SizeResolver?
private var scale: Scale?
private var dispatcher: CoroutineDispatcher?
private var transition: Transition?
private var precision: Precision?
private var bitmapConfig: Bitmap.Config?
private var allowHardware: Boolean?
private var allowRgb565: Boolean?
@DrawableRes private var placeholderResId: Int?
private var placeholderDrawable: Drawable?
@DrawableRes private var errorResId: Int?
private var errorDrawable: Drawable?
@DrawableRes private var fallbackResId: Int?
private var fallbackDrawable: Drawable?
private var resolvedLifecycle: Lifecycle?
private var resolvedSizeResolver: SizeResolver?
private var resolvedScale: Scale?
1、Target
Target 即最终图片的接收载体,ImageRequest 提供了 target
方法用于把 ImageView 包装为 Target 。如果最终图片的接收载体不是 ImageView 的话,就需要开发者自己来实现 Target 接口
fun target(imageView: ImageView) = target(ImageViewTarget(imageView))
fun target(target: Target?) = apply {
this.target = target
resetResolvedValues()
}
Target 接口提供了图片开始加载、图片加载失败、图片加载成功的事件回调,主要是为了显示占位图、错误图、目标图等几个
interface Target {
/**
* Called when the request starts.
*/
@MainThread
fun onStart(placeholder: Drawable?) {}
/**
* Called if an error occurs while executing the request.
*/
@MainThread
fun one rror(error: Drawable?) {}
/**
* Called if the request completes successfully.
*/
@MainThread
fun onSuccess(result: Drawable) {}
}
ImageViewTarget 就是通过调用 setImageDrawable
来显式各个状态的图片,同时也实现了 DefaultLifecycleObserver 接口,意味着 ImageViewTarget 本身就具备了监听生命周期事件的能力
/** A [Target] that handles setting images on an [ImageView]. */
open class ImageViewTarget(
override val view: ImageView
) : PoolableViewTarget<ImageView>, TransitionTarget, DefaultLifecycleObserver {
private var isStarted = false
override val drawable: Drawable? get() = view.drawable
override fun onStart(placeholder: Drawable?) = setDrawable(placeholder)
override fun one rror(error: Drawable?) = setDrawable(error)
override fun onSuccess(result: Drawable) = setDrawable(result)
/** Replace the [ImageView]'s current drawable with [drawable]. */
protected open fun setDrawable(drawable: Drawable?) {
(view.drawable as? Animatable)?.stop()
view.setImageDrawable(drawable)
updateAnimation()
}
···
}
2、Lifecycle
每个 ImageRequest 都会关联一个 Context 对象,如果外部传入的是 ImageView,则会取 ImageView 内部的 Context。Coil 会判断 Context 是否属于 LifecycleOwner 类型,是的话则可以拿到和 Activity 或者 Fragment 关联的 Lifecycle,否则最终取 GlobalLifecycle
和 Activity 或者 Fragment 关联的 Lifecycle 才具备有生命周期感知能力,这样 Coil 才可以在 Activity 处于后台或者已经销毁的时候暂停任务或者停止任务。而 GlobalLifecycle 会默认且一直处于 RESUMED 状态,这样任务就会一直运行直到最终结束,这可能导致内存泄露
private fun resolveLifecycle(): Lifecycle {
val target = target
val context = if (target is ViewTarget<*>) target.view.context else context
//context 属于 LifecycleOwner 类型则返回对应的 Lifecycle,否则返回 GlobalLifecycle
return context.getLifecycle() ?: GlobalLifecycle
}
internal object GlobalLifecycle : Lifecycle() {
private val owner = LifecycleOwner { this }
override fun addObserver(observer: LifecycleObserver) {
require(observer is DefaultLifecycleObserver) {
"$observer must implement androidx.lifecycle.DefaultLifecycleObserver."
}
// Call the lifecycle methods in order and do not hold a reference to the observer.
observer.onCreate(owner)
observer.onStart(owner)
observer.onResume(owner)
}
override fun removeObserver(observer: LifecycleObserver) {}
override fun getCurrentState() = State.RESUMED
override fun toString() = "coil.request.GlobalLifecycle"
}
3、CachePolicy
和 Glide 一样,Coil 也具备了多级缓存的能力,即内存缓存 memoryCachePolicy、磁盘缓存 diskCachePolicy、网络缓存 networkCachePolicy。这些缓存功能是否开启都是通过 CachePolicy 来定义,默认三级缓存全部可读可写
enum class CachePolicy(
val readEnabled: Boolean,
val writeEnabled: Boolean
) {
ENABLED(true, true), //可读可写
READ_ONLY(true, false), //只读
WRITE_ONLY(false, true), //只写
DISABLED(false, false) //不可读不可写,即禁用
}
4、Fetcher
Fetcher 是根据图片来源地址转换为目标数据类型的转换器。例如,我们传入了 Int 类型的 drawableResId,想要以此拿到 Drawable,那么这里的 Class<*>
即 Class<Int>
,Fetcher<*>
即 Fetcher<Drawable>
/** @see Builder.fetcher */
val fetcher: Pair<Fetcher<*>, Class<*>>?,
Fetcher 接口包含三个方法
interface Fetcher<T : Any> {
/**
* 如果能处理 data 则返回 true
*/
fun handles(data: T): Boolean = true
/**
* 根据 data 来计算用于内存缓存时的唯一 key
* 具有相同 key 的缓存将被 MemoryCache 视为相同的数据
* 如果返回 null 则不会将 fetch 后的数据缓存到内存中
*/
fun key(data: T): String?
/**
* 根据 data 将目标图片加载到内存中
*/
suspend fun fetch(
pool: BitmapPool,
data: T,
size: Size,
options: Options
): FetchResult
}
Coil 默认提供了以下八种类型的 Fetcher,分别用于处理 HttpUriUri、HttpUriUrl、File、Asset、ContentUri、Resource、Drawable、Bitmap 等类型的图片来源地址
private val registry = componentRegistry.newBuilder()
···
// Fetchers
.add(HttpUriFetcher(callFactory))
.add(HttpUrlFetcher(callFactory))
.add(FileFetcher(addLastModifiedToFileCacheKey))
.add(AssetUriFetcher(context))
.add(ContentUriFetcher(context))
.add(ResourceUriFetcher(context, drawableDecoder))
.add(DrawableFetcher(drawableDecoder))
.add(BitmapFetcher())
···
.build()
5、Decoder
Decoder 接口用于提供将 BufferedSource 转码为 Drawable 的能力,BufferedSource 就对应着不同类型的图片资源
Coil 提供了以下几个 Decoder 实现类
- BitmapFactoryDecoder。用于实现 Bitmap 转码
- GifDecoder、ImageDecoderDecoder。用于实现 Gif、Animated WebPs、Animated HEIFs 转码
- SvgDecoder。用于实现 Svg 转码
interface Decoder {
//如果此 Decoder 能够处理 source 则返回 true
fun handles(source: BufferedSource, mimeType: String?): Boolean
//用于将 source 解码为 Drawable
suspend fun decode(
pool: BitmapPool,
source: BufferedSource,
size: Size,
options: Options
): DecodeResult
}
六、Disposable
Disposable 是我们调用 load
方法后的返回值,为外部提供用于取消图片加载或者等待图片加载完成的方法
interface Disposable {
//如果任务已经完成或者取消的话,则返回 true
val isDisposed: Boolean
//取消正在进行的任务并释放与此任务关联的所有资源
fun dispose()
//非阻塞式地等待任务结束
@ExperimentalCoilApi
suspend fun await()
}
由于 Coil 是使用协程来加载图片,所以每个任务都会对应一个 Job
如果 ImageRequest 包含的 Target 对应着某个 View(即属于 ViewTarget 类型),那么返回的 Disposable 即 ViewTargetDisposable。而 View 可能需要先后请求多张图片(例如 RecyclerView 的每个 Item 都是 ImageView),那么当启动新任务后旧任务就应该被取消,所以 ViewTargetDisposable 就包含了一个 UUID 来唯一标识每个请求。其它情况就都是返回 BaseTargetDisposable
internal class BaseTargetDisposable(private val job: Job) : Disposable {
override val isDisposed
get() = !job.isActive
override fun dispose() {
if (isDisposed) return
job.cancel()
}
@ExperimentalCoilApi
override suspend fun await() {
if (isDisposed) return
job.join()
}
}
internal class ViewTargetDisposable(
private val requestId: UUID,
private val target: ViewTarget<*>
) : Disposable {
override val isDisposed
get() = target.view.requestManager.currentRequestId != requestId
override fun dispose() {
if (isDisposed) return
target.view.requestManager.clearCurrentRequest()
}
@ExperimentalCoilApi
override suspend fun await() {
if (isDisposed) return
target.view.requestManager.currentRequestJob?.join()
}
}
七、ImageLoader
上面有说过,loadAny
方法最终是会通过调用 imageLoader.enqueue(request)
来发起一个图片加载请求的,那么重点就是要来看 ImageLoader 是如何实现的
@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)
}
ImageLoader 是一个接口,是承载了所有图片加载任务和实现缓存复用的加载器
interface ImageLoader {
//用于提供 ImageRequest 的默认配置项
val defaults: DefaultRequestOptions
//内存缓存
val memoryCache: MemoryCache
//Bitmap缓存池
val bitmapPool: BitmapPool
//异步加载图片
fun enqueue(request: ImageRequest): Disposable
//同步加载图片
suspend fun execute(request: ImageRequest): ImageResult
//停止全部任务
fun shutdown()
}
ImageLoader 的唯一实现类是 RealImageLoader,其enqueue
方法会启动一个协程,在 job 里执行 executeMain
方法得到 ImageResult,ImageResult 就包含了最终得到的图片。同时,job 会被包含在返回的 Disposable 对象里,这样外部才能取消图片加载或者等待图片加载完成
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)
}
}
executeMain 方法的逻辑也比较简单,可以概括为:
- 为 target 和 request 创建一个代理类,用于支持 Bitmap 缓存和 Lifecycle 监听
- 如果外部发起的是异步请求的话(即 REQUEST_TYPE_ENQUEUE),那么就需要等到 Lifecycle 至少处于 Started 状态之后才能继续执行,这样当 Activity 还处于后台时就不会发起请求了
- 获取占位图并传给 target
- 获取 target 需要的图片尺寸大小,以便按需加载,对于 ImageViewTarget 来说,即获取 ImageView 的宽高属性
- 调用 executeChain 方法拿到 ImageResult,判断是否成功,调用 target 对应的成功或者失败的方法
@MainThread
private suspend fun executeMain(initialRequest: ImageRequest, type: Int): ImageResult {
···
// Apply this image loader's defaults to this request.
val request = initialRequest.newBuilder().defaults(defaults).build()
//target 代理,用于支持Bitmap池
val targetDelegate = delegateService.createTargetDelegate(request.target, type, eventListener)
//request 代理,用于支持 lifecycle
val requestDelegate = delegateService.createRequestDelegate(request, targetDelegate, coroutineContext.job)
try {
//如果 data 为 null,那么就抛出异常
if (request.data == NullRequestData) throw NullRequestDataException()
//如果是异步请求的话,那么就需要等到 Lifecycle 至少处于 Started 状态之后才能继续执行
if (type == REQUEST_TYPE_ENQUEUE) request.lifecycle.awaitStarted()
//获取展位图传给 target,从内存缓存中加载或者从全新加载
val cached = memoryCacheService[request.placeholderMemoryCacheKey]?.bitmap
try {
targetDelegate.metadata = null
targetDelegate.start(cached?.toDrawable(request.context) ?: request.placeholder, cached)
eventListener.onStart(request)
request.listener?.onStart(request)
} finally {
referenceCounter.decrement(cached)
}
//获取 target 需要的图片尺寸大小,按需加载
eventListener.resolveSizeStart(request)
val size = request.sizeResolver.size()
eventListener.resolveSizeEnd(request, size)
// Execute the interceptor chain.
val result = executeChain(request, type, size, cached, eventListener)
// Set the result on the target.
//判断 result 成功与否,调用相应的方法
when (result) {
is SuccessResult -> onSuccess(result, targetDelegate, eventListener)
is ErrorResult -> one rror(result, targetDelegate, eventListener)
}
return result
} catch (throwable: Throwable) {
if (throwable is CancellationException) {
onCancel(request, eventListener)
throw throwable
} else {
// Create the default error result if there's an uncaught exception.
val result = requestService.errorResult(request, throwable)
one rror(result, targetDelegate, eventListener)
return result
}
} finally {
requestDelegate.complete()
}
}
executeChain
方法就比较有意思了,有看过 OkHttp 源码的同学应该会对 RealInterceptorChain 有点印象,OkHttp 的拦截器就是通过该同名类来实现的,很显然 Coil 借鉴了 OkHttp 的实现思路,极大方便了后续功能扩展,也给了外部控制整个图片加载流程的入口,可扩展性 +100
private val interceptors = registry.interceptors + EngineInterceptor(registry, bitmapPool, referenceCounter,
strongMemoryCache, memoryCacheService, requestService, systemCallbacks, drawableDecoder, logger)
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 (launchInterceptorChainOnMainThread) {
chain.proceed(request)
} else {
withContext(request.dispatcher) {
chain.proceed(request)
}
}
}
所以说,重点就还是要来看 EngineInterceptor 的 intercept
方法,其逻辑可以概括为:
- 找到能处理本次请求的 fetcher,执行下一步
- 计算本次要加载的图片在内存中的缓存 key,如果内存缓存可用的话就直接使用缓存,结束流程
- 如果存在内存缓存但是不可用(可能是由于硬件加速配置不符或者是本次不允许使用缓存),那么就更新该缓存在内存中的可用状态并更新引用计数,执行下一步
- 调用 execute 方法完成图片加载,得到 drawable,结束流程
execute
方法的逻辑可以概括为:
- 通过 fetcher 来执行磁盘加载或者网络请求,得到 fetchResult,执行下一步
- 如果 fetchResult 属于 DrawableResult 的话,那么就已经拿到目标图片类型 Drawable 了,那么直接返回,结束流程
- 如果 fetchResult 属于 SourceResult 类型,即拿到的数据类型是 BufferedSource,此时还需要转码为 Drawable,执行下一步
- 先判断本次请求是否属于预加载,即可能外部现在不需要使用到该图片,只是想先将图片缓存到本地磁盘,方便后续能够快速加载。预加载的判断标准就是:异步请求 + target 为null + 不允许缓存到内存中。属于预加载的话就不需要将加载到的图片进行转码了,此时就使用 EmptyDecoder,否则就还是需要去找能进行实际转码的 Decoder。拿到 Decoder 后就执行下一步
- 通过 Decoder 完成图片转码,得到 Drawable,结束流程
/** The last interceptor in the chain which executes the [ImageRequest]. */
internal class EngineInterceptor(
private val registry: ComponentRegistry,
private val bitmapPool: BitmapPool,
private val referenceCounter: BitmapReferenceCounter,
private val strongMemoryCache: StrongMemoryCache,
private val memoryCacheService: MemoryCacheService,
private val requestService: RequestService,
private val systemCallbacks: SystemCallbacks,
private val drawableDecoder: DrawableDecoderService,
private val logger: Logger?
) : Interceptor {
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
try {
// This interceptor uses some internal APIs.
check(chain is RealInterceptorChain)
val request = chain.request
val context = request.context
val data = request.data
val size = chain.size
val eventListener = chain.eventListener
// Perform any data mapping.
eventListener.mapStart(request, data)
val mappedData = registry.mapData(data)
eventListener.mapEnd(request, mappedData)
//找到能处理本次请求的 fetcher
val fetcher = request.fetcher(mappedData) ?: registry.requireFetcher(mappedData)
//计算本次要加载的图片在内存中的缓存 key
val memoryCacheKey = request.memoryCacheKey ?: computeMemoryCacheKey(request, mappedData, fetcher, size)
//如果本次允许使用内存缓存的话,那么就尝试从 memoryCacheService 中获取缓存
val value = if (request.memoryCachePolicy.readEnabled) memoryCacheService[memoryCacheKey] else null
// Ignore the cached bitmap if it is hardware-backed and the request disallows hardware bitmaps.
val cachedDrawable = value?.bitmap
?.takeIf { requestService.isConfigValidForHardware(request, it.safeConfig) }
?.toDrawable(context)
//如果缓存可用,则直接返回缓存
if (cachedDrawable != 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
)
)
}
// Fetch, decode, transform, and cache the image on a background dispatcher.
return withContext(request.dispatcher) {
//如果 request.data 属于 BitmapDrawable 或者 Bitmap 类型
//会执行到这里说明 data 不符合本次的使用条件,那么就在内存中将其标记为不可用状态
invalidateData(request.data)
//存在缓存但是没用上,引用计数减一
if (value != null) referenceCounter.decrement(value.bitmap)
// Fetch and decode the image.
val (drawable, isSampled, dataSource) =
execute(mappedData, fetcher, request, chain.requestType, size, eventListener)
// Mark the drawable's bitmap as eligible for pooling.
validateDrawable(drawable)
//尝试将获取到的 bitmap 缓存到内存中
val isCached = writeToMemoryCache(request, memoryCacheKey, drawable, isSampled)
// 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)
}
}
}
/** Load the [data] as a [Drawable]. Apply any [Transformation]s. */
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)
eventListener.fetchStart(request, fetcher, options)
val fetchResult = fetcher.fetch(bitmapPool, data, size, options)
eventListener.fetchEnd(request, fetcher, options, fetchResult)
val baseResult = when (fetchResult) {
is SourceResult -> {
val decodeResult = try {
// Check if we're cancelled.
coroutineContext.ensureActive()
//判断本次请求是否属于预加载,即可能外部只是想先将图片加载到本地磁盘,方便后续使用
//预加载的判断标准就是:异步请求 + target为null + 不缓存到内存中
//属于预加载的话就不需要将加载到的图片进行转码了,就会使用 EmptyDecoder
//否则就还是需要去找能进行转码的 Decoder
val isDiskOnlyPreload = type == REQUEST_TYPE_ENQUEUE &&
request.target == null &&
!request.memoryCachePolicy.writeEnabled
val decoder = if (isDiskOnlyPreload) {
// Skip decoding the result if we are preloading the data and writing to the memory cache is
// disabled. Instead, we exhaust the source and return an empty result.
EmptyDecoder
} else {
request.decoder ?: registry.requireDecoder(request.data, fetchResult.source, fetchResult.mimeType)
}
// Decode the stream.
eventListener.decodeStart(request, decoder, options)
//进行转码,得到目标类型 Drawable
val decodeResult = decoder.decode(bitmapPool, fetchResult.source, size, options)
eventListener.decodeEnd(request, decoder, options, decodeResult)
decodeResult
} catch (throwable: Throwable) {
// Only close the stream automatically if there is an uncaught exception.
// This allows custom decoders to continue to read the source after returning a drawable.
fetchResult.source.closeQuietly()
throw throwable
}
// Combine the fetch and decode operations' results.
DrawableResult(
drawable = decodeResult.drawable,
isSampled = decodeResult.isSampled,
dataSource = fetchResult.dataSource
)
}
is DrawableResult -> fetchResult
}
// Check if we're cancelled.
coroutineContext.ensureActive()
// Apply any transformations and prepare to draw.
val finalResult = applyTransformations(baseResult, request, size, options, eventListener)
(finalResult.drawable as? BitmapDrawable)?.bitmap?.prepareToDraw()
return finalResult
}
}
Fetcher 是根据图片来源地址转换为目标数据类型的转换器。Coil 默认提供了以下八种类型的 Fetcher,分别用于处理 HttpUri、HttpUrl、File、Asset、ContentUri、Resource、Drawable、Bitmap 等类型的图片来源地址
private val registry = componentRegistry.newBuilder()
···
// Fetchers
.add(HttpUriFetcher(callFactory))
.add(HttpUrlFetcher(callFactory))
.add(FileFetcher(addLastModifiedToFileCacheKey))
.add(AssetUriFetcher(context))
.add(ContentUriFetcher(context))
.add(ResourceUriFetcher(context, drawableDecoder))
.add(DrawableFetcher(drawableDecoder))
.add(BitmapFetcher())
···
.build()
所以,如果我们外部要加载的是一张网络图片,且传入的是 String 类型的 ImageUrl,那么最终对应上的就是 HttpUriFetcher,其父类 HttpFetcher 就会通过 OkHttp 来进行网络请求了。至此,整个图片加载流程就结束了
internal class HttpUriFetcher(callFactory: Call.Factory) : HttpFetcher<Uri>(callFactory) {
override fun handles(data: Uri) = data.scheme == "http" || data.scheme == "https"
override fun key(data: Uri) = data.toString()
override fun Uri.toHttpUrl(): HttpUrl = HttpUrl.get(toString())
}
internal abstract class HttpFetcher<T : Any>(private val callFactory: Call.Factory) : Fetcher<T> {
/**
* Perform this conversion in a [Fetcher] instead of a [Mapper] so
* [HttpUriFetcher] can execute [HttpUrl.get] on a background thread.
*/
abstract fun T.toHttpUrl(): HttpUrl
override suspend fun fetch(
pool: BitmapPool,
data: T,
size: Size,
options: Options
): FetchResult {
val url = data.toHttpUrl()
val request = Request.Builder().url(url).headers(options.headers)
val networkRead = options.networkCachePolicy.readEnabled
val diskRead = options.diskCachePolicy.readEnabled
when {
!networkRead && diskRead -> {
request.cacheControl(CacheControl.FORCE_CACHE)
}
networkRead && !diskRead -> if (options.diskCachePolicy.writeEnabled) {
request.cacheControl(CacheControl.FORCE_NETWORK)
} else {
request.cacheControl(CACHE_CONTROL_FORCE_NETWORK_NO_CACHE)
}
!networkRead && !diskRead -> {
// This causes the request to fail with a 504 Unsatisfiable Request.
request.cacheControl(CACHE_CONTROL_NO_NETWORK_NO_CACHE)
}
}
val response = callFactory.newCall(request.build()).await()
if (!response.isSuccessful) {
response.body()?.close()
throw HttpException(response)
}
val body = checkNotNull(response.body()) { "Null response body!" }
return SourceResult(
source = body.source(),
mimeType = getMimeType(url, body),
dataSource = if (response.cacheResponse() != null) DataSource.DISK else DataSource.NETWORK
)
}
}
八、缓存机制
Glide 的缓存机制是分为内存缓存和磁盘缓存两层,Coil 在这两个的基础上还增加了网络缓存这一层,这可以从 ImageRequest 的参数看出来,默认情况下,这三层缓存机制是全部启用的,即全部可读可写
//内存缓存
val memoryCachePolicy: CachePolicy,
//磁盘缓存
val diskCachePolicy: CachePolicy,
//网络缓存
val networkCachePolicy: CachePolicy,
enum class CachePolicy(
val readEnabled: Boolean,
val writeEnabled: Boolean
) {
ENABLED(true, true),
READ_ONLY(true, false),
WRITE_ONLY(false, true),
DISABLED(false, false)
}
在请求图片的时候,我们可以在 lambda 块中配置本次请求的缓存策略
imageView.load(imageUrl) {
memoryCachePolicy(CachePolicy.ENABLED)
diskCachePolicy(CachePolicy.ENABLED)
networkCachePolicy(CachePolicy.ENABLED)
}
下面来看看 Coil 的缓存机制具体是如何定义和实现的
1、内存缓存
Coil 的内存缓存机制集中在 EngineInterceptor 中生效,有两个时机会来判断是否可以写入和读取内存缓存
- 如果本次请求允许从内存中读取缓存的话,即
request.memoryCachePolicy.readEnabled
为 true,那么就尝试从 memoryCacheService 读取缓存 - 如果本次请求允许将图片缓存到内存的话,即
request.memoryCachePolicy.writeEnabled
为 true,那么就将图片存到 strongMemoryCache 中
internal class EngineInterceptor(
private val registry: ComponentRegistry,
private val bitmapPool: BitmapPool,
private val referenceCounter: BitmapReferenceCounter,
private val strongMemoryCache: StrongMemoryCache,
private val memoryCacheService: MemoryCacheService,
private val requestService: RequestService,
private val systemCallbacks: SystemCallbacks,
private val drawableDecoder: DrawableDecoderService,
private val logger: Logger?
) : Interceptor {
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
try {
val request = chain.request
···
//如果本次允许使用内存缓存的话,那么就尝试从 memoryCacheService 中获取缓存
val value = if (request.memoryCachePolicy.readEnabled) memoryCacheService[memoryCacheKey] else null
···
return withContext(request.dispatcher) {
···
//尝试将获取到的 bitmap 缓存到 strongMemoryCache 中
val isCached = writeToMemoryCache(request, memoryCacheKey, drawable, isSampled)
···
}
} catch (throwable: Throwable) {
···
}
}
private fun writeToMemoryCache(
request: ImageRequest,
key: MemoryCache.Key?,
drawable: Drawable,
isSampled: Boolean
): Boolean {
if (!request.memoryCachePolicy.writeEnabled) {
return false
}
if (key != null) {
val bitmap = (drawable as? BitmapDrawable)?.bitmap
if (bitmap != null) {
strongMemoryCache.set(key, bitmap, isSampled)
return true
}
}
return false
}
}
MemoryCacheService 相当于一个工具类,会先后尝试从 StrongMemoryCache 和 WeakMemoryCache 取值,取得到的话会同时通过 BitmapReferenceCounter 将其引用计数 +1
internal class MemoryCacheService(
private val referenceCounter: BitmapReferenceCounter,
private val strongMemoryCache: StrongMemoryCache,
private val weakMemoryCache: WeakMemoryCache
) {
operator fun get(key: MemoryCache.Key?): RealMemoryCache.Value? {
key ?: return null
val value = strongMemoryCache.get(key) ?: weakMemoryCache.get(key)
if (value != null) referenceCounter.increment(value.bitmap)
return value
}
}
Coil 的内存缓存机制实际上是分为两级:
- WeakMemoryCache
- StrongMemoryCache
在默认情况下,Coil 的这两级内存缓存都是开启的,这两者的关系是:
- RealWeakMemoryCache。通过弱引用来保存曾经加载到内存中的 Bitmap
- RealBitmapPool。Bitmap 缓存池,用于在内存中缓存当前不再被使用的 Bitmap,可用于后续复用
- RealBitmapReferenceCounter。RealBitmapReferenceCounter 也通过弱引用来保存 Bitmap,用于对当前处于使用状态的 Bitmap 进行引用标记,计算每个 Bitmap 当前的引用次数及可用状态。例如,当 EngineInterceptor 在 StrongMemoryCache 中找到了可以复用的 Bitmap 后,该 Bitmap 的引用计数就会 +1。当 StrongMemoryCache 由于容量限制需要移除某个 Bitmap 时,该 Bitmap 的引用计数就会 -1。当 Bitmap 的引用次数变为 0 且处于不可用状态时,就会将其从 RealWeakMemoryCache 中移除并存到 BitmapPool 中
- RealStrongMemoryCache。RealStrongMemoryCache 通过最近最少使用算法 LruCache 来缓存 Bitmap,并且是通过强引用的方式来保存。当 EngineInterceptor 加载到一个 Bitmap 后,就会将其存到 RealStrongMemoryCache 的 LruCache 中,并同时将 RealBitmapReferenceCounter 的引用计数 +1,在移除元素时也会相应减少引用计数
这两级缓存的设计初衷是什么呢?或者说,将内存缓存设计为这两层是因为什么呢?
我们都知道,弱引用是不会阻止内存回收的,一个对象如果只具备弱引用,那么在 GC 过后该对象就会被回收,所以 RealWeakMemoryCache 的存在不会导致 Bitmap 被泄漏。而 RealStrongMemoryCache 是通过强引用和 LruCache 来存储 Bitmap 的,由于 LruCache 具有固定容量,那么就存在由于容量不足导致用户当前正在使用的 Bitmap 被移出 LruCache 的可能,如果之后又需要加载同一个 Bitmap 的话,就还可以通过 RealWeakMemoryCache 来取值,尽量复用已经加载在内存中的 Bitmap。所以说,RealStrongMemoryCache 和 RealWeakMemoryCache 的存在意义都是为了尽量复用 Bitmap
此外,BitmapPool 的存在意义是为了尽量避免频繁创建 Bitmap。在使用 Transformation 的时候需要用到 Bitmap 来作为载体,如果频繁创建 Bitmap 可能会造成内存抖动,所以即使当一个 Bitmap 不再被使用,也会将之存到 RealBitmapPool 中缓存起来,方便后续复用。RealBitmapReferenceCounter 会保存 Bitmap 的引用次数和可用状态,当引用次数小于等于 0 且处于不可用状态时,就会将其从 RealWeakMemoryCache 中移除并存到 BitmapPool 中
2、磁盘缓存、网络缓存
Coil 的磁盘缓存和网络缓存可以合在一起讲,因为 Coil 的磁盘缓存其实是通过 OkHttp 本身的网络缓存功能来间接实现的。RealImageLoader 在初始化的时候,默认构建了一个包含 cache 的 OkHttpClient,即默认支持缓存网络请求结果
private fun buildDefaultCallFactory() = lazyCallFactory {
OkHttpClient.Builder()
.cache(CoilUtils.createDefaultCache(applicationContext))
.build()
}
而且,Coil 的磁盘缓存和网络缓存这两个配置也只会在 HttpFetcher 这里读取,即只在进行网络请求的时候生效,所以说,Coil 只会磁盘缓存通过网络请求得到的原始图片,而不缓存其它尺寸大小的图片
HttpFetcher 的网络缓存和磁盘缓存策略是通过修改 Request 的 cacheControl 来实现的,每种缓存策略可以分别配置是否可读可写,一共有以下几种可能:
- 不允许网络请求,允许磁盘读缓存。那么就强制使用本地缓存,如果本地缓存不存在的话就报错,加载失败
- 允许网络请求,不允许磁盘读缓存
- 允许磁盘写缓存。那么就强制去网络请求,且将请求结果缓存到本地磁盘
- 不允许磁盘写缓存。那么就强制去网络请求,且不将请求结果缓存到本地磁盘
- 不允许网络请求,不允许磁盘读缓存。这会导致请求失败,Http 报 504 错误,加载失败
- 允许网络请求,也允许磁盘读缓存和磁盘写缓存。那么就会优先使用本地缓存,本地缓存不存在的话再去网络请求,并将网络请求结果缓存到本地磁盘
internal abstract class HttpFetcher<T : Any>(private val callFactory: Call.Factory) : Fetcher<T> {
/**
* Perform this conversion in a [Fetcher] instead of a [Mapper] so
* [HttpUriFetcher] can execute [HttpUrl.get] on a background thread.
*/
abstract fun T.toHttpUrl(): HttpUrl
override suspend fun fetch(
pool: BitmapPool,
data: T,
size: Size,
options: Options
): FetchResult {
val url = data.toHttpUrl()
val request = Request.Builder().url(url).headers(options.headers)
val networkRead = options.networkCachePolicy.readEnabled
val diskRead = options.diskCachePolicy.readEnabled
when {
//1、不允许网络请求,允许磁盘读缓存
//那么就强制使用本地缓存,如果不存在本地缓存的话就报错
!networkRead && diskRead -> {
request.cacheControl(CacheControl.FORCE_CACHE)
}
//2、允许网络请求,不允许磁盘读缓存
networkRead && !diskRead ->
if (options.diskCachePolicy.writeEnabled) {
//2.1、允许磁盘写缓存
//那么就强制去网络请求,且将请求结果缓存到本地磁盘
request.cacheControl(CacheControl.FORCE_NETWORK)
} else {
//2.2、不允许磁盘写缓存
//那么就强制去网络请求,且不将请求结果缓存到本地磁盘
request.cacheControl(CACHE_CONTROL_FORCE_NETWORK_NO_CACHE)
}
!networkRead && !diskRead -> {
//3、不允许网络请求,不允许磁盘读缓存
//这会导致请求失败,就会导致请求失败,报 504 错误
request.cacheControl(CACHE_CONTROL_NO_NETWORK_NO_CACHE)
}
}
val response = callFactory.newCall(request.build()).await()
if (!response.isSuccessful) {
response.body()?.close()
throw HttpException(response)
}
val body = checkNotNull(response.body()) { "Null response body!" }
return SourceResult(
source = body.source(),
mimeType = getMimeType(url, body),
dataSource = if (response.cacheResponse() != null) DataSource.DISK else DataSource.NETWORK
)
}
···
}
从以上逻辑也可以看出,networkCachePolicy 的 writeEnabled 属性并没有被用到,因为网络请求本身只有发起和不发起两种选择,用 readEnabled 就足够表示了,所以 writeEnabled 对于 networkCachePolicy 来说没有意义
此外,为了在无网络信号的时候可以快速结束整个流程,避免无意义的网络请求,RequestService 会在当前处于离线的时候(即 isOnline 为 false),将 networkCachePolicy 修改为完全禁用状态(CachePolicy.DISABLED)
internal class RequestService(private val logger: Logger?) {
@WorkerThread
fun options(
request: ImageRequest,
size: Size,
isOnline: Boolean
): Options {
···
// Disable fetching from the network if we know we're offline.
val networkCachePolicy = if (isOnline) request.networkCachePolicy else CachePolicy.DISABLED
···
return Options(
context = request.context,
config = config,
colorSpace = request.colorSpace,
scale = request.scale,
allowInexactSize = request.allowInexactSize,
allowRgb565 = allowRgb565,
premultipliedAlpha = request.premultipliedAlpha,
headers = request.headers,
parameters = request.parameters,
memoryCachePolicy = request.memoryCachePolicy,
diskCachePolicy = request.diskCachePolicy,
networkCachePolicy = networkCachePolicy
)
}
}
九、生命周期监听
前文有提到,每个 ImageRequest 都会关联一个 Context 对象,如果外部传入的是 ImageView,则会自动取 ImageView 内部的 Context。Coil 会判断 Context 是否属于 LifecycleOwner 类型,是的话则可以拿到和 Activity 或者 Fragment 关联的 Lifecycle,否则最终取 GlobalLifecycle
和 Activity 或者 Fragment 关联的 Lifecycle 才具备有生命周期感知能力,这样 Coil 才可以在 Activity 处于后台或者已经销毁的时候暂停或者停止任务。而 GlobalLifecycle 会默认且一直会处于 RESUMED 状态,这样任务就会一直运行直到最终结束,这可能导致内存泄露
那么,该 Lifecycle 对象具体是在什么地方起了作用呢?
这个主要看 RealImageLoader 的 executeMain
方法。在发起图片加载请求前,后先创建 request 的代理对象 requestDelegate,requestDelegate 中就包含了对 Lifecycle 的处理逻辑。此外,如果是异步请求的话,会等到 Lifecycle 至少处于 Started 状态之后才能发起请求,这样当 Activity 还处于后台时就不会发起请求了
@MainThread
private suspend fun executeMain(initialRequest: ImageRequest, type: Int): ImageResult {
···
//创建 request 的代理对象
val requestDelegate = delegateService.createRequestDelegate(request, targetDelegate, coroutineContext.job)
try {
···
//如果是异步请求的话,那么就需要等到 Lifecycle 至少处于 Started 状态之后才能继续执行
if (type == REQUEST_TYPE_ENQUEUE) request.lifecycle.awaitStarted()
···
return result
} catch (throwable: Throwable) {
if (throwable is CancellationException) {
onCancel(request, eventListener)
throw throwable
} else {
// Create the default error result if there's an uncaught exception.
val result = requestService.errorResult(request, throwable)
one rror(result, targetDelegate, eventListener)
return result
}
} finally {
requestDelegate.complete()
}
}
createRequestDelegate
方法的逻辑可以总结为:
- 如果 target 对象属于 ViewTarget 类型,那么说明其包含特定 View
- 将请求请求参数包装为 ViewTargetRequestDelegate 类型,而 ViewTargetRequestDelegate 实现了 DefaultLifecycleObserver 接口,其会在收到 onDestroy 事件的时候主动取消 Job 并清理各类资源。所以向 Lifecycle 添加该 Observer 就可以保证在 Activity 销毁后也能同时取消图片加载请求,避免内存泄漏
- 如果 target 属于 LifecycleObserver 类型的话,则也向 Lifecycle 添加该 Observer 。ImageViewTarget 就实现了 DefaultLifecycleObserver 接口,这主要是为了判断 ImageView 对应的 Activity 或者 Fragment 是否处于前台,如果处于前台且存在 Animatable 的话就会自动启动动画,否则就自动停止动画。之所以需要先 removeObserver 再 addObserver,是因为 target 可能需要先后请求多张图片,我们不能重复向 Lifecycle 添加同一个 Observer 对象
- 同时,如果 View 已经 Detached 了的话,那么就需要主动取消请求
- 如果 target 对象不属于 ViewTarget 类型的话,创建的代理对象是 BaseRequestDelegate 类型,也会在收到 onDestroy 事件的时候主动取消 Job
/** Wrap [request] to automatically dispose (and for [ViewTarget]s restart) the [ImageRequest] based on its lifecycle. */
@MainThread
fun createRequestDelegate(
request: ImageRequest,
targetDelegate: TargetDelegate,
job: Job
): RequestDelegate {
val lifecycle = request.lifecycle
val delegate: RequestDelegate
when (val target = request.target) {
//对应第1点
is ViewTarget<*> -> {
//对应第1.1点
delegate = ViewTargetRequestDelegate(imageLoader, request, targetDelegate, job)
lifecycle.addObserver(delegate)
//对应第1.2点
if (target is LifecycleObserver) {
lifecycle.removeObserver(target)
lifecycle.addObserver(target)
}
target.view.requestManager.setCurrentRequest(delegate)
//对应第1.3点
// Call onViewDetachedFromWindow immediately if the view is already detached.
if (!target.view.isAttachedToWindowCompat) {
target.view.requestManager.onViewDetachedFromWindow(target.view)
}
}
//对应第2点
else -> {
delegate = BaseRequestDelegate(lifecycle, job)
lifecycle.addObserver(delegate)
}
}
return delegate
}
十、Transformation
图片变换是基本所有的图片加载库都会支持的功能,Coil 对这个概念的抽象即 Transformation 接口
注意,key()
方法的返回值是用于计算图片在内存缓存中的唯一 Key 时的辅助参数,所以需要实现该方法,为 Transformation 生成一个可以唯一标识自身的字符串 Key。transform
方法包含了一个 BitmapPool 参数,我们在实现图形变换的时候往往是需要一个全新的 Bitmap,此时就应该通过 BitmapPool 来获取,尽量复用已有的 Bitmap
interface Transformation {
/**
* Return a unique key for this transformation.
*
* The key should contain any params that are part of this transformation (e.g. size, scale, color, radius, etc.).
*/
fun key(): String
/**
* Apply the transformation to [input].
*
* @param pool A [BitmapPool] which can be used to request [Bitmap] instances.
* @param input The input [Bitmap] to transform. Its config will always be [Bitmap.Config.ARGB_8888] or [Bitmap.Config.RGBA_F16].
* @param size The size of the image request.
*/
suspend fun transform(pool: BitmapPool, input: Bitmap, size: Size): Bitmap
}
Coil 默认提供了以下几个 Transformation 实现类
- BlurTransformation。用于实现高斯模糊
- CircleCropTransformation。用于将图片转换为圆形
- GrayscaleTransformation。用户实现将图片转换为灰色
- RoundedCornersTransformation。用于为图片添加圆角
十一、实现全局默认配置
如果我们想要设置应用内所有图片在加载时固定显示同一张 loading 图,在加载失败时固定显示一张 error 图, 那么就需要为 Coil 设定一个全局的默认配置。Glide 是通过 AppGlideModule 来实现的,那 Coil 是如何来实现这个效果呢?
Coil 默认会在我们第一次触发图片加载的时候来初始化 RealImageLoader 的单例对象,而 RealImageLoader 的构造参数就包含了一个 DefaultRequestOptions 用于设置默认配置,所以我们可以通过自定义 RealImageLoader 的初始化逻辑来控制全局的默认请求配置
internal class RealImageLoader(
context: Context,
override val defaults: DefaultRequestOptions,
override val bitmapPool: BitmapPool,
private val referenceCounter: BitmapReferenceCounter,
private val strongMemoryCache: StrongMemoryCache,
private val weakMemoryCache: WeakMemoryCache,
callFactory: Call.Factory,
private val eventListenerFactory: EventListener.Factory,
componentRegistry: ComponentRegistry,
addLastModifiedToFileCacheKey: Boolean,
private val launchInterceptorChainOnMainThread: Boolean,
val logger: Logger?
)
RealImageLoader 的单例对象就保存在另一个单例对象 Coil 中,Coil 以两种方式来完成 RealImageLoader 的初始化
- 如果项目中的 Application 继承了 ImageLoaderFactory 接口,那么就通过该接口来完成初始化
- 通过
ImageLoader(context)
来完成默认初始化
/**
* A class that holds the singleton [ImageLoader] instance.
*/
object Coil {
private var imageLoader: ImageLoader? = null
private var imageLoaderFactory: ImageLoaderFactory? = null
/**
* Get the singleton [ImageLoader]. Creates a new instance if none has been set.
*/
@JvmStatic
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
}
}
为了设定默认配置,我们就需要在应用启动之后,开始图片加载之前向 Coil 注入自己的 ImageLoader 实例
/**
* @Author: leavesC
* @Date: 2020/11/22 13:06
* @GitHub:https://github.com/leavesC
* @Desc:
*/
object CoilHolder {
fun init(application: Application) {
Coil.setImageLoader(
ImageLoader.Builder(application)
.placeholder(ActivityCompat.getDrawable(application, R.drawable.icon_loading)) //占位符
.error(ActivityCompat.getDrawable(application, R.drawable.icon_error)) //错误图
.memoryCachePolicy(CachePolicy.ENABLED) //开启内存缓存
.callFactory(createOkHttp(application)) //主动构造 OkHttpClient 实例
.build()
)
}
private fun createOkHttp(application: Application): OkHttpClient {
return OkHttpClient.Builder()
.cache(createDefaultCache(application))
.build()
}
private fun createDefaultCache(context: Context): Cache {
val cacheDirectory = getDefaultCacheDirectory(context)
return Cache(cacheDirectory, 10 * 1024 * 1024)
}
private fun getDefaultCacheDirectory(context: Context): File {
return File(context.cacheDir, "image_cache").apply { mkdirs() }
}
}
献给读者
随着互联网企业的不断发展,产品项目中的模块越来越多,用户体验要求也越来越高,想实现小步快跑、快速迭代的目的越来越难,还有65535,应用之间的互相调用等等问题,插件化技术应用而生。如果没有插件化技术,美团、淘宝这些集成了大量“app”的应用,可能会有几个g那么大。
所以,当今的Android移动开发,不会热修复、插件化、组件化,80%以上的面试都过不了。
Android热修复框架、插件化框架、组件化框架、图片加载框架、网络访问框架、RxJava响应式编程框架、IOC依赖注入框架、最近架构组件Jetpack等等Android第三方开源框架。系统教程知识笔记已整理成PDF电子书上传在【GitHub】
1042页完整版PDF点击我就可以白嫖啦,记得给文章点个赞哦。
文末
欢迎关注我,分享Android干货,交流Android技术。
对文章有何见解,或者有何技术问题,都可以在评论区一起留言讨论,我会虔诚为你解答。
也欢迎大家来我的B站找我玩,有各类Android架构师进阶技术难点的视频讲解
B站直通车:https://space.bilibili.com/544650554
标签:缓存,协程,val,Kotlin,request,private,Coil,轻量级,加载 来源: https://blog.csdn.net/zzz777qqq/article/details/110002679