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

2022-03-25  本文已影响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

结论:

@Throws(InterruptedIOException::class)
internal fun waitForIo() {
    try {
        wait()
    } catch (_: InterruptedException) {
        Thread.currentThread().interrupt() // Retain interrupted status.
        throw InterruptedIOException()
    }
}
// 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() 方法中打断点
// ViewTargetRequestManager.kt
@MainThread
override fun onViewDetachedFromWindow(v: View) {
    currentRequest?.dispose()
}
  1. Coil 硬盘缓存的是原图吗?
    结论:是的。这点和 Glide 是有区别的

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

// 这里的 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 赋值时设置上的。
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)
}
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)
}
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)
    }
}
override fun onSuccess(result: Drawable) = updateDrawable(result)
private fun updateDrawable(drawable: Drawable?) {
    (this.drawable as? Animatable)?.stop()
    // 这里对具体的 target 的 drawable 进行了赋值
    this.drawable = drawable
    updateAnimation()
}
override var drawable: Drawable?
    get() = view.drawable
    set(value) = view.setImageDrawable(value)
  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 那里的。

上一篇 下一篇

猜你喜欢

热点阅读