上一篇记录了Bitmap的(高效)加载,那么这一篇就记录Cache。
对于网络上的图片,第一次使用就需要从网络上去下载下来,但如果每次都去从网络上下载,那就非常浪费流量了,所以需要做缓存。另外的添加了缓存也要做好删除缓存,毕竟有些过久地图片或是很少会再用到的图片,就需要删掉了,释放空间。
这里用到的缓存算法是LRU(Least Recently Used),最近最少使用算法。在该算法的基础上有衍生出两种缓存,LruCache和DiskLruCache,前者用于实现内存缓存,后者用于实现存储设备的缓存。所以这里就是将这两者结合,实现了一个ImageLoader(图片加载器),这里用到了三级缓存(网络缓存,磁盘缓存和内存缓存)。
1. LruCache
引用原文的话:
LruCache是一个泛型类,它内部采用一个LinkedHashMap以强引用的方式存储外界的缓存对象。
另外LruCache是线程安全的。
短短两句话涉及了不少概念。
LinkedHashMap如果去查找Lru算法的话,基本都是在它的基础上实现的;
从构造方法里可以看出它是个泛型类:
然后关于强引用:
之所以说线程安全,因为在LruCache里的添加,删除,获取都是有同步锁机制的。
1.1 LruCache的使用
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);//单位KB
//设定缓存的容量为总容量的1/8
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
//返回bitmap大小的计算
return value.getRowBytes() * value.getHeight() / 1024;
}
};
总的来说就是提供缓存的总容量大小然后重写sizeof方法。这里缓存的总容量大小为当前进程的可用内存的1/8,sizeof里就返回的是bitmap对象的大小计算。这两个的单位应该一致,所以这里都除以1024。
然后是获取的方法:
mMemoryCache.get(key);
添加的方法:
mMemoryCache.put(key, bitmap);
2. DiskLruCache
2.1 DiskLruCache的使用
2.1.1 引用
用DiskLruCache来做磁盘缓存,可以通过依赖来获取:
compile 'com.jakewharton:disklrucache:2.0.2'
2.1.2 创建
DiskLruCache需要通过open方法来创建,而不是普通的构造方法:
//利用open方法来创建,第一个参数是存储路径,第二个参数是版本号,
//第三个参数是单个节点对应的数据的个数,第四个参数是缓存的总大小
mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
2.1.3 添加
与前面的LruCache一样,DiskLruCache也是用到了LinkedHashMap,那么在LruCache里的操作都用到了“key”这个东西,这里也同样用到了key。由于在这个ImageLoader里他们都是操作同一个东西,所以当然是一样的。作者在这里是用图片的url来作key,但需要作一些转换,用url的md5值来作为key,主要是防止url里可能有些特殊字符导致出错:
/**
* 将图片的url转换成key,这里采用url的md5的值作为key
* @param url
* @return
*/
private String hashKeyFromUrl(String url) {
String cacheKey;
try {
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
messageDigest.update(url.getBytes());
cacheKey = bytesToHexString(messageDigest.digest());
} catch (NoSuchAlgorithmException e) {
cacheKey = String.valueOf(url.hashCode());
}
return cacheKey;
}
private String bytesToHexString(byte[] bytes) {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(0xFF & bytes[i]);
if (hex.length() == 1) {
stringBuilder.append('0');
}
stringBuilder.append(hex);
}
return stringBuilder.toString();
}
那么DiskLruCache的缓存添加是通过Editor来完成的,通过edit()方法和key就可以获取到这个Editor对象,进而可以获得文件输出流。
String key = hashKeyFromUrl(url);
//使用Editor进行缓存添加的操作
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null) {
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
}
那么怎么操作这个文件输出流呢?或者说它的数据从哪来呢?
其实它的数据是从它的更上一级,网络缓存那里来的,我们通过url去做网络请求的时候会获得一个输入流,然后我们把输入流写到这个输出流里,那么它就有数据了。
private boolean downloadUrlToStream(String urlString, OutputStream outputStream) {
HttpURLConnection urlConnection = null;
BufferedOutputStream out = null;
BufferedInputStream in = null;
try {
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();
}
try {
in.close();
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return false;
}
现在我们通过网络请求将流写给了磁盘缓存,但需要通过进一步的确认操作来真正的写入。即commit()方法,所以把前面的一块代码修改下:
if (editor != null) {
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
if (downloadUrlToStream(url, outputStream)) {
editor.commit();
} else {
editor.abort();
}
mDiskLruCache.flush();
}
2.1.4 获取
DiskLruCache对缓存的获取则是通过它的Snapshot对象,与上面的类似,它是通过get()方法和key得到的,然后可以进一步的得到文件输入流,那拿到了文件输入流我们通过上一篇的BitmapFactory提供的解码方法就可以得到一个Bitmap对象了。
String key = hashKeyFromUrl(url);
DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
if (snapshot != null) {
//获取该图片的文件输入流
FileInputStream fileInputStream = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
//获取该文件输入流的文件描述
FileDescriptor fileDescriptor = fileInputStream.getFD();
//通过文件描述得到想要的bitmap
bitmap = mImageResizer.decodeSampleBitmapFromFileDescriptor(fileDescriptor, reqWidth, reqHeight);
}
这里对文件输入流的处理是用了FileDescriptor, 也就是对应于decodeFileDescriptor()方法。为什么这里要用这个方法?
作者给的解释是FileInputStream是一种有序的文件流,两次decodeStream调用影响文件流的位置属性,在第二次decodeStream的时候会得到null,那这里我做了测试,确实在第二次的时候会得到null。