Bitmap的加载和Cache
Bitmap的高效加载
BitmapFactory类提供了四类方法:decodeFile、decodeResource、decodeStream和decodeByteArray,分别支持从文件系统、资源、输入流以及字节数组中加载出一个Bitmap对象。
采用BitmapFactory.Options来加载所需尺寸的图片。通过BitmapFactory.Options来缩放图片,主要用到它的inSampleSize参数,即采样率。inSampleSize=1,采样图片为图片的原始大小;inSampleSize=2,采样图片高/宽为原来的1/2,像素数为原来的1/4,内存大小也为1/4。
获取采样率的流程:
1. 将BitmapFactory.Options的inJustDecodeBounds参数设为true并加载图片。
2. 从BitmapFactory.Options中获取图片的原始宽高信息,它们对应于outWidth和outHeight参数
3. 根据采样率的规则并结合目标View的所需大小算出采样率inSampleSize。
4. 将BitmapFactory.Options的inJustDecodeBounds参数设置为false,然后重新加载图片。
说明一下inJustDecodeBounds参数,当次参数设为true,BitmapFactory只会解析图片的原始宽高信息,并不会去真正地加载图片,所以这个操作是轻量级的。
public Bitmap decodeSampledBitmapFromFileDescriptor(FileDescriptor fd, int reqWidth, int reqHeight) {
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFileDescriptor(fd, null, options);
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth,
reqHeight);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFileDescriptor(fd, null, options);
}
public int calculateInSampleSize(BitmapFactory.Options options,
int reqWidth, int reqHeight) {
if (reqWidth == 0 || reqHeight == 0) {
return 1;
}
// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
Log.d(TAG, "origin, w= " + width + " h=" + height);
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
// Calculate the largest inSampleSize value that is a power of 2 and
// keeps both
// height and width larger than the requested height and width.
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
Log.d(TAG, "sampleSize:" + inSampleSize);
return inSampleSize;
}
Android中的缓存策略
缓存策略主要包含缓存的添加、获取和删除这三类操作。
最常用的缓存算法是LRU,近期最少用算法。采用此算法的缓存有两种:LruCache和DiskLruCache。
LruCache
LruCache是一个泛型类,它内部采用一个LinkedHashMap以强引用的方式存储外界的缓存对象,其提供了get和put方法来完成缓存的获取和添加操作,当缓存满时,LruCache会移除较早使用的缓存对象。是线程安全的。
public class LruCache<K, V> {
private final LinkedHashMap<K, V> map;
....
下面代码展示LruCache的典型使用
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;
}
};
sizeOf方法的作用是计算缓存的大小,这里大小的单位要和总容量的单位一致。
添加缓存对象
mMemoryCache.put(key, bitmap);
获取缓存对象
mMemoryCache.get(key);
DiskLruCache
DiskLruCache用于实现存储设备缓存,即磁盘缓存,它通过缓存对象写入文件系统从而实现缓存的效果。
1. DiskLruCache的创建
DiskLruCache提供open方法用于创建自身。
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
参数1表示磁盘缓存在文件系统中的存储路径;参数2表示应用版本号;参数3表示单个节点所对应的数据的个数一般设为1;参数4表示缓存总大小。
典型创建过程
private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50;
File diskCacheDir = getDiskCacheDir(mContext, "bitmap");
if (!diskCacheDir.exists()) {
diskCacheDir.mkdirs();
}
mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1,
DISK_CACHE_SIZE);
- DiskLruCache的缓存添加
DiskLruCache的缓存添加的操作是通过Editor完成的,Editor表示一个缓存对象的编辑对象。以图片缓存为例,首先需要获取图片的url所对应的key,然后根据key就可以通过edit()来获取Editor对象。
private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
checkNotClosed();
validateKey(key);
Entry entry = lruEntries.get(key);
if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER
&& (entry == null || entry.sequenceNumber != expectedSequenceNumber)) {
return null; // snapshot is stale
}
if (entry == null) {
entry = new Entry(key);
lruEntries.put(key, entry);
} else if (entry.currentEditor != null) {
return null; // another edit is in progress
}
Editor editor = new Editor(entry);
entry.currentEditor = editor;
// flush the journal before creating files to prevent file leaks
journalWriter.write(DIRTY + ' ' + key + '\n');
journalWriter.flush();
return editor;
}
典型使用代码:
private String hashKeyFormUrl(String url) {
String cacheKey;
try {
final 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[] 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();
}
获取url所对应的key。
String key = hashKeyFormUrl(url);
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null) {
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
}
获取Editor对象。
有了文件输出流,可以写入到文件系统上。
public boolean downloadUrlToStream(String urlString,
OutputStream outputStream) {
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 (IOException e) {
Log.e(TAG, "downloadBitmap failed." + e);
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
MyUtils.close(out);
MyUtils.close(in);
}
return false;
}
还必须通过Editor的commit()来提交写入过程,如有异常可以通过Editor的abort()来退回整个操作。
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
if (downloadUrlToStream(url, outputStream)) {
editor.commit();
} else {
editor.abort();
}
mDiskLruCache.flush();
图片已经被写入文件系统了。
3. DiskLruCache的缓存查找
和缓存的添加过程类似,缓存查找过程也需要将url转化成key,然后通过DiskLruCache的get方法得到一个Snapshot对象,接着通过Snapshot对象即可得到缓存的文件输入流,有了文件输入流,自然就可以得到Bitmap对象。本文前面介绍了采样率,但是那种方法对FileInputStream的缩放存在问题,原因是FileInputStream是一种有序的文件流,而两次decodeStream调用影响了文件流的位置属性,导致了第二次decodeStream时得到的是null。为了解决这一问题,可以通过文件流来得到它所对应的文件描述符,然后通过BitmapFactory.decodeFileDescriptor方法来加载一张缩放后的图片。
过程如下:
Bitmap bitmap = null;
String key = hashKeyFormUrl(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);
}
}