Android LruCache与DiskLruCache

学习自

  • Android开发艺术探索

例行废话

在上一篇的 认识Bitmap 的博客,学会了如何更好地加载Bitmap,那么这一片文章呢,我们来学习一下如何缓存Bitmap。

当我们第一次从网络上加载图片的时候,这时候我们就已将将图片加载到了内存中了,然么我们第二次再加载同一个图片的时候,就完全没有必要再从网络上再加相同的图片了。当我们第一加载图片的时候直接将图片缓存到本地第二次加载图片的时候直接去本地取就行。这样不经可以节省用户的流量并且还要比直接从网络上读取要更款速(毕竟网络有带宽的限制)。 并且我们可以在本地和内存中分开存储(内存中只放使用频率最高的),因为知己从内存中加载要更快与从本地加载,可以让用户体验更加友好。

Lru算法

当然,因为不管是内存还是手机的存储都不是特别打的,所以我们并不能无节制的缓存图片,尤其是内存中的缓存,如果因为内存中缓存过大影响到APP的运行效率了那可就是因噎废食了。 所以说缓存的大小必须是严格限制的。

现在,如何管理缓存是一个比较重要的问题,最简单的方法是当缓存满了以后按照时间排序,然后移除最早缓存的图片,当然这种方法并不完美,现在普遍采用的算法是 Lru算法,即,最早加载最少使用的缓存被先移除

万幸,我们并不用去自己写相应的内存和本地磁盘的缓存算法, 相应的类已经被提供了。 Android SDK提供了一种 内存的Lru缓存类LruCache. 而磁盘缓存的类并没有在SDK中提供,我们需要手动下载 DiskLruCache.

通过这两个类我们就可以实现图片的缓存了,并且我们还可以通过将这两个类封装一下形成自己的 ImageLoader 框架。

LruCache

下面是LruCache类的声明,可以发现LRUCache 类 和Map集合非常类似( ???? 因为里面就是封装了Map集合)。并且里面的源码非常简单,并没有什么阅读困难,大家也可以去与源码中看看Lru算法具体是怎么实现的。

public class LruCache<K, V> {
  //...
}

使用LruCache

LruCache的初始化

//以kb为单位获取虚拟机的内存的1/8
private val mCacheMaxSize = (Runtime.getRuntime().maxMemory() / 8 / 1024).toInt()
private val mLruCache = object : LruCache<String, Bitmap>(mCacheMaxSize) {
    /**
     * 此方法用来获取缓存的Bitmap的大小,的缓存大小是以kg为单位的
     * 所以我们这里也要转为以kg为单位
     * */
    override fun sizeOf(key: String?, value: Bitmap?): Int {
        return value!!.rowBytes * value.height / 1024
    }
}

上面的代码中我们重写了 LruCache的 sizeOf 方法,在这个方法中计算Bitmap的大小并最终转换为 kb 为单位。 我们有时候也会重写 LruCache 的 entryRemoved 方法,如有有必要的时候用来回收一下资源,此方法会在当缓存的对象被移除的时候会调用此方法。如果你想重新设置 缓存区的大小 请调用 resize 方法。

fun onClick(view: View) {
    val resId = R.drawable.wallbackground
    val wallBackgroundBitmap = mLruCache.get(resId.toString())
    //如果缓存区中存在指定的Bitmap,那么就直接读取
    if (wallBackgroundBitmap != null) {
        iv.setImageBitmap(wallBackgroundBitmap)
    } else {
        //缓存区中没有指定的Bitmap,从资源中读取,然后放入缓存区中
        val bitmap = BitmapFactory.decodeResource(this.resources, resId)
        mLruCache.put(resId.toString(), bitmap)
        iv.setImageBitmap(bitmap)
    }
}

DiskLruCache

DiskLruCache 并不是官方开发但是得到了官方文档的推荐,大家可以直接从大佬的 Github 上下载或直接通过Gradle引用。

implementation 'com.jakewharton:disklrucache:2.0.2'

DiskLruCache的初始化

class MainActivity : AppCompatActivity() {
    //存储bitmap缓存的路径
    private val BITMAP_CACHE_PATH = "bitmaps"
    //磁盘缓存区域的大小
    private val DISK_CACHE_SIZE: Long = 1024 * 1024 * 50

    lateinit var mDiskLruCache: DiskLruCache

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        //初始化DiskLruCache
        val cacheDirectoryFile = File(externalCacheDir, BITMAP_CACHE_PATH)
        //如果目录不存在创建目录
        if (!cacheDirectoryFile.exists()) {
            cacheDirectoryFile.mkdir()
        }
        this.mDiskLruCache = DiskLruCache.open(cacheDirectoryFile, 1, 1, DISK_CACHE_SIZE)
    }
}

