Coil 源码分析 二. 缓存原理分析

1,134 阅读4分钟

概述

在前面的文章里,我们详细地了解了一下 Coil 的图片加载流程,我们知道了 Coil 的图片处理主要是基于自身定义的以下三个装置来完成的:

  • 数据源转换器 Mapper
  • 资源获取器 Fetcher
  • 数据流解码器 Decoder

这三个模块组合起来就能实现图片数据的获取与展示,但是作为一个图片加载框架,还有一个模块是必不可少的,那就是缓存模块。

组成

Coil的缓存结构如下图所示:

image.png

磁盘缓存

Coil的磁盘缓存是由OKHttp来实现的。

RealImageLoader被构造出来的时候默认会生成一个OkHttpClient的实例:

/**
 * Create a new [ImageLoader] instance.
 */
fun build(): ImageLoader {
    val memoryCache = memoryCache ?: buildDefaultMemoryCache()
    return RealImageLoader(
        ...
        callFactory = callFactory ?: buildDefaultCallFactory()
        ...
    )
}

private fun buildDefaultCallFactory() = lazyCallFactory {
    OkHttpClient.Builder()
        .cache(CoilUtils.createDefaultCache(applicationContext))
        .build()
}

在这里,Coil内部所使用的OKHttpClient将会沿用这一 cache 设置。 在HttpFetcher内部,我们可以看到他的磁盘缓存控制策略:

override suspend fun fetch(
    pool: BitmapPool,
    data: T,
    size: Size,
    options: Options
): FetchResult {
    val url = data.toHttpUrl()
    val request = Request.Builder().url(url).headers(options.headers)

    val networkRead = options.networkCachePolicy.readEnabled
    val diskRead = options.diskCachePolicy.readEnabled
    when {
        !networkRead && diskRead -> {
            request.cacheControl(CacheControl.FORCE_CACHE)
        }
        networkRead && !diskRead -> if (options.diskCachePolicy.writeEnabled) {
            request.cacheControl(CacheControl.FORCE_NETWORK)
        } else {
            request.cacheControl(CACHE_CONTROL_FORCE_NETWORK_NO_CACHE)
        }
        !networkRead && !diskRead -> {
            // This causes the request to fail with a 504 Unsatisfiable
            Request.request.cacheControl(CACHE_CONTROL_NO_NETWORK_NO_CACHE)
        }
    }

    ...
}

可以看到,全部是通过OKHttpClientCacheControl来实现的。因此可以预见的是,当我们手动把Coil的网络缓存与磁盘缓存都关闭的话,会导致请求返回504错误。

内存缓存

Coil最主要的缓存逻辑集中在内存缓存,围绕着Bitmap来展开,主要概括为三个方面:

  • LruCache
  • BitmapPool
  • WeakReference

回到刚才RealImageLoader初始化的地方,有这么一句:

val memoryCache = memoryCache ?: buildDefaultMemoryCache()

buildDefaultMemoryCache又做了啥呢?

private fun buildDefaultMemoryCache(): RealMemoryCache {
    val availableMemorySize = Utils.calculateAvailableMemorySize(applicationContext, availableMemoryPercentage)
    val bitmapPoolPercentage = if (bitmapPoolingEnabled) bitmapPoolPercentage else 0.0
    val bitmapPoolSize = (bitmapPoolPercentage * availableMemorySize).toInt()
    val memoryCacheSize = (availableMemorySize - bitmapPoolSize).toInt()

    val bitmapPool = if (bitmapPoolSize == 0) {
        EmptyBitmapPool()
    } else {
        RealBitmapPool(bitmapPoolSize, logger = logger)
    }
    val weakMemoryCache = if (trackWeakReferences) {
        RealWeakMemoryCache(logger)
    } else {
        EmptyWeakMemoryCache
    }
    val referenceCounter = if (bitmapPoolingEnabled) {
        RealBitmapReferenceCounter(weakMemoryCache, bitmapPool, logger)
    } else {
        EmptyBitmapReferenceCounter
    }
    val strongMemoryCache = StrongMemoryCache(weakMemoryCache, referenceCounter, memoryCacheSize, logger)
    return RealMemoryCache(strongMemoryCache, weakMemoryCache, referenceCounter, bitmapPool)
}

