美文网首页
Android 关于 Coil 源码阅读之部分疑问记录

Android 关于 Coil 源码阅读之部分疑问记录

作者: 雁过留声_泪落无痕 | 来源:发表于2022-03-25 13:34 被阅读0次

    背景

    疑问

    1. HttpUriFetcher 中在主线程请求网络为什么没有抛 NetworkOnMainThreadException 异常。
    private suspend fun executeNetworkRequest(request: Request): Response {
        val response = if (isMainThread()) {
            if (options.networkCachePolicy.readEnabled) {
                // Prevent executing requests on the main thread that could block due to a
                // networking operation.
                throw NetworkOnMainThreadException()
            } else {
                // Work around: https://github.com/Kotlin/kotlinx.coroutines/issues/2448
                // 这里为主线程
                callFactory.value.newCall(request).execute()
            }
        } else {
            // Suspend and enqueue the request on one of OkHttp's dispatcher threads.
            callFactory.value.newCall(request).await()
        }
        if (!response.isSuccessful && response.code != HTTP_NOT_MODIFIED) {
            response.body?.closeQuietly()
            // 这里抛了异常
            throw HttpException(response)
        }
        return response
    }
    

    结论:走到这里是因为 networkCachePolicy.readEnabledfalse,在 HttpUriFetcher#newRequest() 方法中会设置 Request 的 CacheControl 为 FORCE_CACHE,所以是不会真正发起网络请求的,因而不会出现 NetworkOnMainThreadException

    1. HttpUriFetcher#executeNetworkRequest() 方法中抛异常为什么没有 crash,源码参见上面的代码。
      结论:在 EngineInterceptor#intercept() 方法中使用了 try-catch
    override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
        try {
            ...
    
            // Slow path: fetch, decode, transform, and cache the image.
            return withContext(request.fetcherDispatcher) {
                // Fetch and decode the image.
                val result = execute(request, mappedData, options, eventListener)
    
                // Write the result to the memory cache.
                val isCached = memoryCacheService.setCacheValue(cacheKey, request, result)
    
                // Return the result.
                SuccessResult(
                    drawable = result.drawable,
                    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 requestService.errorResult(chain.request, throwable)
            }
        }
    }
    
    1. HttpUriFetch#fetch() 方法中是如何处理硬盘缓存的
    override suspend fun fetch(): FetchResult {
        var snapshot = readFromDiskCache()
        try {
            // Fast path: fetch the image from the disk cache without performing a network request.
            val cacheStrategy: CacheStrategy
            if (snapshot != null) {
                // 针对手动添加的缓存,直接返回缓存结果
                // Always return cached images with empty metadata as they were likely added manually.
                if (fileSystem.metadata(snapshot.metadata).size == 0L) {
                    return SourceResult(
                        source = snapshot.toImageSource(),
                        mimeType = getMimeType(url, null),
                        dataSource = DataSource.DISK
                    )
                }
    
                // 在 ImageLoader 中可以设置是否考虑 Header 来处理缓存,
                // 如果不考虑,则缓存会一直有效直到缓存文件夹的大小超过
                // 设定的最大值;否则会根据 Header 中的缓存字段来决定缓存是否可用
                // Return the candidate from the cache if it is eligible.
                if (respectCacheHeaders) {
                    cacheStrategy = CacheStrategy.Factory(newRequest(), snapshot.toCacheResponse()).compute()
                    // 策略表明缓存命中,直接返回缓存
                    if (cacheStrategy.networkRequest == null && cacheStrategy.cacheResponse != null) {
                        return SourceResult(
                            source = snapshot.toImageSource(),
                            mimeType = getMimeType(url, cacheStrategy.cacheResponse.contentType),
                            dataSource = DataSource.DISK
                        )
                    }
                } else {
                    // 这种情况下,存在缓存则直接返回缓存
                    // Skip checking the cache headers if the option is disabled.
                    return SourceResult(
                        source = snapshot.toImageSource(),
                        mimeType = getMimeType(url, snapshot.toCacheResponse()?.contentType),
                        dataSource = DataSource.DISK
                    )
                }
            } else {
                // 缓存不存在的情况下,生成一个缓存策略
                cacheStrategy = CacheStrategy.Factory(newRequest(), null).compute()
            }
    
            // 到了这里,策略中的 networkRequest 一定不为空,需要发起网络请求
            // Slow path: fetch the image from the network.
            var response = executeNetworkRequest(cacheStrategy.networkRequest!!)
            var responseBody = response.requireBody()
            try {
                // 根据缓存快照和网络返回值更新缓存:
                // 有可能存在缓存时间过期但是网络请求返回 304 的情况,表明缓存仍然有效,此时需要更新快照的 metadata;
                // 另外,如果网络请求返回了新的 body 数据,则需要更新整个缓存
                // Write the response to the disk cache then open a new snapshot.
                snapshot = writeToDiskCache(
                    snapshot = snapshot,
                    request = cacheStrategy.networkRequest,
                    response = response,
                    cacheResponse = cacheStrategy.cacheResponse
                )
                // 如果缓存成功写入,表明一切 OK,返回结果即可
                if (snapshot != null) {
                    return SourceResult(
                        source = snapshot.toImageSource(),
                        mimeType = getMimeType(url, snapshot.toCacheResponse()?.contentType),
                        dataSource = DataSource.NETWORK
                    )
                }
    
                // 如果写缓存失败(发生异常,或者网络请求返表明不允许使用缓存等情况),此时看网络返回是否有 body,如果有则返回 body
                // If we failed to read a new snapshot then read the response body if it's not empty.
                if (responseBody.contentLength() > 0) {
                    return SourceResult(
                        source = responseBody.toImageSource(),
                        mimeType = getMimeType(url, responseBody.contentType()),
                        dataSource = response.toDataSource()
                    )
                } else {
                    // 最后,一切都不满足的情况下,使用一个 header 中不带缓存字段的请求,这样一定会拿到 body(不发生异常的情况下)
                    // If the response body is empty, execute a new network request without the
                    // cache headers.
                    response.closeQuietly()
                    response = executeNetworkRequest(newRequest())
                    responseBody = response.requireBody()
    
                    return SourceResult(
                        source = responseBody.toImageSource(),
                        mimeType = getMimeType(url, responseBody.contentType()),
                        dataSource = response.toDataSource()
                    )
                }
            } catch (e: Exception) {
                // 注意关闭资源
                response.closeQuietly()
                throw e
            }
        } catch (e: Exception) {
            // 注意关闭资源
            snapshot?.closeQuietly()
            throw e
        }
    }
    

    结论:见代码中的注释

    1. BitmapFactoryDecoder#decode() 方法中调用了 runInterruptible(context: CoroutineContext = EmptyCoroutineContext, block: () -> T) 方法,也就是说 block 部分代码的执行可能会被中断如果协程取消的话,同时该方法抛出 CancellationException 异常。那么 block 是如何被中断的呢?
    override suspend fun decode() = parallelismLock.withPermit {
        runInterruptible { BitmapFactory.Options().decode() }
    }
    
    Interruptible.kt

    结论:

    • 线程处于阻塞状态时(等待,睡眠,或者占用),如果被中断了(调用了 interrupt() 方法),会抛 InterruptedException
    • 在 EngineInterceptor#intercept() 方法或者 RealImageLoader#executeMain() 方法的 catch 语句块中打断点,可以看到除了 CancellationException 异常,还会有 InterruptedIOException 异常出现,在 InterruptedIOException 的构造函数中打断点,发现是在 Http2Stream#waitForIo() 方法中调过来的
    • OkHttp 的 Http2Stream#waitForIo() 方法中会调用 wait() 然后等待 IO(可能还有其它地方会出现线程处于阻塞状态的情况时被中断,只是该方法这儿出现的几率较大,毕竟等待 IO 比较耗时)。理论上就是在 wait 的时候有其它地方调用了 Thread#interrupt() 方法,导致了 InterruptedIOException 异常被抛出
    @Throws(InterruptedIOException::class)
    internal fun waitForIo() {
        try {
            wait()
        } catch (_: InterruptedException) {
            Thread.currentThread().interrupt() // Retain interrupted status.
            throw InterruptedIOException()
        }
    }
    
    • Http2Stream#waitForIo() 在 wait 时被中断,势必有其它地方调用了 Thread#interrupt() 方法,于是在 Thread#interrupt() 方法中打断点,查看调用栈。可以明显看到是 kotlin 协程中调过来的,再往上找,发现是在 ViewTargetRequestManager#setRequest() 方法中调用了 dispose() 方法,进而调用了 cancel() 方法
    // ViewTargetRequestManager.kt
    fun setRequest(request: ViewTargetRequestDelegate?) {
        currentRequest?.dispose()
        currentRequest = request
    }
    
    // RequestDelegate.kt
    override fun dispose() {
        job.cancel()
        if (target is LifecycleObserver) lifecycle.removeObserver(target)
        lifecycle.removeObserver(this)
    }
    
    在 Thread#interrupt() 方法中打断点
    • 复现:同一个 ImageView 在发起一个网络请求加载网络图片的时候,还未完成的时候又去发起另一个请求,就会使得之前那个请求调用 job.cancel() 取消协程,进而去中断之前的 block 里的操作
    • 另外:滑动屏幕使某些 view 不可见,就会触发 ViewTargetRequestManager#onViewDetachedFromWindow() 方法,也会对协程进行取消操作
    // ViewTargetRequestManager.kt
    @MainThread
    override fun onViewDetachedFromWindow(v: View) {
        currentRequest?.dispose()
    }
    
    • 最后,也就是说协程的取消就会导致 runInterruptible 中 block 里的代码被中断,也就是 runInterruptible 的实际用处。
    1. Coil 硬盘缓存的是原图吗?
      结论:是的。这点和 Glide 是有区别的

    2. Coil 返回的 Bitmap 是原尺寸的吗?
      结论:都可能。

    • 明确设置了 ImageRequest.Builder(this).size(Size.ORIGINAL) 会返回原始尺寸。
    • 使用了 ImageView 作为 target,且 ImageView 的 scaleType 为 CENTER 或者 MATRIX 会返回原始尺寸,因为这种情况下解析出来的 Size 也为 Size.ORIGINAL
    • 否则,会根据设定的 Size 和 Scale 计算出一个缩放值;然后再根据设定的 Precision 值对这个缩放值进行校正;最后再根据缩放值进行缩放。
    // 这里的 allowInexactSize 就是根据 precision 计算出来的
    // Only upscale the image if the options require an exact size.
    if (options.allowInexactSize) {
        scale = scale.coerceAtMost(1.0)
    }
    

    另外,对于 ImageView,一般不用去手动调用 ImageRequest#scale(scale: Scale) 方法,因为它会自动根据 ImageView 的 scaleType 来进行指定使用 Scale.FIT 还是 Scale.FILL

    /**
     * Set the scaling algorithm that will be used to fit/fill the image into the size provided
     * by [sizeResolver].
     *
     * NOTE: If [scale] is not set, it is automatically computed for [ImageView] targets.
     */
    fun scale(scale: Scale) = apply {
        this.scaleResolver = ScaleResolver(scale)
    }
    

    其具体的 ScaleResolver 为 ImageViewScaleResolver 类:

    internal class ImageViewScaleResolver(private val view: ImageView) : ScaleResolver {
    
        override fun scale(): Scale {
            val params = view.layoutParams
            if (params != null && (params.width == WRAP_CONTENT || params.height == WRAP_CONTENT)) {
                // Always use `Scale.FIT` if one or more dimensions are unbounded.
                return Scale.FIT
            } else {
                // 这里调用了 ImageView 的一个扩展属性,参见下方
                return view.scale
            }
        }
    
        override fun equals(other: Any?): Boolean {
            if (this === other) return true
            return other is ImageViewScaleResolver && view == other.view
        }
    
        override fun hashCode() = view.hashCode()
    }
    
    internal val ImageView.scale: Scale
        get() = when (scaleType) {
            FIT_START, FIT_CENTER, FIT_END, CENTER_INSIDE -> Scale.FIT
            else -> Scale.FILL
        }
    
    enum class Scale {
    
        /**
         * Fill the image in the view such that both dimensions (width and height) of the image will be
         * **equal to or larger than** the corresponding dimension of the view.
         */
        FILL,
    
        /**
         * Fit the image to the view so that both dimensions (width and height) of the image will be
         * **equal to or less than** the corresponding dimension of the view.
         *
         * Generally, this is the default value for functions that accept a [Scale].
         */
        FIT
    }
    

    这里,再看一下 BitmapFactoryDecoder#configureScale() 方法:

    private fun BitmapFactory.Options.configureScale(exifData: ExifData) {
        // Requests that request original size from a resource source need to be decoded with
        // respect to their intrinsic density.
        val metadata = source.metadata
        if (metadata is ResourceMetadata && options.size.isOriginal) {
            inSampleSize = 1
            inScaled = true
            inDensity = metadata.density
            inTargetDensity = options.context.resources.displayMetrics.densityDpi
            return
        }
    
        // This occurs if there was an error decoding the image's size.
        if (outWidth <= 0 || outHeight <= 0) {
            inSampleSize = 1
            inScaled = false
            return
        }
    
        // srcWidth and srcHeight are the original dimensions of the image after
        // EXIF transformations (but before sampling).
        // 这里解析出了图片原始的尺寸
        val srcWidth = if (exifData.isSwapped) outHeight else outWidth
        val srcHeight = if (exifData.isSwapped) outWidth else outHeight
    
        val (width, height) = options.size
        // 注意这里:
        // 1. 如果 width 是像素值,则直接使用这个像素值,否则使用原始图片的值,也就是 srcWidth
        // 2. height 同理
        // 3. 另外,请参见 ImageRequest#resolveSizeResolver() 方法,其明确指出了,
        //    如果 scaleType 为 CENTER 或者 MATRIX,则 size 为 Size.ORIGINAL,
        //    也就是说,会导致这里的 width 和 height 都不是具体的像素值,进而被赋值为了原始图片的宽高值
        val dstWidth = width.pxOrElse { srcWidth }
        val dstHeight = height.pxOrElse { srcHeight }
    
        // calculateInSampleSize 里面用了 Integer.highestOneBit(Int) 方法,这是为了获得 2 的整数次幂结果
        // Calculate the image's sample size.
        inSampleSize = DecodeUtils.calculateInSampleSize(
            srcWidth = srcWidth,
            srcHeight = srcHeight,
            dstWidth = dstWidth,
            dstHeight = dstHeight,
            scale = options.scale
        )
    
        // 上面计算出来的 inSampleSize 只是一个大概值,这里需要计算出一个 double 类型的精确缩放值
        // Calculate the image's density scaling multiple.
        var scale = DecodeUtils.computeSizeMultiplier(
            srcWidth = srcWidth / inSampleSize.toDouble(),
            srcHeight = srcHeight / inSampleSize.toDouble(),
            dstWidth = dstWidth.toDouble(),
            dstHeight = dstHeight.toDouble(),
            scale = options.scale
        )
    
        // Only upscale the image if the options require an exact size.
        if (options.allowInexactSize) {
            scale = scale.coerceAtMost(1.0)
        }
    
        inScaled = scale != 1.0
        if (inScaled) {
            if (scale > 1) {
                // Upscale
                inDensity = (Int.MAX_VALUE / scale).roundToInt()
                inTargetDensity = Int.MAX_VALUE
            } else {
                // Downscale
                inDensity = Int.MAX_VALUE
                inTargetDensity = (Int.MAX_VALUE * scale).roundToInt()
            }
        }
    }
    

    可以看到,根据 options.scaleoptions.allowInexactSize 去计算出了最终的一个 scale 值,进而去指定了 inDensityinTargetDensity 两个值,最终影响 Bitmap 的宽高。
    再者,从这里也可以看出如果 scaleType 为 CENTER 或者 MATRIX 时,是返回的原始尺寸,详见注释(并参考 ImageRequest#resolveSizeResolver() 方法)。

    private fun resolveSizeResolver(): SizeResolver {
        val target = target
        if (target is ViewTarget<*>) {
            // CENTER and MATRIX scale types should be decoded at the image's original size.
            val view = target.view
            if (view is ImageView && view.scaleType.let { it == CENTER || it == MATRIX }) {
                // 注意这里的 size 是 Size.ORIGINAL,而不是一个具体的像素值
                return SizeResolver(Size.ORIGINAL)
            } else {
                return ViewSizeResolver(view)
            }
        } else {
            // Fall back to the size of the display.
            return DisplaySizeResolver(context)
        }
    }
    
    1. 设置了 target 后 BitmapDrawable 是如何设置到 ImageView 上的?
      结论:在对 ImageViewTarge 的 drawable 赋值时设置上的。
    • RealImageLoader#onSuccess()
    private fun onSuccess(
        result: SuccessResult,
        target: Target?,
        eventListener: EventListener
    ) {
        val request = result.request
        val dataSource = result.dataSource
        logger?.log(TAG, Log.INFO) {
            "${dataSource.emoji} Successful (${dataSource.name}) - ${request.data}"
        }
        // 这里会调用 transition,后面花括号里的闭包代码是在没有 transition 时自行的代码
        transition(result, target, eventListener) { target?.onSuccess(result.drawable) }
        eventListener.onSuccess(request, result)
        request.listener?.onSuccess(request, result)
    }
    
    • RealImageLoader#transition()
    private inline fun transition(
        result: ImageResult,
        target: Target?,
        eventListener: EventListener,
        setDrawable: () -> Unit
    ) {
        if (target !is TransitionTarget) {
            // 执行闭包代码
            setDrawable()
            return
        }
    
        val transition = result.request.transitionFactory.create(target, result)
        if (transition is NoneTransition) {
            // 执行闭包代码
            setDrawable()
            return
        }
    
        eventListener.transitionStart(result.request, transition)
        // 根据具体的 transition 类型执行 transition() 方法
        transition.transition()
        eventListener.transitionEnd(result.request, transition)
    }
    
    • 以 CrossfadeTransition 为例,CrossfadeTransition#transition()
    override fun transition() {
        val drawable = CrossfadeDrawable(
            start = target.drawable,
            end = result.drawable,
            scale = result.request.scaleResolver.scale(),
            durationMillis = durationMillis,
            fadeStart = result !is SuccessResult || !result.isPlaceholderCached,
            preferExactIntrinsicSize = preferExactIntrinsicSize
        )
        when (result) {
            // 这里回调到了具体的 target 上去
            is SuccessResult -> target.onSuccess(drawable)
            is ErrorResult -> target.onError(drawable)
        }
    }
    
    • GenericViewTarget#onSuccess(),不管有没有 transition 都会执行到这儿(没有 transition 就会执行上面指出的闭包里的代码 transition(result, target, eventListener) { target?.onSuccess(result.drawable) }
    override fun onSuccess(result: Drawable) = updateDrawable(result)
    
    • GenericViewTarget#updateDrawable()
    private fun updateDrawable(drawable: Drawable?) {
        (this.drawable as? Animatable)?.stop()
        // 这里对具体的 target 的 drawable 进行了赋值
        this.drawable = drawable
        updateAnimation()
    }
    
    • ImageViewTarget.kt
    override var drawable: Drawable?
        get() = view.drawable
        set(value) = view.setImageDrawable(value)
    
    • 最终,通过 ImgageView#setImageDrawable(Drawable) 方法把图片给设置上了
    1. ExceptionCatchingSource 的作用是什么?
      结论:其实注释已经写得很清楚了,阻止 BitmapFactory#decodeStream() 吞没异常。当然,这里只能阻止 read 时发生的异常。
    /** Prevent [BitmapFactory.decodeStream] from swallowing [Exception]s. */
    private class ExceptionCatchingSource(delegate: Source) : ForwardingSource(delegate) {
    
        var exception: Exception? = null
            private set
    
        // BitmapFactory#decodeStream() 会调用这里的 read() 方法
        override fun read(sink: Buffer, byteCount: Long): Long {
            try {
                return super.read(sink, byteCount)
            } catch (e: Exception) {
                // 发生异常时记录异常,在我们自己的业务代码中需要主动判断 exception 是否为空
                exception = e
                // 把异常再抛给 BitmapFactory,但是它不会再往上抛,也就是吞没了
                throw e
            }
        }
    }
    
    1. BitmapFactoryDecoder 中的 parallelismLock 是干什么的?
      结论:这是协程里的信号量,控制最多可以允许多少个协程并行运行的。那每解码一张图片都单独实例化了一个 decoder,意思就是一个 decoder 对应一张图片,怎么存在多个解码并行呢?再看代码可以发现,每次实例化 decoder 时是传入了同一个 Semaphore 实例的,也就是说所有 BitmapFactoryDecoder 公用一个 Semaphore 实例,这样就限制了并发量了。
    class BitmapFactoryDecoder @JvmOverloads constructor(
        private val source: ImageSource,
        private val options: Options,
        private val parallelismLock: Semaphore = Semaphore(Int.MAX_VALUE)
    ) : Decoder {
        ...
    }
    
    class Factory @JvmOverloads constructor(
        maxParallelism: Int = DEFAULT_MAX_PARALLELISM
    ) : Decoder.Factory {
    
        private val parallelismLock = Semaphore(maxParallelism)
    
        override fun create(result: SourceResult, options: Options, imageLoader: ImageLoader): Decoder {
            // 这里每次传入的是同一个 Semaphore 实例
            return BitmapFactoryDecoder(result.source, options, parallelismLock)
        }
    
        override fun equals(other: Any?) = other is Factory
    
        override fun hashCode() = javaClass.hashCode()
    }
    
    1. HttpUriFetch#writeToDiskCache() 中的 fileSystem.write(editor.metadata) { CacheResponse(response).writeTo(this) } 代码调用了 write 方法,但是却与 write 的方法的签名不符是怎么回事?
    // 可以看到这里只传入了两个参数
    fileSystem.write(editor.metadata) {
        CacheResponse(response).writeTo(this)
    }
    

    结论:这是因为 okio 使用了 kotlin 支持多平台的特性,参考 Get started with Kotlin Multiplatform Mobile | Kotlin (kotlinlang.org)

    通过按住 ctrl 的同时鼠标左键点击 write,跳转到 write 方法内部,看到如下代码:

    @Throws(IOException::class)
    @JvmName("-write")
    // 注意这里有个 actual 修饰符
    actual inline fun <T> write(file: Path, mustCreate: Boolean, writerAction: BufferedSink.() -> T): T {
        return sink(file, mustCreate = mustCreate).buffer().use {
            it.writerAction()
        }
    }
    

    这里明明就需要三个参数,但是HttpUriFetch#writeToDiskCache()中调用 write 方法时只传了两个参数,这到底是怎么编译过的??表示很是纳闷!

    折腾了很久,刚开始还以为是 inline 有什么魔法,后来才注意到 write 方法前有个 actual 修饰符,立马明白了,这个 FileSystem 类是一个跨平台的类,于是查看 okio 的源码(参考:square/okio),才发现 expect 类里的第二个参数是有默认值的,如下:

    // 注意这里有个 expect 修饰符
    expect abstract class FileSystem() {
      @Throws(IOException::class)
      inline fun <T> write(
        file: Path,
        mustCreate: Boolean = false,
        writerAction: BufferedSink.() -> T
      ): T
    }
    

    也就是说,expect 里的默认值是可以传递到 actual 里的,跟着 kotlin 官方文档写了一个 demo,发现确实如此!参考 Get started with Kotlin Multiplatform Mobile | Kotlin (kotlinlang.org)

    Demo 如下:

    package com.example.kotlinmultiplatformsharedmodule
    
    expect fun hello(a: Int, b: String = "hello")
    
    package com.example.kotlinmultiplatformsharedmodule
    
    actual fun hello(a: Int, b: String) {
    
    }
    

    调用 hello 的代码为 hello(4),通过打断点来看实际情况:

    kotlin 跨平台默认值

    可以看到默认值为 hello,而这个默认值其实是定义在 expect 那里的。

    相关文章

      网友评论

          本文标题:Android 关于 Coil 源码阅读之部分疑问记录

          本文链接:https://www.haomeiwen.com/subject/edokjrtx.html