在实际的开发项目中,尤其是内容项目,涉及到图片资源的展示,例如列表中展示、视频资源的封面…,图片往往是从服务端下发,端上加载做渲染,那么我们是否每次加载图片都需要从服务端请求获取图片资源,这就涉及到了图片资源的缓存问题,像业内比较主流的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

内存缓存是真正在做资源的收集和回收,尤其是在大量的图片加载时的复用和回收,如果对这些图片资源做统一的管理,就需要内存缓存来做这个事

当内存缓存中的图片被使用的时候,就上提回到活动缓存中,活动缓存中的图片被移除之后,放在内存缓存中

图片站 缓存redis 图片缓存框架_android


这里有一个问题,为什么要使用活动缓存,只用内存缓存是不是也能解决问题?

因为内存缓存采用的是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,像爱奇艺在关闭网络之后,依然能够加载出之前浏览过的图片,就是采用了磁盘缓存,这里先不做过多介绍了,后续会把这个缓存策略加上,其实跟内存缓存是一个道理。