可以看出,这里是初始化了四个对象:

  • strongMemoryCache
  • weakMemoryCache
  • referenceCounter
  • bitmapPool 其中 referenceCounter主要是作为bitmapPool的入池操作的判断者的角色来出现的,为什么这么说呢?因为referenceCounter内部维护了一个WeakReference<Bitmap>与其 “使用计数” 的对应关系,怎么叫 “使用计数” 呢?就是当一个 bitmapCoil 内部被使用并且还没有显式释放时,使用计数反映到referenceCounter会加一。当Coil内部的某个bitmap被使用完毕并在接下来不会被使用,使用计数反映到referenceCounter会减一。当使用计数为0时,会触发 bitmapPool对当前的bitmap进行入池操作:
@Synchronized
override fun decrement(bitmap: Bitmap): Boolean {
    val key = bitmap.identityHashCode
    val value = getValueOrNull(key, bitmap) ?: run {
        logger?.log(TAG, Log.VERBOSE) { "DECREMENT: [$key, UNKNOWN, UNKNOWN]" }
        return false
    }
    value.count--
    logger?.log(TAG, Log.VERBOSE) { "DECREMENT: [$key, ${value.count}, ${value.isValid}]" }

    // If the bitmap is valid and its count reaches 0, remove it
    // from the WeakMemoryCache and add it to the BitmapPool.
    val removed = value.count <= 0 && value.isValid
    if (removed) {
        values.remove(key)
        weakMemoryCache.remove(bitmap)
        // Add the bitmap to the pool on the next frame.
        MAIN_HANDLER.post { bitmapPool.put(bitmap) }
    }

    cleanUpIfNecessary()
    return removed
}

可以看到同时,weakMemoryCache会移除掉对这个bitmap的引用。

WeakMemoryCache

WeakMemoryCache的唯一实现类是RealWeakMemoryCache

RealWeakMemoryCache内部维护了一个 HashMap来维护 CacheKeyBitmap的关系,从名字也可以猜出来,其中Bitmap是通过弱引用的方式来持有的。

internal class RealWeakMemoryCache(private val logger: Logger?) : WeakMemoryCache {
    @VisibleForTesting internal val cache = hashMapOf<Key, ArrayList<WeakValue>>()
    @VisibleForTesting internal var operationsSinceCleanUp = 0
}

当然WeakMemoryCache即使缓存的是bitmap的弱引用,他也有一个缓存上限,这个主要是由变量operationsSinceCleanUp来实现,每当外部对WeakMemoryCache进行了读写操作,operationsSinceCleanUp就会自增,当其值超过10,便会进行清理操作:

private fun cleanUpIfNecessary() {
    if (operationsSinceCleanUp++ >= CLEAN_UP_INTERVAL) {
        cleanUp()
    }
}

/** Remove any dereferenced bitmaps from the cache. */
@VisibleForTesting
internal fun cleanUp() {
    operationsSinceCleanUp = 0

    // Remove all the values whose references have been collected.
    val iterator = cache.values.iterator()
    while (iterator.hasNext()) {
        val list = iterator.next()

        if (list.count() <= 1) {
            // Typically, the list will only contain 1 item. Handle this case in an optimal way here.
            if (list.firstOrNull()?.bitmap?.get() == null) {
                iterator.remove()
            }
        } else {
            // Iterate over the list of values and delete individual entries that have been collected.
            list.removeIfIndices { it.bitmap.get() == null }

            if (list.isEmpty()) {
                iterator.remove()
            }
        }
    }
}

LruCachestrongMemoryCache

关于LruCache,这里有一篇详细的文章来讲解其发展与具体实现。这里主要来看Coil中的应用。

