Coil 加载图片闪烁/抖动问题分析

735 阅读2分钟
  • Coil 2.5.0
  • Kotlin 1.9.10

问题现象

mBinding.bannerHomeFgHome.load(url)

在加载图片时,如果多次调用以上代码可能会出现闪烁/抖动的情况,比如 Tab 频繁切换、接口回调频繁等。

问题原因

Coil 开始加载时会强制使用 placeholder 做占位图。

即使我们没有设置,它也会尝试通过一些途径获取一个,如果结果为 null,就会导致 ImageView 出现空白。这时候如果没有固定高度,则整个视图都会发生抖动。

RealImageLoader L169-172

// Set the placeholder on the target.
val placeholderBitmap = memoryCache?.get(request.placeholderMemoryCacheKey)?.bitmap
val placeholder = placeholderBitmap?.toDrawable(request.context) ?: request.placeholder
request.target?.onStart(placeholder)

GenericViewTarget L44-48

protected fun updateDrawable(drawable: Drawable?) {
    (this.drawable as? Animatable)?.stop()
    this.drawable = drawable
    updateAnimation()
}

解决方法

将当前 drawable 设置为 placeholder

mBinding.bannerHomeFgHome.load(url){
    placeholder(mBinding.bannerHomeFgHome.drawable)
}

placeholderMemoryCacheKey 的尝试

虽然 coil 首先会通过request.placeholderMemoryCacheKey 尝试从缓存中获取 bitmap,官方也有相关文章描述如何使用,但我尝试是实现不了的,至少在当前版本(2.5.0)是这样。

因为官方做法是通过ImageRequest获取memoryCacheKey,类似这样:

mBinding.bannerHomeFgHome.result?.request?.memoryCacheKey

但是这里的 memoryCacheKey 并不是最终 Coil 使用的 key,而是我们通过 load 方法传入的,因为我们仅仅填入了 data 参数,所以其他值当然一直为 null。

当然我们没填不代表 Coil 就不启用内存缓存和磁盘缓存,我们可以在 Coil 加载图片的源码 EngineInterceptor#intercept 中看到,cacheKey 实际上是通过 MemoryCacheService.newCacheKey 方法返回的,内部判断了如果memoryCacheKey为 null 的时候,会根据 data、Transform 等参数生成一个唯一 key。

如果我们能找到最终的这个 memoryCacheKey也可以使用它来实现 placeholder。

fun newCacheKey(
    request: ImageRequest,
    mappedData: Any,
    options: Options,
    eventListener: EventListener
): MemoryCache.Key? {
    // Fast path: an explicit memory cache key has been set.
    request.memoryCacheKey?.let { return it }
    // Slow path: create a new memory cache key.
    eventListener.keyStart(request, mappedData)
    val base = imageLoader.components.key(mappedData, options)
    eventListener.keyEnd(request, base)
    if (base == null) return null
    // Optimize for the typical case where there are no transformations or parameters.
    val transformations = request.transformations
    val parameterKeys = request.parameters.memoryCacheKeys()
    if (transformations.isEmpty() && parameterKeys.isEmpty()) {
        return MemoryCache.Key(base)
    }
    // Else, create a memory cache key with extras.
    val extras = parameterKeys.toMutableMap()
    if (transformations.isNotEmpty()) {
        request.transformations.forEachIndexedIndices { index, transformation ->
            extras[EXTRA_TRANSFORMATION_INDEX + index] = transformation.cacheKey
        }
        extras[EXTRA_TRANSFORMATION_SIZE] = options.size.toString()
    }
    return MemoryCache.Key(base, extras)
}