本章的主题是Bitmap的加载和Cache,主要包含三个方面的内容。首先讲述如何有效地加载一个Bitmap,接着介绍Android中常用的缓存策略,最后本章会介绍如何优化列表的卡顿现象。
Bitmap 的高效加载
BitmapFactory 类提供了四类方法:
- decodeFile()
- decodeResource()
- decodeStream()
- decodeByteArray()
decodeFile()
、 decodeResource()
和 decodeResource()
最终都是调用了 decodeStream()
,decodeStream()
内部调用的是底层的 native 方法。
高效加载 Bitmap 的核心思想就是采用 BitmapFactory.Options
加载所需尺寸的图片。
很多时候,ImageView 的尺寸并没有图片的原始尺寸那么大,这时显然没有必要按原始尺寸加载,ImageView 用多少我们就给他多少。
通过 BitmapFactory.Options
将图片缩小后再加载,这样降低内存占用,从而在一定程度上避免 OOM。
BitmapFactory.Options
主要用到了 BitmapFactory.Options
中的 inSampleSize
参数。即采样率。
采样后的高 = 采样前的高 / insampleSize
采样后的宽 = 采样前的宽 / insampleSize
采样后所占内存大小 = 采样前所占内存大小 / insampleSize^2
- 如果 inSampleSize < 1,那么等同于 1
- inSampleSize 必须是 2 的幂次方,设置其他值会向下取与它最接近的幂次方的值。比如设置 3,那么系统会取 2。
使用流程:
- 将
BitmapFactory.Options
的inJustDecodeBounds
参数设置为 ture 并加载图片。 - 从
BitmapFactory.Options
中取出图片的原始宽高信息,它们对应于outWidth
和outHeight
参数。 - 根据采样率的规则并结合目标 View 的所需大小计算出采样率 inSampleSize。
- 将
BitmapFactory.Options
的inJustDecodeBounds
参数设为 false,然后重新加载图片。
inJustDecodeBounds = true 时,BitmapFactory 只会解析图片的原始宽/高信息,并不会真正的去加载图片。
public static Bitmap decodeSampleBitmap(Resources resources, int resId, int reqWidth,
int reqHeight) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(resources, resId, options);
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(resources, resId, options);
}
private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth,
int reqHeight) {
// 原始的宽和高
final int width = options.outWidth;
final int height = options.outHeight;
Log.d(TAG, "origin, w= " + width + " h=" + height);
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
// 为了将 inSampleSize 设置为 2 的幂次方,先将原始高度 /2
final int halfHeight = height / 2;
final int halfWidth = width / 2;
// 将 inSampleSize 不断 * 2 直到宽和高有一个满足需要时
while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
Log.d(TAG, "sampleSize:" + inSampleSize);
return inSampleSize;
}
复制代码
Android 中的缓存策略
常用的缓存算法是 LRU(Least Recently Used),即近期最少使用算法,核心思想是:当缓存满了时,优先淘汰近期最少使用的缓存对象。
LruCache 实现内存缓存。
DiskLruCache 用于设备存储缓存。
LruCache
LruCache 是 Android 3.1 所提供的一个缓存类,通过 support-v4 兼容包可以兼容到早期的 Android 版本。
public class LruCache<K, V> {
private final LinkedHashMap<K, V> map;
/** Size of this cache in units. Not necessarily the number of elements. */
private int size;
private int maxSize;
private int putCount;
private int createCount;
private int evictionCount;
private int hitCount;
private int missCount;
// ...
}
复制代码
它内部通过一个 LinkedHashMap 以强引用的方式存储外界的缓存对象。
- 强引用:直接的对象引用
- 软引用:当一个对象只有软引用存在时,系统内存不足时此对象会被 gc 回收
- 弱引用:当一个对象只有弱引用存在时,此对象会随时被 gc 回收
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
LruCache<String, Bitmap> lruCache = new LruCache<String, Bitmap>(cacheSize) {
@Override protected int sizeOf(String key, Bitmap value) {
// 计算 bitmap 的大小 单位:m
// getRowBytes(): 用于计算位图每一行所占用的内存字节数
// 经实测发现:位图大小getByteCount() = getRowBytes() * getHeight(),也就是说位图所占用的内存空间数等于位图的每一行所占用的空间数乘以位图的行数。
// https://yq.aliyun.com/articles/32572
return value.getRowBytes() * value.getHeight() * 1024;
}
@Override
protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
super.entryRemoved(evicted, key, oldValue, newValue);
// 做一些资源回收工作
}
};
// 取
Bitmap key = lruCache.get("key");
// 存
lruCache.put("key",bitmap);
// 删
lruCache.remove("key")
复制代码
DiskLruCache
DiskLruCache 得到了 Android 官方文档的推荐,但它不属于 Android SDK 的一部分,它的源码可以从如下网址得到: https://android.googlesource.com/platform/libcore/+/android-4.1.1_r1/luni/src/main/java/libcore/io/DiskLruCache.java。但是在 Android 会发生编译错误。
JakeWharton 提供了一份没有编译错误的: https://github.com/JakeWharton/DiskLruCache
compile 'com.jakewharton:disklrucache:2.0.2'
复制代码
创建
通过 open 方法创建自身:
/**
* Opens the cache in {@code directory}, creating a cache if none exists
* there.
*
* @param directory 磁盘缓存文件所在的路径
* @param appVersion 版本号
* @param valueCount 单个节点所对应的数据的个数,一般设为1即可
* @param maxSize 缓存的最大值
* @throws IOException if reading or writing the cache directory fails
*/
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
throws IOException
复制代码
final long DISK_CACHE_SIZE = 1024 * 10224 * 50; // 大小为50m
try {
PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
DiskLruCache open =
DiskLruCache.open(getCacheDir(), packageInfo.versionCode, 1, DISK_CACHE_SIZE);
} catch (IOException | PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
复制代码
缓存添加
通过 Editor 完成。Editor 表示一个缓存对象的编辑对象。
以图片缓存为例,用图片 url 的 md5 值作为 key。
// 将url转成md5
private String hashKeyFromUrl(String url) {
String cacheKey;
try {
MessageDigest md5 = MessageDigest.getInstance("MD5");
md5.update(url.getBytes());
cacheKey = bytesToHexString(md5.digest());
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
cacheKey = String.valueOf(url.hashCode());
}
return cacheKey;
}
// 将字节码转成16进制
private String bytesToHexString(byte[] digest) {
StringBuilder builder = new StringBuilder();
for (byte aDigest : digest) {
String hex = Integer.toHexString(0xFF & aDigest);
if (hex.length() == 1) {
builder.append('0');
}
builder.append(hex);
}
return builder.toString();
}
复制代码
// 将图片下载写入 DiskLurCache 的输出流里
private boolean downloadUrlToStream(String urlS, OutputStream outputStream) throws IOException {
final int IO_BUFFER_SIZE = 1024;
HttpURLConnection urlConnection = null;
BufferedOutputStream out = null;
BufferedInputStream in = null;
try {
final URL url = new URL(urlS);
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) {
e.printStackTrace();
} finally {
if (urlConnection != null) urlConnection.disconnect();
if (out != null) {
out.close();
}
if (in != null) {
in.close();
}
}
return false;
}
复制代码
使用:
DiskLruCache diskLruCache = openDiskLruCache();
String key = hashKeyFromUrl("url");
try {
if (diskLruCache != null) {
DiskLruCache.Editor edit = diskLruCache.edit(key);
// 获得文件输出流
if (edit != null) {
OutputStream outputStream = edit.newOutputStream(DISK_CACHE_INDEX);
if (downloadUrlToStream("imageUrl",outputStream)){
// 提交写入操作
edit.commit();
}else {
// 发生错误就回滚整个操作
edit.abort();
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
复制代码
缓存查找
通过存入时的 key 去找到对应的 Snapshot 对象, 通过它即可得到缓存的文件输入流。
避免加载过程中导致的 OOM,应该通过 BitmapFactory.Options 对象来加载一张缩放后的图片,但是这种方法对 FileInputStream 的缩放存在问题。
FileInputStream 是一种有序的文件流,两次 decodeStream 调用影响了文件流的位置属性,导致第 2 次 deocdeSream 时得到的时 null。
解决:
通过文件流来得到它所对应的文件描述符,然后再通过 BitmapFactory.decodeFileDescriptor()
来加载一张缩放后的图片。
try {
Bitmap bitmap;
String key = hashKeyFromUrl("url");
DiskLruCache.Snapshot snapshot = diskLruCache.get(key);
if (snapshot!=null){
FileInputStream inputStream = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
// 获取文件描述符
FileDescriptor fd = inputStream.getFD();
// 再通过 `BitmapFactory.decodeFileDescriptor()` 来加载一张缩放后的图片
bitmap = BitmapUtils.decodeSampledBitmapFromFileDescriptor(fd,reqW,reqH);
if (bitmap!=null) {
addToDiskCache(diskLruCache,"url",bitmap);
}
}
} catch (IOException e) {
e.printStackTrace();
}
复制代码
列表的卡顿
- 在 ListView 的
getView()
中不应做耗时任务。 - 控制异步任务执行的频率
- 开启硬件加速
android:hardwareAccelerated="true"