概述
在前面的文章里,我们详细地了解了一下 Coil
的图片加载流程,我们知道了 Coil
的图片处理主要是基于自身定义的以下三个装置来完成的:
- 数据源转换器
Mapper
- 资源获取器
Fetcher
- 数据流解码器
Decoder
这三个模块组合起来就能实现图片数据的获取与展示,但是作为一个图片加载框架,还有一个模块是必不可少的,那就是缓存模块。
组成
Coil
的缓存结构如下图所示:
磁盘缓存
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)
}
}
...
}
可以看到,全部是通过OKHttpClient
的CacheControl
来实现的。因此可以预见的是,当我们手动把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>
与其 “使用计数” 的对应关系,怎么叫 “使用计数” 呢?就是当一个bitmap
在Coil
内部被使用并且还没有显式释放时,使用计数反映到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
来维护 CacheKey
与Bitmap
的关系,从名字也可以猜出来,其中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()
}
}
}
}
LruCache
,strongMemoryCache
关于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
的特性有关,感兴趣的可以参看硬件位图相关介绍