Android进阶知识(二十六):Android中的缓存策略
缓存策略在Android中有着很广泛的使用场景,尤其在图片加载的情景下,缓存策略更为重要。在图片加载的情景下,缓存的目的在于提高程序的效率,同时解决不必要的流量开销的问题。
缓存策略没有统一的标准,其主要包括缓存的添加、获取和删除三类操作。那缓存为何需要删除呢?
关键在于不管是内存缓存还是存储设备缓存,它们的缓存大小都是有限制的,当缓存容量已满,但是程序还需要添加缓存,这就需要删除旧的缓存添加新的缓存,而如何定义缓存的新旧就是一种策略,不同的策略对应着不同的缓存算法。
常用的一种缓存算法为LRU,LRU是近期最少使用算法,核心思想为当缓存已满时,会优先淘汰那些近期最少使用的缓存对象。
采用LRU算法的缓存有两种:LruCache(实现内存缓存)和DiskLruCache(存储设备缓存)。
一、LruCache
LruCache是Android 3.1所提供的一个泛型缓存类,通过support-v4兼容包可以兼容到早期版本。LruCache内部采用一个LinkedHashMap以强引用的方式存储外界的缓存对象,其提供了get和put方法来完成缓存的获取和添加操作,当缓存已满时,LruCache会移除较早使用的缓存对象,然后添加新的缓存对象。
关于强引用、软引用和弱引用的区别见下表。
引用 | 描述 |
---|---|
强引用 | 直接的对象引用。 |
软引用 | 当一个对象只有软引用存在时,系统内存不足时此对象会被gc回收。 |
弱引用 | 当一个对象只有弱引用存在时,此对象会随时被gc回收。 |
另外,LruCache是线程安全的。LruCache的定义如下。
public class LruCache<K, V> {
private final LinkedHashMap<K, V> map;
...
}
以图片缓存为例子,LruCache的典型初始化代码如下。LruChache的初始化只需要提供缓存的总容量大小以及重写sizeOf方法即可,其中sizeOf方法作用是计算缓存对象的大小。
// 当前进程可用内存
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// 缓存总容量大小
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
}
};
特殊情况下需要重写LruCache的entryRemoved方法,LruCache移除旧缓存时会调用该方法,可以在该方法中完成一些资源回收工作。LruCache的缓存添加、获取以及删除都比较简单。
mMemoryCache.put(key, bitmap);
mMemoryCache.get(key);
mMemoryCache.remove(key);
二、DiskLruCache
DiskLruCache用于实现存储设备缓存,即磁盘缓存,它通过将缓存对象写入文件系统从而实现缓存效果。DiskLruCache不属于Android SDK的一部分,可以从如下网站获取源码。
https://android.googlesource.com/platform/libcore/+/android-4.1.1_r1/luni/src/main/java/libcore/io/DiskLruCache.java
值得一提的是,DiskLruCache源码获取后不能直接在Android中使用,需要稍微修改编译错误。下面笔者将介绍DiskLruCache的创建、缓存查找以及缓存添加。
- DiskLruCache的创建
DiskLruCache并不能通过构造方法来创建,它提供了open方法用于创建自身,如下所示。
public static DiskLruCache open(File directory, int appVersion,
int valueCount, long maxSize)
其中,open方法的四个参数具体描述如下表。
参数 | 描述 |
---|---|
File directory | 表示磁盘缓存在文件系统中的存储路径。路径可以选择SD卡上的缓存目录(/sdcard/Android/data/package_name/cache),当应用卸载后,此目录会被一并删除。 |
int appVersion | 表示应用的版本号,当版本号发生改变时候DiskLruCache会清空之前所有的缓存文件,由于实际应用意义不大,因此一般设置为1 |
int valueCount | 表示单个节点所对应的数据的个数,一般设定为1 |
long maxSize | 表示缓存的总大小,当缓存大小超出这个值,DiskLruCache会清除一些缓存以保证总大小不大于该值 |
典型的DiskLruCache的创建代码如下所示。
private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50; // 50MB
File diskCacheDir = getDiskCacheDir(mContext, "bitmap");
if (!diskCacheDir.exists()) {
diskCacheDir.mkdirs();
}
mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
- DiskLruCache的缓存添加
DiskLruCache的缓存添加操作是通过Editor完成的,Editor表示一个缓存对象的编辑对象。缓存的添加分为4步,以下以图片为例子。
1) 创建key
由于图片的url可能有特殊字符,这将影响url在Android中的使用,因此需要通过hashKeyFormUrl方法将图片url转换成md5值的key。
String key = hashKeyFormUrl(url);
2) 根据key获取Editor对象,并获取文件输出流
对于当前key来说,如果当前不存在其他Editor对象,那么editor()返回一个新的Editor对象,如果这个缓存正在被编辑,那么editor()返回null。DiskLruCache不允许同时编辑一个缓存对象。
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null) {
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
}
其中由于前面设置了DiskLruCache的open方法设置了一个节点只有一个数据,因此这里DISK_CACHE_INDEX设置为0即可。
3) 将数据通过文件输出流写入文件系统
有了文件输入流之后,当从网络下载图片时,通过该输出流将图片写入文件系统。简略代码如下所示。
out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);
int b;
while ((b = in.read()) != null) {
// in为网络下载图片的输入流
out.write(b);
}
4) 通过Editor的commit()提交写入操作
通过文件输出流写入文件系统,并没有真正地将图片写入,需要通过Editor的commit()提交写入操作。如果图片下载过程发生异常,那么可以通过Editor的abort()方法回退整个操作。
// downloadUrlToStream方法用于下载图片并写入文件输出流
// 返回true下载正常,false下载异常
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
if (downloadUrlToStream(url, outputStream)) {
editor.commit();
} else {
editor.abort();
}
mDiskLruCache.flush();
- DiskLruCache的缓存查找
缓存查找过程过程为:首先根据key通过DiskLruCache的get方法得到一个Snapshot对象,接着再通过Snapshot对象获得缓存的文件输出流,从而得到Bitmap对象。
Bitmap bitmap = null;
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 = mImageResizer.decodeSampledBitmapFromFileDescriptor(
fileDescriptor, reqWidth, reqHeight);
if (bitmap != null) {
addBitmapToMemoryCache(key, bitmap);
}
}
如上代码,为了避免加载图片导致OOM,通过BitmapFactory.Options对图片进行了缩放——方法decodeSampleBitmapFromFileDescriptor(高效加载Bitmap在笔者的前一篇笔记:Android进阶知识(二十五):Bitmap简介及其高效加载中介绍了)。
另外,FileInputStream是一种有序的文件流,高效加载需要两次调用decodeStream,这会影响文件流的位置属性,导致第二次decodeStream得到的是null。为了解决这个问题,可以通过文件流来得到所对应的文件描述符,再进行缩放加载。这也是笔者在前一篇笔记中提到的decodeStream进行高效加载比较特殊的原因。
至此DiskLruCache的创建、缓存的添加以及查找过程就介绍完了,除此之外,DiskLruCache还提供了remove、delete等方法用于磁盘缓存的删除操作,这里就不多介绍了。
三、ImageLoader实现思想
笔者在Android进阶知识(二十五):Bitmap简介及其高效加载中介绍了Bitmap的高效加载方式,以及前面介绍了LruCache和DiskLruCache的使用,有了这些知识那么对于我们常见的图片加载第三方库ImageLoader,读者就可以进行实现了。
一个优秀的ImageLoader应该具备如下的功能:
- 图片的同步加载
- 图片的异步加载
- 图片压缩
- 内存缓存
- 磁盘缓存
- 网络拉取
内存缓存和磁盘缓存是ImageLoader的核心,通过这两级缓存极大地提高了程序的效率并且有效地降低了对用户所造成的流量消耗,只有当这两级缓存不可用才需要从网络拉取。
一个需要注意的是,ListView或者GridView中View的复用有一个缺点:假设itemA对应ImageViewA,如果进行快速滑动,那么itemB复用了ImageViewA,由于网络加载图片尚未下载好A图片,导致显示时候itemB显示A图片。
为了解决由于View复用所导致的列表错位问题,在给ImageView设置图片之前可以检测它的url是否改变,改变不设置。
对于列表卡顿的优化,解决思想是不要在主线程中做太耗时的操作即可提高滑动的流畅度,从三个方面可以说明这个问题:异步方式处理耗时操作;控制异步任务执行频率;开启硬件加速。
参考资料:《Android开发艺术探索》