Coil源码解析(三)之缓存

2,038 阅读7分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第16天,点击查看活动详情

前言

前两篇Coil源码解析文章从比较高的视角分析了Coil的整个工作流程,旨在从整体观感上去了解和认识Coil。从这篇文章开始,陆续会就不同的细分方面去学习Coil。这篇文章主要探讨Coil的缓存用法以及具体实现。

三级缓存

在具体分析Coil源码之前,先来说说三级缓存的概念。以下顺序为从近到远(这里的远近是指与目的地的距离,假如把图片加载到ImageView,那目的地就是ImageView)。

内存缓存

内存缓存是离目的地最近的一级缓存,直接把资源放在内存里。例如需要同一个Bitmap需要重复加载到ImageView,这时候可以选择不释放这个Bitmap,即取即用,这就是内存缓存。

优点

  • 距离近,所以加载速度最快。

缺点

  • 内存容量小,容易造成内存吃紧。
  • 一般与应用的生命周期共存亡,应用停止后资源也会被清除。
  • 内存掉电不会保存。

磁盘缓存

磁盘缓存也称外存缓存,一般对应Android中的SD卡层级的缓存。例如你用手机拍了一张照片,不论你是退出了相机应用还是重启手机,过了两天还可以再看这张照片,因为它保存在SD卡中。

优点

  • 持久化保存,不会随着应用生命周期或者设备的断电而消失。

缺点

  • 距离比内存缓存远,加载速度不如内存缓存。
  • 更换设备后数据不复存在。

网络缓存

网络缓存就不必说了,资源放在网络服务器上,需要的时候进行网络请求获取资源。

优点

  • 不与设备本身硬件绑定,能做到换机数据缓存。

缺点

  • 距离最远,加载速度最慢。

总的来说,三级缓存加载速度逐层减慢,但持久化程度逐层增加。由于Coil最终一步就是从网络加载资源,所以可以不考虑网络缓存(或者说不叫缓存了)。只需要对内存缓存以及磁盘缓存进行分析。

用法

imageView.load(
    url,
    //Coil给Context提供了一个扩展属性获取全局单例的ImageLoader。
    context.imageLoader
    //newBuilder用于获取ImageLoader的构造器
    .newBuilder()
    //指定内存缓存策略
    .memoryCache {
        MemoryCache.Builder(context)
        //指定最大Size为当前可用内存的25%
        .maxSizePercent(0.25)
        //启用或停用对缓存资源的弱引用
        .weakReferencesEnabled(true)
        //启用或停用对缓存资源的强引用
        .strongReferencesEnabled(true)
        //指定最大Size为1MB
        .maxSizeBytes(1024 * 1024)
        //构建内存缓存策略
        .build()
    }
    //指定磁盘缓存策略
    .diskCache {
        DiskCache.Builder()
        //指定磁盘缓存的路径,没有默认值,必填。
        .directory(context.cacheDir.resolve("coil_cache"))
        //指定最大Size为当前可用磁盘空间的2%
        .maxSizePercent(0.02)
        //指定最大Size为10MB
        .maxSizeBytes(1024 * 1024 * 10)
        //指定清理逻辑执行的协程Dispatcher
        .cleanupDispatcher(Dispatchers.IO)
        //指定磁盘缓存的最大大小。如果设置了maxSizeBytes则忽略该设置。
        .maximumMaxSizeBytes(...)
        //指定磁盘缓存的最小大小。如果设置了maxSizeBytes则忽略该设置。
        .minimumMaxSizeBytes(...)
        //指定OKio的FileSystem
        .fileSystem(...)
        //构建磁盘缓存策略
        .build()
    }.build() //构建ImageLoader
)

从用法可以看出,缓存策略是保存在ImageLoader中的。

MemoryCache

MemoryCache是接口,只有唯一一个实现类RealMemoryCache,RealMemoryCache内部包含一个StrongMemoryCache以及一个WeakMemoryCache。WeakMemoryCache和StrongMemoryCache分别有两个实现类(EmptyxxxMemoryCache以及RealxxxMemoryCache)。类图如下:

classDiagram
class MemoryCache{
	<<interface>>
	+Int size
	+Int maxSize
	+Set~Key~ keys
	+get(Key key):Value
	+set(Key key,Value value)
	+remove(Key key):Boolean
	+clear()
	+trimMemory(Int level)
}
class RealMemoryCache{
	+StrongMemoryCache strongMemoryCache
	+WeakMemoryCache weakMemoryCache
}
class StrongMemoryCache{
	<<interface>>
}
class WeakMemoryCache{
	<<interface>>
}
class EmptyWeakMemoryCache
class EmptyStrongMemoryCache
class RealWeakMemoryCache
class RealStrongMemoryCache

