携手创作,共同成长!这是我参与「掘金日新计划 · 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的缓存中的内存缓存已经解析完毕了,由于篇幅问题,磁盘缓存下一篇再继续解析!