/** A [StrongMemoryCache] implementation backed by an [LruCache]. */
private class RealStrongMemoryCache(
    private val weakMemoryCache: WeakMemoryCache,
    private val referenceCounter: BitmapReferenceCounter,
    maxSize: Int,
    private val logger: Logger?
) : StrongMemoryCache {

    private val cache = object : LruCache<Key, InternalValue>(maxSize) {
        override fun entryRemoved(
            evicted: Boolean,
            key: Key,
            oldValue: InternalValue,
            newValue: InternalValue?
        ) {
            val isPooled = referenceCounter.decrement(oldValue.bitmap)
            if (!isPooled) {
                // Add the bitmap to the WeakMemoryCache if it wasn't just added to the BitmapPool.
                weakMemoryCache.set(key, oldValue.bitmap, oldValue.isSampled, oldValue.size)
            }
        }

        override fun sizeOf(key: Key, value: InternalValue) = value.size
    }

StrongMemoryCache的实现类RealStrongMemoryCache中,维护了一个LruCache来对Coil内部的bitmap进行缓存,当LruCache中发生元素回收,删除,以及当存入StrongMemoryCache的元素大小超过最大限制时,会将该 bitmap存入上文中的WeakMemoryCache:

@Synchronized
override fun set(key: Key, bitmap: Bitmap, isSampled: Boolean) {
    // If the bitmap is too big for the cache, don't even attempt to store it. Doing so will cause
    // the cache to be cleared. Instead just evict an existing element with the same key if it exists.
    val size = bitmap.allocationByteCountCompat
    if (size > maxSize) {
        val previous = cache.remove(key)
        if (previous == null) {
            // If previous != null, the value was already added to the weak memory cache in LruCache.entryRemoved.
            weakMemoryCache.set(key, bitmap, isSampled, size)
        }
        return
    }

    referenceCounter.increment(bitmap)
    cache.put(key, InternalValue(bitmap, isSampled, size))
}

BitmapPool

BitmapPool中的元素,是由BitmapReferenceCounter确定没有“使用计数”时存入的,他的获取分布在 Coil的各个角落,如进行图片解码的 decoder 中,进行图片变换的Transformation中。

class BlurTransformation @JvmOverloads constructor(
    private val context: Context,
    private val radius: Float = DEFAULT_RADIUS,
    private val sampling: Float = DEFAULT_SAMPLING
) : Transformation {

    override fun key(): String = "${BlurTransformation::class.java.name}-$radius-$sampling"

    override suspend fun transform(pool: BitmapPool, input: Bitmap, size: Size): Bitmap {
        val paint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG)

        val scaledWidth = (input.width / sampling).toInt()
        val scaledHeight = (input.height / sampling).toInt()
        val output = pool.get(scaledWidth, scaledHeight, input.safeConfig)
        ...

        return output
    }
}

bitmapPool在进行元素写入时对池的大小进行判断,这个时候可以很安全的进行recycle操作。

注意点

当使用硬件 bitmap时,Coil的内存缓存机制可能会失效:

if (value != null && isCachedValueValid(memoryCacheKey, value, request, size)) {
    return SuccessResult(
        drawable = value.bitmap.toDrawable(context),
        request = request,
        metadata = Metadata(
            memoryCacheKey = memoryCacheKey,
            isSampled = value.isSampled,
            dataSource = DataSource.MEMORY_CACHE,
            isPlaceholderMemoryCacheKeyPresent = chain.cached != null
        )
    )
}
/** Return true if [cacheValue] satisfies the [request]. */
@VisibleForTesting
internal fun isCachedValueValid(
    cacheKey: MemoryCache.Key?,
    cacheValue: RealMemoryCache.Value,
    request: ImageRequest,
    size: Size
): Boolean {
    // Ensure the size of the cached bitmap is valid for the request.
    if (!isSizeValid(cacheKey, cacheValue, request, size)) {
        return false
    }

    // Ensure we don't return a hardware bitmap if the request doesn't allow it.
    if (!requestService.isConfigValidForHardware(request, cacheValue.bitmap.safeConfig)) {
        logger?.log(TAG, Log.DEBUG) {
            "${request.data}: Cached bitmap is hardware-backed, which is incompatible with the request."
        }
        return false
    }

    // Else, the cached drawable is valid and we can short circuit the request.
    return true
}

这个与硬件bitmap的特性有关,感兴趣的可以参看硬件位图相关介绍