Andoid Disk Cache

上一篇文章中我们介绍了缓存的基本原理以及如何借助于LruCache类来实现一级缓存,也就是在内存中的缓存。但是由于内存空间的局限,我们不可能在一级缓存中放入太多的东西,那么聪明的你也一定想到了,没错,同电脑缓存一样,我们还可以记住外部存储在给应用做二级缓存,也就是缓存在磁盘之上。这样一来,我们就可以缓存更多的数据,但是二级缓存同样也有自身的局限性,比如性能相对于一级缓存要慢很多,另外对于缓存的网络上的内容还要考虑其实效性。

实现

不同于内存缓存,Android SDK中的并没有提供一个现成的disk的缓存类,但是Square的Jake Wharton提供了一个DiskLRUCache:JakeWharton/DiskLruCache,并且已被Google认证并且使用于Android项目中。顾名思义,该方案同样是机遇LRU的替换策略。所以对于我们开发者来说,直接拿过来就可以了。

你可以简单地将代码拷贝到你的android工程当中,如果是采用Gradle,可以这样引用:

compile 'com.jakewharton:disklrucache:2.0.2'

使用

open

如果想要使用DiskLruCache,首先我们要通过其open方法创建一个DiskLruCache的实例,接口定义如下:

public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)

该方法需要四个参数,

  • directory: 数据的缓存地址,一般存在应用的package-specific目录 如有疑问,可以参考这里
  • appVersion: 当前应用程序的版本号
  • valueCount: 指定同一个key可以对应多少个缓存文件,基本都是传1
  • maxSize: 指定最多可以缓存多少字节的数据

缓存地址前面已经说过了,通常都会存放在 /sdcard/Android/data/your.package.name/cache 这个路径下面,关于如何获取该路径,可以参考我的另外一篇文章

关于应用程序版本号这个参数,需要注意的是,每当版本号改变,缓存路径下存储的所有数据都会被清除掉,所以在传入这个参数时需要慎重,除非你觉得需要更新cache,否则该参数不要该拜年。

因此,open()方法就可以这样写:

DiskLruCache mDiskLruCache = null;
int VERSION_CODE = 1;
long int MAX_SIZE = 10 * 1024 * 1024; //10M
try {
    File cacheDir = getDiskCacheDir(context, "bitmap");//自己定义的获取缓存目录函数
    if (!cacheDir.exists()) {
        cacheDir.mkdirs();
    }
    mDiskLruCache = DiskLruCache.open(cacheDir, VERSION_CODE, 1, MAX_SIZE);
} catch (IOException e) {
    e.printStackTrace();
}

写入缓存

写入的操作是借助DiskLruCache.Editor这个类完成的。类似地,这个类也是不能new的,需要调用DiskLruCache的edit()方法来获取实例,接口如下所示:

public Editor edit(String key) throws IOException

可以看到,edit()方法接收一个参数key,这个key将会成为缓存文件的文件名,并且必须要和图片的URL是一一对应的。那么怎样才能让key和图片的URL能够一一对应呢?直接使用URL来作为key?不太合适,因为图片URL中可能包含一些特殊字符,这些字符有可能在命名文件时是不合法的。其实最简单的做法就是将图片的URL进行MD5编码,编码后的字符串肯定是唯一的,并且只会包含0-F这样的字符,完全符合文件的命名规则

那么我们就写一个方法用来将字符串进行MD5编码,代码如下所示:

public String hashKeyForDisk(String key) {
    String cacheKey;
    try {
        final MessageDigest mDigest = MessageDigest.getInstance("MD5");
        mDigest.update(key.getBytes());
        cacheKey = bytesToHexString(mDigest.digest());
    } catch (NoSuchAlgorithmException e) {
        cacheKey = String.valueOf(key.hashCode());
    }
    return cacheKey;
}

private String bytesToHexString(byte[] bytes) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < bytes.length; i++) {
        String hex = Integer.toHexString(0xFF & bytes[i]);
        if (hex.length() == 1) {
            sb.append('0');
        }
        sb.append(hex);
    }
    return sb.toString();
}

代码很简单,现在我们只需要调用一下hashKeyForDisk()方法,并把图片的URL传入到这个方法中,就可以得到对应的key了。
因此,现在就可以这样写来得到一个DiskLruCache.Editor的实例:

String imageUrl = "http://www.someweb.com/sample.jpg";
String key = hashKeyForDisk(imageUrl);
DiskLruCache.Editor editor = mDiskLruCache.edit(key);

读取缓存

缓存已经写入成功之后,接下来我们就该学习一下如何读取了。读取的方法要比写入简单一些,主要是借助DiskLruCache的get()方法实现的,接口如下所示:

public synchronized Snapshot get(String key) throws IOException

很明显,get()方法要求传入一个key来获取到相应的缓存数据,而这个key毫无疑问就是将图片URL进行MD5编码后的值了,因此读取缓存数据的代码就可以这样写:

String imageUrl = "http://www.someweb.com/sample.jpg";
String key = hashKeyForDisk(imageUrl);
DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);

很奇怪的是,这里获取到的是一个DiskLruCache.Snapshot对象,这个对象我们该怎么利用呢?很简单,只需要调用它的getInputStream()方法就可以得到缓存文件的输入流了。同样地,getInputStream()方法也需要传一个index参数,这里传入0就好。有了文件的输入流之后,想要把缓存图片显示到界面上就轻而易举了。所以,一段完整的读取缓存,并将图片加载到界面上的代码如下所示:

try {
    String imageUrl = "http://www.someweb.com/sample.jpg";
    String key = hashKeyForDisk(imageUrl);
    DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
    if (snapShot != null) {
        InputStream is = snapShot.getInputStream(0);
        Bitmap bitmap = BitmapFactory.decodeStream(is);
        mImage.setImageBitmap(bitmap);
    }
} catch (IOException e) {
    e.printStackTrace();
}

我们使用了BitmapFactory的decodeStream()方法将文件流解析成Bitmap对象,然后把它设置到ImageView当中

移除缓存

移除缓存主要是借助DiskLruCache的remove()方法实现的,接口如下所示:

public synchronized boolean remove(String key) throws IOException

看个例子:

try {
    String imageUrl = "http://www.someweb.com/sample.jpg";  
    String key = hashKeyForDisk(imageUrl);  
    mDiskLruCache.remove(key);
} catch (IOException e) {
    e.printStackTrace();
}

后记

在了解了两级缓存的基本原理以及使用方法之后,接下来会通过一个具体的例子来演示如何在具体的项目中来使用二级缓存。

Reference

Displaying Bitmaps Efficiently