1 Coil介绍和简单用法示例
1.1 Coil简介
Coil(Coroutine Image Loader)是一个图片加载框架,可以满足android和compose多平台图片加载需要,github项目地址为github.com/coil-kt/coi... 。Coil具有以下特点:
- 快速:Coil执行多项优化,包括内存、磁盘缓存、图像降采样、自动暂停/取消请求等
- 轻量:Coil仅依赖于Kotlin、Coroutines和Okio
- 易于使用:Coil得Api利用Kotlin得语言功能实现简单性并减少样板代码
- 现代:Coil是Kotlin优先的,可与包括Compose、Coroutines、Okio、OkHttp和Ktor在内的现代库互操作
1.2 Coil用法示例
1.2.1 view的使用方式
首先是导入Coil库,在项目的gradle文件或者kts配置文件中,增加如下配置(目前最新是3.2.0,最新版本请到gitbub项目中查看):
kotlin
implementation("io.coil-kt.coil3:coil:3.2.0")
implementation("io.coil-kt.coil3:coil-network-okhttp:3.2.0")
//网络数据加载库
implementation("io.coil-kt.coil3:coil-network-okhttp:3.2.0")
implementation("io.coil-kt.coil3:coil-network-ktor2:3.2.0")
implementation("io.coil-kt.coil3:coil-network-ktor3:3.2.0")
需要特别说明的是,如果不导入网络数据加载相关的库的话,Coil3以后的版本默认是不支持网络数据加载的,这避免了一大堆的网络依赖引入到我们的项目中,保证了一些有自定义网络请求方案的用户或者不需要加载网络url图片的用户可以快速便捷上手Coil,不需要去解决烦人的库依赖问题。
网络数据加载库中的三个可以任意选一个导入就可以,如果你选择的是Okhttp的话,导入完成后就可自动加载Url类型的图片资源(例如网络图片Url格式为example.com/image.jpg );如果选择的是Ktor2或者Ktor3库的话,情况会复杂一些,需要手动为每个平台手动导入一个Ktor引擎(JavaScript平台除外),以下是各个平台的ktor引擎导入方式:
kotlin
androidMain {
dependencies {
implementation("io.ktor:ktor-client-android:<ktor-version>")
}
}
appleMain {
dependencies {
implementation("io.ktor:ktor-client-darwin:<ktor-version>")
}
}
jvmMain {
dependencies {
implementation("io.ktor:ktor-client-java:<ktor-version>")
}
}
如果需要使用自定义的网络库,可以导入io.coil-kt.coil3:coil-network-core库,实现NetworkClient,在ImageLoader中使用自定义实现的NetworkClient注册NetworkFetcher(具体的Coil网络库操作可以参考文档coil-kt.github.io/coil/networ... )。
完成以上的依赖导入操作后,来到了我们愉快的调用库函数加载图片流程了:
kotlin
imageView.load("https://example.com/image.jpg")
Coil甚至比Glide的加载更加简洁,一行代码直接搞定;当然了如果要设置其他的属性就需要另外的代码了,Coil中也有Glide中类似的placeHolder、errorHolder、transform等相应的实现,基本可以实现从Glide到Coil的无缝切换。
1.2.2 Compose中的使用方式
首先依然是库的导入,在我们的项目gradle或者kts文件中加入如下依赖:
kotlin
implementation("io.coil-kt.coil3:coil-compose:3.2.0")
//网络数据加载库
implementation("io.coil-kt.coil3:coil-network-okhttp:3.2.0")
implementation("io.coil-kt.coil3:coil-network-ktor2:3.2.0")
implementation("io.coil-kt.coil3:coil-network-ktor3:3.2.0")
网络数据库的加载部分在1.2.1的部分已经说明,这里不再赘述。直接看一下在compose项目中的使用方法,使用AsyncImage组合即可很方便地加图片:
kotlin
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data("https://example.com/image.jpg")
.crossfade(true)
.build(),
placeholder = painterResource(R.drawable.placeholder),
contentDescription = stringResource(R.string.description),
contentScale = ContentScale.Crop,
modifier = Modifier.clip(CircleShape),
)
除了支持官方标准Image组合一样的图片展示功能外,AsyncImage组合可以异步执行图片请求并完成渲染,也与Glide图片加载框架一样支持设置placeHolder、error等属性,也支持加载过程中的各种回调,如onLoading、onSuccess和onError等。
2 Coil源码实现分析
2.1 图片加载主流程分析
- 整个加载流程从1.2.1的load函数开始,具体的代码如下:
kotlin
imageView.load("https://example.com/image.jpg")
- imageView.load中的load方法的具体实现是ImageView一个扩展函数(coil3.SingletonImageLoaders_androidKt#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)
}
- 可以看到load函数中使用Builder模式构建了一个ImageRequest对象,该ImageRequest对象中包含了传入的data数据(如果是网络图片请求的话就是Url),然后将生成的ImageRequest对象通过ImageLoader对象的enqueue方法入列
- 我们继续跟踪看看imageLoader的来源,在我们调用imageView.load过程中imageLoader参数项使用的是默认参数,来自于context的扩展变量,经过coil3.SingletonImageLoader#get和coil3.SingletonImageLoader#newImageLoader等方法的层层调用,最终实际上生成了一个RealImageLoader对象,回到上面的流程3的分析中,ImageLoader对象的enqueue方法真正的实现类实际在coil3.RealImageLoader#enqueue方法中,enqueue方法中的代码如下:
kotlin
override fun enqueue(request: ImageRequest): Disposable {
// Start executing the request on the main thread.
val job = scope.async(Dispatchers.Main.immediate) {
execute(request, REQUEST_TYPE_ENQUEUE)
}
// Update the current request attached to the view and return a new disposable.
return getDisposable(request, job)
}
enqueue方法中,通过async构建一个协程并返回协程句柄job,在此协程内就一个execute挂起函数,协程外部调用getDisposable方法返回一个Disposable接口对象,该接口对象的具体实现类有两个(coil3.request.OneShotDisposable和coil3.request.ViewTargetDisposable),这个接口的作用是处理ImageRequest对象是否被ImageLoader执行,或者通过Disposable的job对象取消ImageRequest请求,这里不进行详细分析了,感兴趣的直接去源码里面看看实现
- 上面流程4中提到的execute挂起函数,下面贴出该方法的代码:
kotlin
private suspend fun execute(initialRequest: ImageRequest, type: Int): ImageResult {
// Wrap the request to manage its lifecycle.
val requestDelegate = requestService.requestDelegate(
request = initialRequest,
job = coroutineContext.job,
findLifecycle = type == REQUEST_TYPE_ENQUEUE,
).apply { assertActive() }
// Apply this image loader's defaults and other configuration to this request.
val request = requestService.updateRequest(initialRequest)
// Create a new event listener.
val eventListener = options.eventListenerFactory.create(request)
try {
// Fail before starting if data is null.
if (request.data == NullRequestData) {
throw NullRequestDataException()
}
// Set up the request's lifecycle observers.
requestDelegate.start()
// Enqueued requests suspend until the lifecycle is started.
if (type == REQUEST_TYPE_ENQUEUE) {
requestDelegate.awaitStarted()
}
// Set the placeholder on the target.
val cachedPlaceholder = request.placeholderMemoryCacheKey?.let { memoryCache?.get(it)?.image }
request.target?.onStart(placeholder = cachedPlaceholder ?: request.placeholder())
eventListener.onStart(request)
request.listener?.onStart(request)
// Resolve the size.
val sizeResolver = request.sizeResolver
eventListener.resolveSizeStart(request, sizeResolver)
val size = sizeResolver.size()
eventListener.resolveSizeEnd(request, size)
// Execute the interceptor chain.
val result = withContext(request.interceptorCoroutineContext) {
RealInterceptorChain(
initialRequest = request,
interceptors = components.interceptors,
index = 0,
request = request,
size = size,
eventListener = eventListener,
isPlaceholderCached = cachedPlaceholder != null,
).proceed()
}
// Set the result on the target.
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 {
// Create the default error result if there's an uncaught exception.
val result = ErrorResult(request, throwable)
onError(result, request.target, eventListener)
return result
}
} finally {
requestDelegate.complete()
}
}
在上述代码中,在withContext的协程构建器中返回目标数据后,走到了onSuccess逻辑,这部分逻辑的实现如下:
kotlin
private fun onSuccess(
result: SuccessResult,
target: Target?,
eventListener: EventListener,
) {
val request = result.request
val dataSource = result.dataSource
options.logger?.log(TAG, Logger.Level.Info) {
"${dataSource.emoji} Successful (${dataSource.name}) - ${request.data}"
}
transition(result, target, eventListener) {
target?.onSuccess(result.image)
}
eventListener.onSuccess(request, result)
request.listener?.onSuccess(request, result)
}
上述代码的11-13行完成了transition和更新资源到目标view的逻辑,具体的逻辑在coil3.RealImageLoader_androidKt#transition和coil3.target.GenericViewTarget#onSuccess中,这部分逻辑相对简单,具体的部分感兴趣可以看看具体的实现,不在此处赘述,继续看主要加载流程
- 流程5中贴出来的代码看起来有点多对不对,其实关键的部分就是42-52行那部分代码,proceed方法中的责任链模式,相信了解Okhttp的同学对这个都很了解了,一起看看proceed(coil3.intercept.RealInterceptorChain#proceed)方法中做的处理:
kotlin
override suspend fun proceed(): ImageResult {
val interceptor = interceptors[index]
val next = copy(index = index + 1)
val result = interceptor.intercept(next)
checkRequest(result.request, interceptor)
return result
}
- 流程6的proceed方法除了我们自定义的拦截器外,最重要的就是Coil自己的拦截器EngineInterceptor,一起看看引擎的拦截器中的intercept方法做了些什么:
kotlin
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
try {
val request = chain.request
val data = request.data
val size = chain.size
val eventListener = chain.eventListener
val options = requestService.options(request, size)
val scale = options.scale
// Perform any data mapping.
eventListener.mapStart(request, data)
val mappedData = imageLoader.components.map(data, options)
eventListener.mapEnd(request, mappedData)
// Check the memory cache.
val cacheKey = memoryCacheService.newCacheKey(request, mappedData, options, eventListener)
val cacheValue = cacheKey?.let { memoryCacheService.getCacheValue(request, it, size, scale) }
// Fast path: return the value from the memory cache.
if (cacheValue != null) {
return memoryCacheService.newResult(chain, request, cacheKey, cacheValue)
}
// Slow path: fetch, decode, transform, and cache the image.
return withContext(request.fetcherCoroutineContext) {
// Fetch and decode the image.
val result = execute(request, mappedData, options, eventListener)
// Register memory pressure callbacks.
systemCallbacks.registerMemoryPressureCallbacks()
// Write the result to the memory cache.
val isCached = memoryCacheService.setCacheValue(cacheKey, request, result)
// Return the result.
SuccessResult(
image = result.image,
request = request,
dataSource = result.dataSource,
memoryCacheKey = cacheKey.takeIf { isCached },
diskCacheKey = result.diskCacheKey,
isSampled = result.isSampled,
isPlaceholderCached = chain.isPlaceholderCached,
)
}
} catch (throwable: Throwable) {
if (throwable is CancellationException) {
throw throwable
} else {
return ErrorResult(chain.request, throwable)
}
}
}
- 流程7贴出来的代码依然一大堆,但是整个方法的核心代码其实也就是第27行的execute方法,这是真正进行请求的地方,其他的都是一些缓存和异常的处理逻辑,我们先丢开这些细枝末节的地方,继续看看整个加载主流程,execute方法实现:
kotlin
private suspend fun execute(
request: ImageRequest,
mappedData: Any,
options: Options,
eventListener: EventListener,
): ExecuteResult {
@Suppress("NAME_SHADOWING")
var options = options
var components = imageLoader.components
var fetchResult: FetchResult? = null
val executeResult = try {
options = requestService.updateOptions(options)
if (request.fetcherFactory != null || request.decoderFactory != null) {
components = components.newBuilder()
.addFirst(request.fetcherFactory)
.addFirst(request.decoderFactory)
.build()
}
// Fetch the data.
fetchResult = fetch(components, request, mappedData, options, eventListener)
// Decode the data.
when (fetchResult) {
is SourceFetchResult -> withContext(request.decoderCoroutineContext) {
decode(fetchResult, components, request, mappedData, options, eventListener)
}
is ImageFetchResult -> {
ExecuteResult(
image = fetchResult.image,
isSampled = fetchResult.isSampled,
dataSource = fetchResult.dataSource,
diskCacheKey = null, // This result has no file source.
)
}
}
} finally {
// Ensure the fetch result's source is always closed.
(fetchResult as? SourceFetchResult)?.source?.closeQuietly()
}
// Apply any transformations and prepare to draw.
val finalResult = transform(executeResult, request, options, eventListener, logger)
finalResult.image.prepareToDraw()
return finalResult
}
- 流程8中的代码关键在22行的fetch方法,他的具体实现如下:
kotlin
private suspend fun fetch(
components: ComponentRegistry,
request: ImageRequest,
mappedData: Any,
options: Options,
eventListener: EventListener,
): FetchResult {
val fetchResult: FetchResult
var searchIndex = 0
while (true) {
val pair = components.newFetcher(mappedData, options, imageLoader, searchIndex)
checkNotNull(pair) { "Unable to create a fetcher that supports: $mappedData" }
val fetcher = pair.first
searchIndex = pair.second + 1
eventListener.fetchStart(request, fetcher, options)
val result = fetcher.fetch()
try {
eventListener.fetchEnd(request, fetcher, options, result)
} catch (throwable: Throwable) {
// Ensure the source is closed if an exception occurs before returning the result.
(result as? SourceFetchResult)?.source?.closeQuietly()
throw throwable
}
if (result != null) {
fetchResult = result
break
}
}
return fetchResult
}
- 流程走到这里,关注17行的fetcher.fetch()代码,Fetcher接口的实现类有很多,比如AssetUriFetcher、BitmapFetcher、ByteArrayFetcher等针对各种资源类型的Fetcher,这些实现都是生成各种类型的数据资源并最终显示到ImageView上的,具体每个类型的数据转换我不做介绍了,感兴趣的可以自己去看一下每种Fetcher的实现。这里我们重点介绍一下NetworkFetcher,顾名思义NetworkFetcher主要是处理网络来源的图片数据的,来看以下它的fetch方法实现:
kotlin
override suspend fun fetch(): FetchResult {
var snapshot = readFromDiskCache()
try {
// Fast path: fetch the image from the disk cache without performing a network request.
var readResult: CacheStrategy.ReadResult? = null
var cacheResponse: NetworkResponse? = null
if (snapshot != null) {
// Always return files with empty metadata as it's likely they've been written
// to the disk cache manually.
if (fileSystem.metadata(snapshot.metadata).size == 0L) {
return SourceFetchResult(
source = snapshot.toImageSource(),
mimeType = getMimeType(url, null),
dataSource = DataSource.DISK,
)
}
// Return the image from the disk cache if the cache strategy agrees.
cacheResponse = snapshot.toNetworkResponseOrNull()
if (cacheResponse != null) {
readResult = cacheStrategy.value.read(cacheResponse, newRequest(), options)
if (readResult.response != null) {
return SourceFetchResult(
source = snapshot.toImageSource(),
mimeType = getMimeType(url, readResult.response.headers[CONTENT_TYPE]),
dataSource = DataSource.DISK,
)
}
}
}
// Slow path: fetch the image from the network.
val networkRequest = readResult?.request ?: newRequest()
var fetchResult = executeNetworkRequest(networkRequest) { response ->
// Write the response to the disk cache then open a new snapshot.
snapshot = writeToDiskCache(snapshot, cacheResponse, networkRequest, response)
if (snapshot != null) {
cacheResponse = snapshot!!.toNetworkResponseOrNull()
return@executeNetworkRequest SourceFetchResult(
source = snapshot!!.toImageSource(),
mimeType = getMimeType(url, cacheResponse?.headers?.get(CONTENT_TYPE)),
dataSource = DataSource.NETWORK,
)
}
// If we failed to read a new snapshot then read the response body if it's not empty.
val responseBody = response.requireBody().readBuffer()
if (responseBody.size > 0) {
return@executeNetworkRequest SourceFetchResult(
source = responseBody.toImageSource(),
mimeType = getMimeType(url, response.headers[CONTENT_TYPE]),
dataSource = DataSource.NETWORK,
)
}
return@executeNetworkRequest null
}
// Fallback: if the response body is empty, execute a new network request without the
// cache headers.
if (fetchResult == null) {
fetchResult = executeNetworkRequest(newRequest()) { response ->
SourceFetchResult(
source = response.requireBody().toImageSource(),
mimeType = getMimeType(url, response.headers[CONTENT_TYPE]),
dataSource = DataSource.NETWORK,
)
}
}
return fetchResult
} catch (e: Exception) {
snapshot?.closeQuietly()
throw e
}
}
- 流程10中关注coil3.network.NetworkFetcher#executeNetworkRequest方法,这个方法是真正去执行网络请求的地方,具体的代码实现如下:
kotlin
private suspend fun <T> executeNetworkRequest(
request: NetworkRequest,
block: suspend (NetworkResponse) -> T,
): T {
// Prevent executing requests on the main thread that could block due to a
// networking operation.
if (options.networkCachePolicy.readEnabled) {
assertNotOnMainThread()
}
return networkClient.value.executeRequest(request) { response ->
if (response.code !in 200 until 300 && response.code != HTTP_RESPONSE_NOT_MODIFIED) {
throw HttpException(response)
}
block(response)
}
}
- 流程11的executeRequest方法是NetworkClient接口中定义的,其中NetworkClient接口一共有三个实现类,分别是CallFactoryNetworkClient、Ktor2版本的KtorNetworkClient和Ktor3版本的KtorNetworkClient,CallFactoryNetworkClient的网络请求实现如下:
kotlin
internal value class CallFactoryNetworkClient(
private val callFactory: Call.Factory,
) : NetworkClient {
override suspend fun <T> executeRequest(
request: NetworkRequest,
block: suspend (response: NetworkResponse) -> T,
) = callFactory.newCall(request.toRequest()).await().use { response ->
block(response.toNetworkResponse())
}
}
使用了OkHttp的Call接口扩展方法await使用suspendCancellableCoroutine开启协程挂起异步的网络请求,至此CallFactoryNetworkClient网络请求部分结束,下面再看看Ktor版本的KtorNetworkClient的实现是怎样的,这里以Ktor3版本的实现为例:
kotlin
internal value class KtorNetworkClient(
private val httpClient: HttpClient,
) : NetworkClient {
override suspend fun <T> executeRequest(
request: NetworkRequest,
block: suspend (response: NetworkResponse) -> T,
) = httpClient.prepareRequest(request.toHttpRequestBuilder()).execute { response ->
block(response.toNetworkResponse())
}
}
可见KtorNetworkClient版本的也还是执行网络请求,网络资源的加载,至此图片加载的主流程就结束了。
2.2 Coil在compose中的图片加载流程
在前面的1.2.2中介绍了Compose的用法,主要是使用了AsyncImage组合,本小结主要看看AsyncImage组合是如何实现图片加载的
- 首先是组合的方法实现:
kotlin
@Composable
private fun AsyncImage(
state: AsyncImageState,
contentDescription: String?,
modifier: Modifier,
transform: (State) -> State,
onState: ((State) -> Unit)?,
alignment: Alignment,
contentScale: ContentScale,
alpha: Float,
colorFilter: ColorFilter?,
filterQuality: FilterQuality,
clipToBounds: Boolean,
) {
val request = requestOfWithSizeResolver(
model = state.model,
contentScale = contentScale,
)
validateRequest(request)
Layout(
modifier = modifier.then(
ContentPainterElement(
request = request,
imageLoader = state.imageLoader,
modelEqualityDelegate = state.modelEqualityDelegate,
transform = transform,
onState = onState,
contentScale = contentScale,
filterQuality = filterQuality,
alignment = alignment,
alpha = alpha,
colorFilter = colorFilter,
clipToBounds = clipToBounds,
previewHandler = previewHandler(),
contentDescription = contentDescription,
),
),
measurePolicy = UseMinConstraintsMeasurePolicy,
)
}
- 上述代码中实现了ModifierNodeElement接口对应的Node为ContentPainterNode,看一下coil3.compose.internal.ContentPainterElement#create的方法实现:
kotlin
override fun create(): ContentPainterNode {
val input = Input(imageLoader, request, modelEqualityDelegate)
// Create the painter during modifier creation so we reuse the same painter object when the
// modifier is being reused as part of the lazy layouts reuse flow.
val painter = AsyncImagePainter(input)
painter.transform = transform
painter.onState = onState
painter.contentScale = contentScale
painter.filterQuality = filterQuality
painter.previewHandler = previewHandler
painter._input = input
return ContentPainterNode(
painter = painter,
constraintSizeResolver = request.sizeResolver as? ConstraintsSizeResolver,
alignment = alignment,
contentScale = contentScale,
alpha = alpha,
colorFilter = colorFilter,
clipToBounds = clipToBounds,
contentDescription = contentDescription,
)
}
其中ContentPainterNode实现了Modifier.Node(), DrawModifierNode, LayoutModifierNode, SemanticsModifierNode
- 上述代码的关键在AsyncImagePainter中,AsyncImagePainter类继承自Painter类实现了RememberObserver接口,RememberObserver接口中的onRemembered方法会在组合成功的记住的时候会被调用,方法的实现如下:
kotlin
override fun onRemembered() = trace("AsyncImagePainter.onRemembered") {
(painter as? RememberObserver)?.onRemembered()
launchJob()
isRemembered = true
}
- 流程3中的launchJob(coil3.compose.AsyncImagePainter#launchJob)方法中是真正加载数据的地方,其实现如下:
kotlin
private fun launchJob() {
val input = _input ?: return
rememberJob = scope.launchWithDeferredDispatch {
val previewHandler = previewHandler
val state = if (previewHandler != null) {
// If we're in inspection mode use the preview renderer.
val request = updateRequest(input.request, isPreview = true)
previewHandler.handle(input.imageLoader, request)
} else {
// Else, execute the request as normal.
val request = updateRequest(input.request, isPreview = false)
input.imageLoader.execute(request).toState()
}
updateState(state)
}
}
可以看到代码中13行执行了ImageLoader的execute方法,获取结果后调用updateState更新stateFlow的值,也就更新了Painter,由于状态更新coil3.compose.internal.AbstractContentPainterNode#draw方法触发,界面会触发绘制操作
2.3 Coil图片加载的细枝末节
2.3.1 如何自动取消请求
或许在2.1中分析主流程的时候,你会想问Coil是如何控制请求的取消,这里我们继续看看代码,具体的细节隐藏在2.1的流程5贴出的代码中,其中第3行requestDelegate具体的实现在coil3.request.AndroidRequestService#requestDelegate中,具体的代码实现如下:
kotlin
override fun requestDelegate(
request: ImageRequest,
job: Job,
findLifecycle: Boolean,
): RequestDelegate {
val target = request.target
if (target is ViewTarget<*>) {
val lifecycle = request.lifecycle ?: request.findLifecycle()
return ViewTargetRequestDelegate(imageLoader, request, target, lifecycle, job)
}
val lifecycle = request.lifecycle ?: if (findLifecycle) request.findLifecycle() else null
if (lifecycle != null) {
return LifecycleRequestDelegate(lifecycle, job)
}
return BaseRequestDelegate(job)
}
从上面的代码很容易可以看出来,ViewTargetRequestDelegate和LifecycleRequestDelegate承接了真正的生命周期回调,这两个类都实现了coil3.request.RequestDelegate和androidx.lifecycle.DefaultLifecycleObserver接口,且都传入了job对象,这个job对象对应的就是coil3.RealImageLoader#enqueue方法中async构建的协程句柄,通过这个job对象进行任务的取消和dispose
2.3.2 请求的内存缓存实现
在coil3.intercept.EngineInterceptor#intercept方法中,在真正执行网络请求之前,除了进行了进行了数据的Mapping(都转成Uri格式的形式)之外,还通过coil3.memory.MemoryCacheService类进行了缓存操作,通过层层调用最终会走到coil3.memory.RealMemoryCache类中,其中RealMemoryCache又分别通过两个代理类coil3.memory.RealStrongMemoryCache和coil3.memory.RealWeakMemoryCache进行缓存数据的处理,其中RealStrongMemoryCache用到了著名的Lru算法 在这里将RealStrongMemoryCache和RealWeakMemoryCache中的关键实现分别看一下,首先是RealStrongMemoryCache:
kotlin
private val cache = object : LruCache<Key, InternalValue>(maxSize) {
override fun sizeOf(
key: Key,
value: InternalValue,
) = value.size
override fun entryRemoved(
key: Key,
oldValue: InternalValue,
newValue: InternalValue?,
) = weakMemoryCache.set(key, oldValue.image, oldValue.extras, oldValue.size)
}
可以看到在强引用中被删除的数据会被添加到弱引用中,coil3.memory.RealWeakMemoryCache#set方法在后面介绍,这里先看下RealStrongMemoryCache的set方法:
kotlin
override fun set(
key: Key,
image: Image,
extras: Map<String, Any>,
size: Long,
) {
if (size <= maxSize) {
cache.put(key, InternalValue(image, extras, size))
} else {
// If the value is too big for the cache, don't attempt to store it as doing
// so will cause the cache to be cleared. Instead, evict an existing element
// with the same key if it exists and add the value to the weak memory cache.
cache.remove(key)
weakMemoryCache.set(key, image, extras, size)
}
}
RealStrongMemoryCache的set方法逻辑很简单,如果小于设定的最大值则直接往LruCache中存放,当存入的缓存数据大于设定的最大值的时候,会尝试往弱引用中存放,现在看看coil3.memory.RealWeakMemoryCache#set方法在做些什么,其关键代码如下:
kotlin
override fun set(
key: Key,
image: Image,
extras: Map<String, Any>,
size: Long,
) {
val values = cache.getOrPut(key) { arrayListOf() }
// Insert the value into the list sorted descending by size.
val newValue = InternalValue(WeakReference(image), extras, size)
if (values.isEmpty()) {
values += newValue
} else {
for (index in values.indices) {
val value = values[index]
if (size >= value.size) {
if (value.image.get() === image) {
values[index] = newValue
} else {
values.add(index, newValue)
}
break
}
}
}
cleanUpIfNecessary()
}
可见coil3.memory.RealWeakMemoryCache#set方法是按照缓存的image数据大小升序排列的,当数据到达一定大小时会触发clearUp操作(默认设定的CLEAN_UP_INTERVAL = 10,大于10个的时候触发弱引用清空)
2.3.3 Coil中的硬盘缓存
在coil3.network.NetworkFetcher#fetch中会调用coil3.network.NetworkFetcher#readFromDiskCache方法,经过各层调用会走到coil3.disk.RealDiskCache,其中也用到了Lru算法进行了数据的处理,其主要的数据实体控制类为coil3.disk.DiskLruCache,其中这个里面主要涉及对于DiskLruCache中的数据增删改查操作,其中缓存是使用一个名为journal的文件记录,一个典型的文件如下所示:
kotlin
libcore.io.DiskLruCache
1
100
2
CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
DIRTY 335c4c6028171cfddfbaae1a9c313c52
CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
REMOVE 335c4c6028171cfddfbaae1a9c313c52
DIRTY 1ab96a171faeeee38496d8b330771a7a
CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
READ 335c4c6028171cfddfbaae1a9c313c52
READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
前五行为文件头,分别为字符串常量"libcore.io.DiskLruCache"、cache版本、应用版本、数据值和一个空白行。 文件中的每一后续行都是一个缓存条目状态的记录。每行包含由空格分隔的值:一个状态标识符、一个键(key),以及可选的、特定于该状态的值。
DIRTY 行:用于跟踪一个条目正在被创建或更新。每一个成功的 DIRTY 操作之后都应紧跟着一个 CLEAN 或 REMOVE 操作。缺少匹配的 CLEAN 或 REMOVE 的 DIRTY 行表明可能需要删除临时文件。
CLEAN 行:用于记录一个已成功发布并可供读取的缓存条目。一条CLEAN 行后面会跟着其每个值的长度。
READ 行:用于跟踪访问情况,以实现 LRU(最近最少使用)算法。
REMOVE 行:用于跟踪已被删除的条目。 随着缓存操作的进行,日志文件会被持续追加写入。日志文件可能会通过丢弃冗余行来进行压缩(compaction)。在压缩过程中,会使用一个名为 "journal.tmp" 的临时文件;当缓存被打开时,如果该临时文件存在,则应将其删除。
这一块的具体的代码比较多,重点在于理解journal日志文件的作用,代码部分自己看就可以了,面试中经常会被问到的问题了,属于是面试重点问题(划重点啦)
2.3.4 Coil中的transformation是如何实现的
Coil框架允许我们设置一些图片的变换,比如设置一个圆角的Transformation,这些Transformation是如何被处理的呢,其实在2.1分析图片加载主流程的时候,2.1的流程8中有贴出来这一块的代码,实际上transfrom的逻辑入口在 coil3.intercept.EngineInterceptorKt#transform方法中,具体的实现如下:
kotlin
internal suspend fun transform(
result: ExecuteResult,
request: ImageRequest,
options: Options,
eventListener: EventListener,
logger: Logger?,
): ExecuteResult {
val transformations = request.transformations
if (transformations.isEmpty()) {
return result
}
// Skip the transformations as converting to a bitmap is disabled.
val image = result.image
if (image !is BitmapImage && !request.allowConversionToBitmap) {
logger?.log(TAG, Logger.Level.Info) {
val type = result.image::class.simpleName
"allowConversionToBitmap=false, skipping transformations for type $type."
}
return result
}
// Apply the transformations.
val input = convertImageToBitmap(image, options, transformations, logger)
eventListener.transformStart(request, input)
val output = transformations.foldIndices(input) { bitmap, transformation ->
transformation.transform(bitmap, options.size).also { coroutineContext.ensureActive() }
}
eventListener.transformEnd(request, output)
return result.copy(image = output.asImage())
}
在这里通过输入的原始数据ExecuteResult转化为初始输入,依次将transformation作用在input上,得到transform之后的结果。
2.3.5 不同类型的数据格式Coil中如何解码
在2.1的主流程分析中,执行到coil3.intercept.EngineInterceptor#execute方法返回的结果后,会执行coil3.intercept.EngineInterceptor#decode方法,方法的实现如下:
kotlin
private suspend fun decode(
fetchResult: SourceFetchResult,
components: ComponentRegistry,
request: ImageRequest,
mappedData: Any,
options: Options,
eventListener: EventListener,
): ExecuteResult {
val decodeResult: DecodeResult
var searchIndex = 0
while (true) {
val pair = components.newDecoder(fetchResult, options, imageLoader, searchIndex)
checkNotNull(pair) { "Unable to create a decoder that supports: $mappedData" }
val decoder = pair.first
searchIndex = pair.second + 1
eventListener.decodeStart(request, decoder, options)
val result = decoder.decode()
eventListener.decodeEnd(request, decoder, options, result)
if (result != null) {
decodeResult = result
break
}
}
// Combine the fetch and decode operations' results.
return ExecuteResult(
image = decodeResult.image,
isSampled = decodeResult.isSampled,
dataSource = fetchResult.dataSource,
diskCacheKey = (fetchResult.source as? FileImageSource)?.diskCacheKey,
)
}
通过每个具体的Decoder实现类的decode方法进行解码,完成格式转换,具体的Decoder实现类有如GifDecoder、SVGDecoder等格式的数据,每种数据源的输入最终都会输出coil3.decode.DecodeResult形式的数据。
3 小结
看框架的源码本身是一个枯燥的过程,所以本文可能看起来也有那么一些无聊,存在大段的代码。其实框架的代码还有很多写的很出色的地方,比如builder模式、拦截器模式等设计模式,还有一些扩展函数的使用,甚至一些接口的设计等,这些都是值得我们Android开发学习的地方。写本篇文章的初衷也是为了记录下自己的一些学习经历,毕竟好记性不如烂笔头嘛,有些代码中的细节可能这篇文章写的不是很清楚,欢迎大家提出意见,一起学习和改进。加油每一位Android开发!!!
创作不易,转发麻烦注明来源,谢谢