背景
- coil-kt/coil: Image loading for Android backed by Kotlin Coroutines. (github.com)
- 时间:2022.03.24
- commit id: 0e9dfff70b1c89710f6e9a31cae9a32578118757
疑问
- 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.readEnabled
为 false
,在 HttpUriFetcher#newRequest()
方法中会设置 Request 的 CacheControl 为 FORCE_CACHE,所以是不会真正发起网络请求的,因而不会出现 NetworkOnMainThreadException
- 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)
}
}
}
- 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
}
}
结论:见代码中的注释
- 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 的实际用处。
-
Coil 硬盘缓存的是原图吗?
结论:是的。这点和 Glide 是有区别的 -
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.scale
和 options.allowInexactSize
去计算出了最终的一个 scale 值,进而去指定了 inDensity
和 inTargetDensity
两个值,最终影响 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)
}
}
- 设置了 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) 方法把图片给设置上了
- 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
}
}
}
- 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()
}
-
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)
,通过打断点来看实际情况:
可以看到默认值为 hello
,而这个默认值其实是定义在 expect 那里的。
网友评论