在实际的开发项目中,尤其是内容项目,涉及到图片资源的展示,例如列表中展示、视频资源的封面…,图片往往是从服务端下发,端上加载做渲染,那么我们是否每次加载图片都需要从服务端请求获取图片资源,这就涉及到了图片资源的缓存问题,像业内比较主流的Glide、Coil,内部都有自己的图片缓存策略,那么我们自己也可以手写一个图片缓存策略框架
图片缓存框架
- 1 准备工作 -- 资源封装
- 2 活动缓存 -- 弱引用
- 3 内存缓存 -- LruCache
- 4 磁盘缓存 -- DiskLruCache
1 准备工作 – 资源封装
在此之前,需要先做一些准备工作,像从服务端返回的都是图片的URL(统一资源定位符),例如
https://i0.hdslb.com/bfs/article/ed71fd02c03429137f19373a942696d228567a28.jpg@750w_750h_progressive.webp
这代表资源在网络中的唯一标识,通过这个URL可以拿到图片加载,当然为了安全起见,通常需要对URL做一次sha256加密,最终拿到的key给存储起来
class Key(val url: String) {
private var key:String? = null
//初始化操作
init {
this.key = Tools.sha256(url)
}
fun getKey():String?{
return key
}
fun setKey(url: String){
this.key = url
}
}
与key对应的是图片,也就是bitmap对象,需要对其进行封装,判断当前Bitmap是否在被使用,如果被使用的话,不能被回收,也就意味着不需要从网络加载
/**
* bitmap封装
*/
class BitmapCache {
//引用计数,类似于LruCache
private var count = 0
private var key:String? = null
private var bitmap: Bitmap? = null
fun bitmapInUse(){
//判断bitmap是否为空
if(bitmap == null){
throw IllegalArgumentException("bitmap must not be empty")
}
if(bitmap!!.isRecycled){
//判断bitmap资源是否已经被释放
return
}
count++
}
fun bitmapNotUse(){
if(count-- <= 0){
//该资源已经被释放
mListener?.invoke(key!!,this)
}
}
fun recycleBitmap(){
if(count > 0){
Log.d(TAG,"当前bitmap正在被使用,不能被释放")
return
}
if(bitmap?.isRecycled!!){
Log.d(TAG,"当前bitmap已经被释放")
return
}
bitmap!!.recycle()
//资源回收
System.gc()
}
//对key的操作
fun setKey(key: String){
this.key = key
}
fun getKey():String?{
return key
}
private var mListener:((String,BitmapCache)->Unit)? = null
fun setOnBitmapNotUseListener(listener:(String,BitmapCache)->Unit){
this.mListener = listener
}
companion object{
val instance:BitmapCache by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
BitmapCache()
}
private const val TAG = "BitmapCache"
}
}
2 活动缓存 – 弱引用
活动缓存是用来管理正在使用的图片,这里会使用到弱引用,这个是一个技术点,需要介绍一下
/**
* 用于做资源的统一管理
*/
class ActiveCache {
private var queue: ReferenceQueue<BitmapCache>? = null
private var thread: Thread? = null
//是否关闭当前线程
private var isCloseCurrentThread: Boolean = false
//是否是手动移除
private var isShutdownRemove = false
//通过map存储对象
private val map: HashMap<Key, WeakReference<BitmapCache>> by lazy {
HashMap()
}
//往活动缓存添加
fun addValue(key: Key, cache: BitmapCache, block: (String, BitmapCache) -> Unit) {
cache.setOnBitmapNotUseListener(block)
map[key] = BitmapCacheWeakReference(cache, getQueue(), key)
}
//移除缓存 手动移除
fun removeValue(key: Key): WeakReference<BitmapCache>? {
isShutdownRemove = true
val cache = map.remove(key)
isShutdownRemove = false
if (null == cache) {
return cache
}
return null
}
fun closeThread() {
isCloseCurrentThread = true
if (!map.isNullOrEmpty()) {
map.clear()
System.gc()
}
}
inner class BitmapCacheWeakReference(
private val cache: BitmapCache,
private val referenceQueue: ReferenceQueue<BitmapCache>,
val key: Key
) : WeakReference<BitmapCache>(cache, referenceQueue) {
//只有通过这个方式,才能监听到对象的回收和移除
}
private fun getQueue(): ReferenceQueue<BitmapCache> {
if (queue == null) {
queue = ReferenceQueue()
//通过开启线程,死循环的方式,来判断被回收的对象
thread = Thread(Runnable {
while (!isCloseCurrentThread) {
//所有注册的弱引用对象,都会被登记在这个ReferenceQueue队列中 --- 自动移除
val value = queue!!.remove() as BitmapCacheWeakReference
//如果是手动移除的,就不需要走这个链路
if (!isShutdownRemove) {
if (!map.isNullOrEmpty()) {
//当弱引用被回收之后,需要从map中移除
Log.e(TAG, "$value 从BitmapCacheWeakReference队列中移除了")
map.remove(value.key)
}
}
}
})
thread!!.start()
}
return queue!!
}
companion object {
private const val TAG = "ActiveCache"
}
}
这里我需要对弱引用 做一些讲解,在活动缓存中,采用弱引用修饰Bitmap,目的就是为了gc能够回收当前对象,那么为了能够监听到对象被移除,采用了ReferenceQueue的监听机制
public WeakReference(T referent) {
super(referent);
}
public WeakReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
WeakReference中提供了2个构造方法,一个带queue,另一个不带queue,其中queue存在的意义在于,我们可以轮询监听这个queue;当我们创建一个弱引用对象之后,gc移除弱引用对象的时候,这个对象会被注册到ReferenceQueue,我们可以调用remove方法来从这个队列中移除被回收的对象
3 内存缓存 – LruCache
内存缓存是真正在做资源的收集和回收,尤其是在大量的图片加载时的复用和回收,如果对这些图片资源做统一的管理,就需要内存缓存来做这个事
当内存缓存中的图片被使用的时候,就上提回到活动缓存中,活动缓存中的图片被移除之后,放在内存缓存中
这里有一个问题,为什么要使用活动缓存,只用内存缓存是不是也能解决问题?
因为内存缓存采用的是LRU,它是有一个最大上限的max_size,当达到这个上限的时候,会移除最近用的最少的资源,这样其实是不安全的,为什么不安全?
首先当图片加载资源图片的时候,如果加载的正好是最近使用最少的一张图片,而这个时候,LRU的缓存达到了上限,内部的算法就会移除这个图片内存,这个时候,图片加载就会报错!崩溃!
那么再有就是,我不要内存缓存了,我只要活动缓存是不是也可以?
其实多加一层缓存,就是为了能够避免重复的网络请求;因为弱引用本身就容易被回收,如果资源被回收之后,不能回到内存缓存中,下次再需要这个资源的时候,还需要重新从网络加载请求,这样就造成了资源浪费,这个时候,直接从内存缓存中取出,加到活动缓存中去,更合理
/**
* 内存缓存
*/
class MemoryCache(
maxSize: Int
) : LruCache<Key, BitmapCache>(maxSize) {
//是否手动移除
private var isShutdownRemove = false
/**
* @param key
* 添加缓存
*/
fun addValue(key: Key, value: BitmapCache) {
put(key, value)
}
/**
* 手动移除
*/
fun removeValue(key: Key?) : BitmapCache{
isShutdownRemove = true
val value = remove(key)
isShutdownRemove = false
return value
}
/**
* 元素被移除 --- 自动移除
* 1 当有重复的key
* 2 元素超限,超过max_size
*/
override fun entryRemoved(
evicted: Boolean,
key: Key?,
oldValue: BitmapCache?,
newValue: BitmapCache?
) {
super.entryRemoved(evicted, key, oldValue, newValue)
if(listener != null && !isShutdownRemove){
listener!!.invoke(key!!, oldValue!!)
}
}
/**
* 每一个元素的大小
*/
override fun sizeOf(key: Key?, value: BitmapCache?): Int {
return value!!.getBitmap()!!.allocationByteCount
}
private var listener: ((Key, BitmapCache) -> Unit)? = null
fun setOnLruCacheMemoryCacheRemoveListener(listener: (Key, BitmapCache) -> Unit) {
this.listener = listener
}
companion object {
private var instance:MemoryCache? = null
fun getInstance(maxSize: Int):MemoryCache{
if(instance == null){
synchronized(MemoryCache::class.java){
if(instance == null){
instance = MemoryCache(maxSize)
}
}
}
return instance!!
}
}
}
4 磁盘缓存 – DiskLruCache
磁盘缓存是缓存的最后一重,如果在磁盘缓存中没有找到,就需要进行网络请求;磁盘缓存采用的是DiskLruCache,像爱奇艺在关闭网络之后,依然能够加载出之前浏览过的图片,就是采用了磁盘缓存,这里先不做过多介绍了,后续会把这个缓存策略加上,其实跟内存缓存是一个道理。