目前比较常用的缓存策略是LruCache(Android3.1提供)和DiskLruCache(是官方文档推荐,但不属于Android SDK,需要自行下载源码编译)。
下载地址: https://android.googlesource.com/platform/libcore/+/android-4.1.1-r1/luni/src/main/java/libcore/io/DiskLruCache.java。
LruCache常被用作内存缓存,而DiskLruCache常被用作磁盘缓存。Lru是Least Recently Used的缩写,
LurCache内部其实是用了一个LinkedHashMap来存储数据的,它的构造如下:
public LruCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
this.map = new LinkedHashMap(0, 0.75f, true);
}
构造了一个初始容量为0,负载因子为0.75,accessOrder为true的LinkedHashMap。accessOrder为true意味着链表中元素的顺序为访问顺序,即调用get方法后,会将这次访问的元素移至链表尾部,这样最前面的一个元素就是最近最少使用的了。当元素数量达到指定的最大数量之后是怎么删除的呢?主要是LinkedHashMap中的removeEldestEntry方法。例如可以这样重写这个方法:
final int MAX_ENTRIES = 50;
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > MAX_ENTRIES;
}
这样当put新元素的时候,如果removeEldestEntry返回了true,就会删除最老的那个元素。而返回true 的条件就是LinkedHashMap的size大于我们指定的值。这就是LruCache的实现原理。DiskLruCache类似。
下面开始将如何高效的加载Bitmap。
Bitmap 的加载主要是通过BItmapFactory,BitmapFactory有4中方法加载Bitmap:decodeFile,decodeResource,decodeStream和decodeByteArray。其中decodeFile,decodeResource间接调用了decodeStream方法。
很多时候我们图片的大小是大于ImageView的大小的,这个时候我们就需要对Bitmap进行缩放,如何缩放呢?
主要是通过采样率来进行缩放。设置采样率的方式为 BitmapFactory.Options的inSampleSize参数。当inSampleSize为1时,表示是图片的原始大小不缩放,当inSampleSize大于1,比如为2时,那么采样后的图片的宽带都为原来的1/2,而像素数为原图的1/4,其占有的内存大小也为1/4。而且采样率必须为大于1的整数才有效果,当小于1时,其作用相当于1,无缩放效果。另外最新的官方文档中指出,inSampleSize的取值应该总是2的指数,比如,1,2,4,8,16等等。如果外界传递给系统的inSampleSize不为2的指数,那么系统会向下取整并选择一个最接近2的指数来代替。比如传3,系统会取2来代替。但是通过验证,这个结论并非在所有的Android版本都适用,建议开发的时候按2的指数取。
通过采样率加载可以按照以下步骤:
1.将BitmapFactory.Options的inJustDecodeBounds参数设置为true并加载图片。(设置为true之后并不会真正的去加载图片,只会解析图片的原始宽高。这个操作是轻量级的,但是得注意这个操作获取的图片的宽高信息跟图片的位置以及程序运行的设备有关,比如放在不同的Drawable下面或运行在不同分辨率的机器上)。
2.从BitmapFactory.Options中取出图片的原始宽高信息,它们对应于outWidth和outHeight参数。
3.根据采样率的规则并结合目标View的所需大小计算出采样率inSampleSize。
4.将BitmapFactory.Options的inJustDecodeBounds参数设为false,然后重新加载图片。
下面给出一个比较通用的计算采样率的算法:
public int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight){
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize=1;
if(height > reqHeight || width > reqWidth){
final int halfHeight = height /2;
final int halfWidth = width / 2;
while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) > = reqWidth){
inSampleSize *=2;
}
}
return inSampleSize;
}
传入的options为解析后的options,其中这个算法的关键部分可以仔细体会一下。
LruCache的典型初始化代码:
int maxMemory = (int) ((Runtime.getRuntime().maxMemory()) / 1024);
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache(cacheSize){
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight() / 1024;
}
};
sizeOf方法是计算缓存对象大小的。这里大小需要和总容量的单位一致,上面的为KB。
DiskLruCache提供了open方法来创建自身,如下所示:
public static DiskLruCache open(File dir, int appVersion, int valueCount, long maxSize);
dir表示缓存的文件的目录。
appVersion表示应用的版本号,一般设为1,版本号发生改变时,DiskLruCache会清空之前所有的缓存文件。
valueCount 表示耽搁节点所对应的数据的个数,一般设为1即可。
maxSize表示缓存的最大值,比如50MB,但是要转换成byte。
DiskLruCache缓存的添加:
DiskLruCache的缓存的添加时通过Editor完成的,Editor表示一个缓存对象的编辑对象。
例如:
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if(editor != null){
OutputStream outputStream = editor.newOutputSream(index);
}
因为前面设置了valueCount为1,这里index可以直接传0;
拿到这个outputSream之后,就可以把从网络上下载下路的文件流写入里面,注意最后写完了,要调一下editor的commit方法,即editor.commit();
DiskLruCache缓存的查找:
Bitmap bitmap = null;
DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
if(snapShot != null){
FileInputStream fileInputStream = (FileInputStream)snapShot.getInputStream(index);
FileDescriptor fileDescriptor = fileInputStream.getFD();
bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor);
}
之所以通过fileDescriptor是因为,通过采样率来缩放图片之后,会对FileInputStream的缩放存在问题,因为FileInputStream是一种有序的文件流,而两次decodeStream调用影响了文件流的位置属性,导致了第二次decodeStream时得到的是null;所以这里用文件描述符来解决。
mDiskLruCache.remove(key):删除某个文件。
mDiskLruCache.delete():删除所有缓存文件。
其他方法可自行查阅。