Coil源码解析

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. 整个加载流程从1.2.1的load函数开始,具体的代码如下:
kotlin 复制代码
imageView.load("https://example.com/image.jpg")
  1. 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)
}
  1. 可以看到load函数中使用Builder模式构建了一个ImageRequest对象,该ImageRequest对象中包含了传入的data数据(如果是网络图片请求的话就是Url),然后将生成的ImageRequest对象通过ImageLoader对象的enqueue方法入列
  2. 我们继续跟踪看看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请求,这里不进行详细分析了,感兴趣的直接去源码里面看看实现

  1. 上面流程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中,这部分逻辑相对简单,具体的部分感兴趣可以看看具体的实现,不在此处赘述,继续看主要加载流程

  1. 流程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
    }
  1. 流程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)
            }
        }
    }
  1. 流程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
    }
  1. 流程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
    }
  1. 流程走到这里,关注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
        }
    }
  1. 流程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)
        }
    }
  1. 流程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组合是如何实现图片加载的

  1. 首先是组合的方法实现:
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,
    )
}
  1. 上述代码中实现了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

  1. 上述代码的关键在AsyncImagePainter中,AsyncImagePainter类继承自Painter类实现了RememberObserver接口,RememberObserver接口中的onRemembered方法会在组合成功的记住的时候会被调用,方法的实现如下:
kotlin 复制代码
override fun onRemembered() = trace("AsyncImagePainter.onRemembered") {
        (painter as? RememberObserver)?.onRemembered()
        launchJob()
        isRemembered = true
    }
  1. 流程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开发!!!

创作不易,转发麻烦注明来源,谢谢

相关推荐
恋猫de小郭1 小时前
Flutter 3.38 发布,快来看看有什么更新吧
android·前端·flutter
百锦再6 小时前
第11章 泛型、trait与生命周期
android·网络·人工智能·python·golang·rust·go
会跑的兔子7 小时前
Android 16 Kotlin协程 第二部分
android·windows·kotlin
键来大师7 小时前
Android15 RK3588 修改默认不锁屏不休眠
android·java·framework·rk3588
江上清风山间明月10 小时前
Android 系统超级实用的分析调试命令
android·内存·调试·dumpsys
百锦再10 小时前
第12章 测试编写
android·java·开发语言·python·rust·go·erlang
用户693717500138414 小时前
Kotlin 协程基础入门系列:从概念到实战
android·后端·kotlin
SHEN_ZIYUAN15 小时前
Android 主线程性能优化实战:从 90% 降至 13%
android·cpu优化
曹绍华15 小时前
android 线程loop
android·java·开发语言
雨白15 小时前
Hilt 入门指南:从 DI 原理到核心用法
android·android jetpack