MemoryCache <|.. RealMemoryCache
RealMemoryCache *-- WeakMemoryCache
RealMemoryCache *-- StrongMemoryCache
StrongMemoryCache <|.. EmptyStrongMemoryCache
StrongMemoryCache <|.. RealStrongMemoryCache
WeakMemoryCache <|.. EmptyWeakMemoryCache
WeakMemoryCache <|.. RealWeakMemoryCache

从名字可以看出,WeakMemoryCache以及StrongMemoryCache分别承担着弱引用内存缓存以及强引用内存缓存的功能。

咱们先来看看MemoryCache的构造器的build方法:

fun build(): MemoryCache {
    //判断是否启用弱引用
    val weakMemoryCache = if (weakReferencesEnabled) {
        RealWeakMemoryCache()
    } else {
        EmptyWeakMemoryCache()
    }
    //判断是否启用强引用
    val strongMemoryCache = if (strongReferencesEnabled) {
        val maxSize = if (maxSizePercent > 0) {
            calculateMemoryCacheSize(context, maxSizePercent)
        } else {
            maxSizeBytes
        }
        if (maxSize > 0) {
            RealStrongMemoryCache(maxSize, weakMemoryCache)
        } else {
            EmptyStrongMemoryCache(weakMemoryCache)
        }
    } else {
        EmptyStrongMemoryCache(weakMemoryCache)
    }
    return RealMemoryCache(strongMemoryCache, weakMemoryCache)
}

可以看到:只有在启用了弱引用才会构建一个RealWeakMemoryCache,否则构建EmptyWeakMemoryCache;只有启用了强引用并且maxSize大于零才会构建一个RealStrongMemoryCache,否则构建EmptyWeakMemoryCache。

顾名思义,只有RealxxxCache才有真正的缓存操作,而EmptyxxxCache则是空实现。

StrongMemoryCache

由于缓存的处理手法的关系,我们先讲StrongMemoryCache,慢慢就会引出了WeakMemoryCache了。

Strong代表引用类型里面的强引用,也就是大家最常用的引用方式。换句话说,StrongMemoryCache即是对Bitmap进行强引用的内存缓存。StrongMemoryCache拥有着与MemoryCache接口基本相同的方法,上面的类图上也有所罗列,但为了读者们方便阅读,这里再总结一下。

internal interface WeakMemoryCache {
    //key的集合
    val keys: Set<Key>
​
    //取出与key对应的value
    fun get(key: Key): Value?
​
    //将key与Bitmap及其相关信息绑定起来并储存
    fun set(key: Key, bitmap: Bitmap, extras: Map<String, Any>, size: Int)
​
    //将该key的value移除
    fun remove(key: Key): Boolean
​
    //清除所有缓存
    fun clearMemory()
​
    //根据level执行容量控制
    fun trimMemory(level: Int)
}

接下来就看看实际的实现类RealStrongMemoryCache。先看构造方法。

internal class RealStrongMemoryCache(
    maxSize: Int,
    private val weakMemoryCache: WeakMemoryCache
)

可以看到,RealStrongMemoryCache内部会持有一个WeakMemoryCache。至于为什么,卖个关子。

接下来就是最关键的地方:

private val cache = object : LruCache<Key, InternalValue>(maxSize) {
    override fun sizeOf(key: Key, value: InternalValue) = value.size
    override fun entryRemoved(
        evicted: Boolean,
        key: Key,
        oldValue: InternalValue,
        newValue: InternalValue?
        ) = weakMemoryCache.set(key, oldValue.bitmap, oldValue.extras, oldValue.size)
}

破案了,RealStrongMemoryCache的核心还是LruCache这个类。LRU(Least Recently Used)是一种缓存策略,总的来说就是末尾淘汰制——到了必须要淘汰某个对象的时候,就会淘汰最近一次使用理现在时间最远的那个对象。不了解的读者自行恶补!

重写了两个方法,其中重点是这个entryRemoved。当Lru缓存中的对象被移除的时候(不论是手动调用remove移除还是被末尾淘汰),这个entryRemoved就会被调用。而RealStrongMemoryCache的做法是将被移除的这个对象放进了WeakMemoryCache中。刚刚卖得关子也破案了,Coil的策略是,当一个缓存被强引用缓存移除时,不会直接丢弃,而是放入弱引用缓存中再进行一次挣扎。

还有一个值得注意的地方,就是trimMemory方法。

override fun trimMemory(level: Int) {
    if (level >= TRIM_MEMORY_BACKGROUND) {
        clearMemory()
    } else if (level in TRIM_MEMORY_RUNNING_LOW until TRIM_MEMORY_UI_HIDDEN) {
        cache.trimToSize(size / 2)
    }
}

