12.2 Android中的缓存策略
缓存策略在Android中有着广泛的使用场景,尤其在图片加载这个场景下,缓存策略就变得更为重要。
当程序第一次从网络加载图片后,就将其缓存到存储设备上,这样下次使用这张图片就不用再从网络上获取了,这样就为用户节省了流量。很多时候为了提高应用的用户体验,往往还会把图片在内存中再缓存一份,这样当应用打算从网络上请求一张图片时,程序会首先从内存中获取,如果内存中没有那就从存储设备中去获取,如果存储设备中也没有,那就从网络上下载这张图片。因为从内存中加载图片比从存储设备中加载图片要快,所以这样既提高了程序的效率又为用户节约了不必要的流量开销。上述的缓存策略不仅仅适用于图片,也适用于其他文件类型。
说到缓存策略,其实并没有统一的标准。一般来说,缓存策略主要包含缓存的添加,获取和删除这三类操作。不管是内存缓存还是存储设备缓存,它们的缓存大小都是有限制的,因为内存和诸如SD卡之类的存储设备都是有容量限制的,因此在使用缓存时总是要为缓存指定一个最大的容量。如果当缓存容量满了,但是程序还需要向其添加缓存,这个时候该怎么办呢?这就需要删除一些旧的缓存并添加新的缓存,如何定义缓存的新旧这就是一种策略,不同的策略就对应着不同的缓存算法,比如可以简单地根据文件的最后修改时间来定义缓存的新旧,当缓存满时就将最后修改时间较早的缓存移除,这就是一种缓存算法,但是这种算法并不算很完美。
目前常用的缓存算法是LRU,LRU是近期最少使用算法,它的核心思想是当缓存满时,会优先淘汰那些近期最少使用的缓存对象。采用LRU算法的缓存有两种:LruCache和DiskLruCache,LruCache用于实现内存缓存,而DiskLruCache则充当了存储设备缓存,通过这两者的完美结合,就可以很方便地实现一个具有很高实用价值的ImageLoader。
12.2.1 LruCache
LruCache是Android3.1所提供的一个缓存类,通过support-v4兼容包可以兼容到早期的Android版本。LruCache是一个泛型类,它内部采用一个LinkedHashMap以强引用的方式存储外界的缓存对象,其提供了get和put方法来完成缓存的获取和添加操作,当缓存满时,LruCache会移除较早使用的缓存对象,然后再添加新的缓存对象。这里要明白强引用,软引用,弱引用的区别。
- 强引用:直接的对象引用
- 软引用:当一个对象只有软引用存在时,系统内存不足时此对象会被gc回收。
- 弱引用:当一个对象只有弱引用存在时,此对象会随时被gc回收。
另外LruCache是线程安全的,下面是LruCache的定义:
public class LruCache<K, V> {
private final LinkedHashMap<K, V> map;
...
}
下面的代码展示了LruCache的典型的初始化过程:
int maxMemory = (int) Runtime.getRuntime().maxMemory();
int cacheSize = maxMemory / 8;
LruCache mMemoryCache = new LruCache<String,Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
}
};
在上面的代码中,只需要提供缓存的总容量大小并重写sizeOf方法即可。sizeOf方法的作用是计算缓存对象的大小,这里大小的单位需要和总容量的单位一致。对于上面的示例代码来说,总容量的大小为当前进程的可用内存的1/8,单位为KB,而sizeOf方法则完成了Bitmap对象的大小计算。很明显,之所以除以1024也是为了将其单位转换为KB。一些特殊情况下,还需要重写LruCache的entryRemoved方法,LruCache移除旧缓存时会调用entryRemoved方法,因此可以在entryRemoved中完成一些资源回收工作(如果需要的话)。
除了LruCache的创建以外,还有缓存的获取和添加,这也很简单,从LruCache中获取一个缓存对象,如下所示:
mMemoryCache.get(key);
向LruCache中添加一个缓存对象
mMemoryCache.put(key,bitmap);
LruCache还支持删除操作,通过remove方法即可删除一个指定的缓存对象。可以看到LruCache的实现以及使用都非常简单,虽然简单,但是仍然不影响它具有强大的功能,从Android3.1开始,LruCache就已经是Android源码的一部分了。
12.2.2 DiskLruCache
DiskLruCache用于实现存储设备缓存,即磁盘缓存,它通过将缓存对象写入文件系统从而实现缓存的效果。DiskLruCache得到了Android官方文档的推荐,但它不属于AndroidSDK的一部分,如需使用可直接copy这个类到项目中去。
DiskLruCache地址
或者可以在Jake大神的Github上找到
下面分别从DiskLruCache的创建、缓存查找和缓存添加这三个方面来介绍DiskLruCache的使用方式
1. DiskLruCache的创建
DiskLruCache并不能通过构造方法来创建,它提供了open方法用于创建自身。
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
open方法有四个参数,其中第一个参数表示磁盘缓存在文件系统中的存储路径。缓存路径可以选择SD卡上的缓存目录,具体是指/sdcard/Android/data/package_name/cache目录,其中package_name表示当前应用的包名,当应用被卸载后,此目录会一并被删除。当然也可以选择SD卡上的其他指定目录,还可以选择data下的当前应用的目录,具体可根据需要灵活设定。这里给出一个建议:如果应用卸载后就希望删除缓存文件,那么就选择SD卡上的缓存目录,如果希望保留缓存数据那就应该选择SD卡上的其他特定目录。
第二个参数表示应用的版本号,一般设为1即可。当版本号发生改变时DiskLruCache会清空之前所有的缓存文件,而这个特性在实际开发中作用并不大,很多情况下即使应用的版本号发生了改变缓存文件却仍然是有效的,因此这个参数设为1比较好。
第三个参数表示单个节点所对应的数据的个数,一般设为1即可,第四个参数表示缓存的总大小,比如50MB,当缓存大小超出这个设定值以后,DiskLruCache会清除一些缓存,从而保证总大小不大于这个设定值。
下面是一个典型的DiskLruCache的创建过程:
public DiskLruCache mDiskLruCache;
private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50; // 50MB
File diskCacheDir = getDiskCacheDir(mContext,"bitmap");
if (!diskCacheDir.exists()) {
diskCacheDir.mkdirs();
}
try {
mDiskLruCache = DiskLruCache.open(diskCacheDir,1,1,DISK_CACHE_SIZE);
} catch (IOException e) {
e.printStackTrace();
}
2. DiskLruCache的缓存添加
DiskLruCache的缓存添加的操作是通过Editor完成的,Editor表示一个缓存对象的编辑对象。这里仍然以图片缓存举例,首先需要获取图片url所对应的key,然后根据key就可以通过edit()来获取Editor对象,如果这个缓存正在被编辑,那么edit()会返回null,即DiskLruCache不允许同时编辑一个缓存对象。之所以要把url转换成key,是因为图片的url中很可能有特殊字符,这将影响url在Android中直接使用,一般采用url的MD5值作为key。
如下所示:
private String hashKeyFormUrl(String url){
String cacheKey;
try {
MessageDigest mDigest = MessageDigest.getInstance("MD5");
mDigest.update(url.getBytes());
cacheKey = bytesToHexString(mDigest.digest());
} catch (NoSuchAlgorithmException e) {
cacheKey = String.valueOf(url.hashCode());
}
return cacheKey;
}
private String bytesToHexString(byte[] digest) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < digest.length; i++) {
String hex = Integer.toHexString(0xFF&digest[i]);
if(hex.length() == 1){
sb.append('0');
}
sb.append(hex);
}
return sb.toString();
}
将图片的url转成key以后,就可以获取Editor对象了。对于这个key来说,如果当前不存在其他Editor对象,那么edit()就会返回一个新的Editor对象,通过它就可以得到一个文件输出流。需要注意的是,由于前面在DiskLruCache的open方法中设置了一个节点只能有一个数据,因此下面的DISK_CACHE_INDEX常量直接设为0即可。
int DISK_CACHE_INDEX = 0;
String key = hashKeyFormUrl(url);
try {
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if(editor != null){
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
}
} catch (IOException e) {
e.printStackTrace();
}
有了文件输出流,接下来要怎么做?其实是这样的,当从网络下载图片时,图片就可以通过这个文件输出流写入到文件系统上,这个过程的实现如下所示:
public boolean downloadUrlToStream(String urlString, OutputStream outputStream) throws IOException {
HttpURLConnection urlConnection = null;
BufferedOutputStream out = null;
BufferedInputStream in = null;
try {
final URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
in = new BufferedInputStream(urlConnection.getInputStream(),IO_BUFFER_SIZE);
out = new BufferedOutputStream(outputStream,IO_BUFFER_SIZE);
int b;
while ((b = in.read()) != -1) {
out.write(b);
}
return true;
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
in.close();
out.close();
}
return false;
}
经过上面的步骤,其实并没有真正地将图片写入文件系统,还必须通过Editor的commit()来提交写入操作,如果图片下载过程发生了异常,那么还可以通过Editor的abort()来回退整个操作。
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if(editor != null){
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
if(downloadUrlToStream(url,outputStream)){
editor.commit();
}else {
editor.abort();
}
}
经过上面的几个步骤,图片已经被正确地写入到文件系统了,接下来图片获取的操作,就不需要请求网络了。
3. DiskLruCache的缓存查找
和缓存的添加过程类似,缓存查找过程也需要将url转换为key,然后通过DiskLruCache的get方法得到一个Snapshot对象,接着再通过Snapshot对象即可得到缓存的文件输入流,有了文件输出流,自然就可以得到Bitmap对象了。为了避免加载图片过程中导致的OOM问题,一般不建议直接加载原始图片。前面已经介绍了通过BitmapFactory.Options对象来加载一张缩放后的图片,但是那种方法对FileInputStream的缩放存在问题,原因是FileInputStream是一种有序的文件流,而两次decodeStream调用影响了文件流的位置属性,导致了第二次decodeStream时得到的是null。为了解决这个问题,可以通过文件流来得到它所对应的文件描述符,然后再通过BitmapFactory.decodeFileDescriptor方法来加载一张缩放后的图片,这个过程的实现如下所示:
Bitmap bitmap = null;
String keys = hashKeyFormUrl(url);
try {
DiskLruCache.Snapshot snapshot = mDiskLruCache.get(keys);
if(snapshot != null){
FileInputStream fileInputStream = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
FileDescriptor fileDescriptor = fileInputStream.getFD();
bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor,reqWidth,reqHeight);
if(bitmap != null){
addBitmapToMemoryCache(keys,bitmap);
}
}
上面介绍了DiskLruCache的创建,缓存的添加和查找过程,除此之外,DiskLruCache还提供了remove,delete等方法用于磁盘缓存的删除操作。