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开发!!!

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

相关推荐
用户2018792831674 小时前
通俗易懂的讲解:Android系统启动全流程与Launcher诞生记
android
二流小码农4 小时前
鸿蒙开发:资讯项目实战之项目框架设计
android·ios·harmonyos
用户2018792831675 小时前
WMS 的核心成员和窗口添加过程
android
用户2018792831675 小时前
PMS 创建之“软件包管理超级工厂”的建设
android
用户2018792831675 小时前
通俗易懂的讲解:Android APK 解析的故事
android
渣渣_Maxz6 小时前
使用 antlr 打造 Android 动态逻辑判断能力
android·设计模式
Android研究员6 小时前
HarmonyOS实战:List拖拽位置交换的多种实现方式
android·ios·harmonyos
guiyanakaung6 小时前
一篇文章让你学会 Compose Multiplatform 推荐的桌面应用打包工具 Conveyor
android·windows·macos
恋猫de小郭6 小时前
Flutter 应该如何实现 iOS 26 的 Liquid Glass ,它为什么很难?
android·前端·flutter
葱段6 小时前
【Compose】Android Compose 监听TextField粘贴事件
android·kotlin·jetbrains