会根据应用的onTrimMemory回调的level执行清除或者缩小缓存的一半大小。

再看看set方法:

override fun set(key: Key, bitmap: Bitmap, extras: Map<String, Any>) {
    val size = bitmap.allocationByteCountCompat
    if (size <= maxSize) {
        cache.put(key, InternalValue(bitmap, extras, size))
    } else {
        cache.remove(key)
        weakMemoryCache.set(key, bitmap, extras, size)
    }
}

假如想要放入StrongMemoryCache的Bitmap大于允许缓存的最大值,则不会放入StrongMemoryCache中,而是会将其放入WeakMemoryCache中并删掉StrongMemoryCache中的该key的缓存(如果有)。这是为了让大Bitmap也能一定程度上进行缓存。

WeakMemoryCache

WeakMemoryCache的实际实现类为RealWeakMemoryCache。RealWeakMemoryCache的核心是一个LinkedHashMap,以缓存Key为键,值是一个ArrayList。说实话笔者其实没搞懂为什么要用一个ArrayList而不是直接用一个弱引用的Bitmap。以下内容纯粹是叙述了,至于为什么要这么做笔者能力有限,实在想不出所以然。

internal val cache = LinkedHashMap<Key, ArrayList<InternalValue>>()

internal class InternalValue(
    //用于判断是否是相同的Bitmap
    val identityHashCode: Int,
    //Bitmap的弱引用
    val bitmap: WeakReference<Bitmap>,
    val extras: Map<String, Any>,
    //Bitmap的size
    val size: Int
)

set

我们来看看set方法:

override fun set(key: Key, bitmap: Bitmap, extras: Map<String, Any>, size: Int) {
	val values = cache.getOrPut(key) { arrayListOf() }

		run {
			val identityHashCode = bitmap.identityHashCode
			val newValue = InternalValue(identityHashCode, WeakReference(bitmap), extras, size)
			for (index in values.indices) {
				val value = values[index]
				if (size >= value.size) {
					if (value.identityHashCode == identityHashCode 
                        && value.bitmap.get() === bitmap) {
						values[index] = newValue
					} else {
						values.add(index, newValue)
					}
					return@run
				}
			}
			values += newValue
		}

		cleanUpIfNecessary()
}

以下是set的流程图

graph TB
开始 --> A[获取Bitmap的HashCode] --> B[构建新的InternalValue]
subgraph 获取key对应的ArrayList并遍历
B --> C{是否大于等于当前的size} --> |是| D{是否HashCode相同并Bitmap强相等} --> |是|E[取代当前的value]
D --> |否| H[将value插入当前位置]
end
H --> F
C --> |否| G[将value加到list最后面]
G --> F
E --> F[清理]
F --> 结束

get

再来看看get方法:

override fun get(key: Key): Value? {
	val values = cache[key] ?: return null

	val value = values.firstNotNullOfOrNullIndices { value ->
		value.bitmap.get()?.let { Value(it, value.extras) }
	}

	cleanUpIfNecessary()
	return value
}

get方法就相对简单了,首先找有没有跟key对应的ArrayList,没在则直接返回null,接下来从list中找到第一个Bitmap不为空的InternalValue并转成Value,最后清理并返回结果。

cleanupIfNecessary

细心的读者可能发现了,无论在set还是get的代码中,最后都会调用这个cleanupIfNecessary方法,从方法名猜测,这个方法是用来判断是否需要执行cleanup操作的。

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

果不其然,每次调用cleanUpIfNecessary都会让operationsSinceCleanUp加一,直到超过CLEAN_UP_INTERVAL(固定是10)就会执行cleanup操作。也就是说只要get和set加起来操作达到10次,就会触发一次cleanup。

既然如此,我们继续看看cleanup吧!

internal fun cleanUp() {
    //计数置零
    operationsSinceCleanUp = 0

    //获取迭代器进行迭代
    val iterator = cache.values.iterator()
    while (iterator.hasNext()) {
        val list = iterator.next()

        //list只有1个或0个参数,则直接判断第一个value的bitmap是否为空(弱引用被回收则为空)
        //据说这样操作比下面的通用操作快
        if (list.count() <= 1) {
            if (list.firstOrNull()?.bitmap?.get() == null) {
                //为空直接remove该list
                iterator.remove()
            }
        } else {
            //移除list中所有已经被回收的value
            list.removeIfIndices { it.bitmap.get() == null }

            //移除后list空了则把list也remove了
            if (list.isEmpty()) {
                iterator.remove()
            }
        }
    }
}

结言

至此Coil的缓存中的内存缓存已经解析完毕了,由于篇幅问题,磁盘缓存下一篇再继续解析!