DiskLruCache.open方法的参数说明.

  1. directory 表示缓存的路径
  2. appVersion 代表APP的版本,这个数值发生变化的时候,缓存会被清空,一般来说缓存都是通用的,所以这个值为1就行
  3. valueCount 代表每个key对应的元素的数量,传1就好
  4. maxSize 最大的缓存,当超过这个数值的时候,将会删除一些缓存

DiskLruCache

class MainActivity : AppCompatActivity() {
    //存储bitmap缓存的路径
    private val BITMAP_CACHE_PATH = "bitmaps"
    //磁盘缓存区域的大小
    private val DISK_CACHE_SIZE: Long = 1024 * 1024 * 50

    lateinit var mDiskLruCache: DiskLruCache

    private val IMG_URL = "http://www.shycoder.cn/usr/themes/handsome/usr/img/my/head.png"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        //初始化DiskLruCache
        val cacheDirectoryFile = File(externalCacheDir, BITMAP_CACHE_PATH)
        //如果目录不存在创建目录
        if (!cacheDirectoryFile.exists()) {
            cacheDirectoryFile.mkdir()
        }
        this.mDiskLruCache = DiskLruCache.open(cacheDirectoryFile, 1, 1, DISK_CACHE_SIZE)
    }

    fun onClick(view: View) {
      //开启线程
        thread {
            //取URL的MD5值做为Key
            val urlMD5 = getMD5(IMG_URL)
            //根据Key获取缓存
            val snapshot = mDiskLruCache.get(urlMD5)
            //没有缓存就去网络下载
            if (snapshot == null) {
                //向磁盘缓存中放入数据
                val editor = mDiskLruCache.edit(urlMD5)
                //因为我们设置的 每个Key都对应1一个缓存,所以这里直接传0就行了,下同。
                val outputStream = editor.newOutputStream(0)
                if (downloadImgFromUrl(IMG_URL, outputStream)) {
                    //提交
                    editor.commit()
                } else {
                    //放弃操作
                    editor.abort()
                }
                mDiskLruCache.flush()
            } else {
                val inputStream = snapshot.getInputStream(0)
                //在这里你可以通过 BitmapFactory.decodeFileDescriptor进行一下图片压缩,这里我就稍微偷一下懒(手动笑哭)
                val bitmap = BitmapFactory.decodeStream(inputStream)
                inputStream.safeClose()
                runOnUiThread {
                    ivHead.setImageBitmap(bitmap)
                }
            }

        }
    }

    /**
     * 从网络上下载图片并写入到指定的流中
     * @param imgUrl 图片的URL地址
     * @param outputStream 要写入的输出流
     * */
    private fun downloadImgFromUrl(imgUrl: String, outputStream: OutputStream): Boolean {
        var bufferedOutputStream: BufferedOutputStream? = null
        var bufferedInputStream: BufferedInputStream? = null
        try {
            bufferedInputStream = BufferedInputStream(URL(imgUrl).openStream())
            bufferedOutputStream = BufferedOutputStream(outputStream)
            var len = bufferedInputStream.read()
            while (len != -1) {
                bufferedOutputStream.write(len)
                len = bufferedInputStream.read()
            }
            return true
        } catch (ex: Exception) {
            ex.printStackTrace()
        } finally {
            //safeClose是自己封装的一个扩展方法
            bufferedInputStream.safeClose()
            bufferedOutputStream.safeClose()
        }
        return false
    }

    //获取MD5值
    private fun getMD5(url: String): String {
        val messageDigest = MessageDigest.getInstance("MD5")
        val buffer = url.toByteArray()
        messageDigest.update(buffer)
        return BigInteger(1, messageDigest.digest()).toString(16)
    }

}

OK,上面的代码是一个DiskLruCache的使用的小Demo,当没有缓存的时候就去网络下载,否则就直接加载,上面的代码的注释也非常详细这里就不在讲解了。

总结

本章学习了Android的缓存策略,这种思想不仅可以进行图片缓存什么类型的数据或者文件都可以进行缓存,关于文中提到了 Bitmap压缩可以参考我这篇博文 Andriod-认识Bitmap 。 在下一篇文章呢,我们将编写一个我们自己的ImageLoader。

除非特殊声明否则,本文章均属 鲁迅认识的那只猹 原创,未经许可禁止转载,否则将保留追究法律责任的权利。

如果损害了您的相关权益,请及时联系我,我将妥善处理。