13、Bitmap内存管理:Bitmap内存模型,inBitmap复用,Bitmap压缩与缓存策略
说到Android内存管理,Bitmap绝对是个绕不开的话题。我见过太多应用因为Bitmap没处理好,直接OOM崩溃。说实话,Bitmap是Android里最吃内存的对象之一,一张1920x1080的图片,加载到内存里可能就要占用8MB甚至更多。你想想看,如果列表里滑几张图,内存就飙上去了。
这一章,我就跟你聊聊Bitmap的内存模型、复用技巧、压缩策略和缓存方案。这些都是我在实际项目中反复踩坑后总结出来的经验。
13.1 Bitmap内存模型
先搞清楚Bitmap在内存里是怎么存的。这很重要,因为只有理解了底层原理,你才知道怎么优化。
在Android 3.0之前,Bitmap的像素数据是放在Native堆里的,而Bitmap对象本身在Java堆。从3.0到7.0,像素数据移到了Java堆。到了Android 8.0(API 26)之后,又改回了Native堆。嗯,这里要注意,不同版本的内存模型不一样,处理方式也不同。
一张Bitmap占用的内存大小,计算公式很简单:
内存大小 = 宽度 × 高度 × 每个像素的字节数
每个像素的字节数取决于Bitmap的配置:
| Config | 每像素字节数 | 说明 |
|---|---|---|
| ALPHA_8 | 1 | 只有透明度,没有颜色 |
| RGB_565 | 2 | 没有透明度,颜色精度低 |
| ARGB_4444 | 2 | 已废弃,质量差 |
| ARGB_8888 | 4 | 默认配置,质量最高 |
| RGBA_F16 | 8 | HDR图片使用 |
| HARDWARE | 特殊 | 只存在于GPU内存 |
举个例子,一张1080x1920的图片,用ARGB_8888加载:
1080 × 1920 × 4 = 8,294,400 字节 ≈ 7.9MB
一张图就快8MB了。如果列表里同时显示10张,那就是80MB。再加上其他内存开销,OOM是迟早的事。
核心要点:Bitmap内存大小由分辨率和色彩格式决定。不要盲目使用ARGB_8888,如果图片不需要透明度,RGB_565能省一半内存。
13.2 inBitmap复用
inBitmap是Android 3.0引入的一个优化手段。说白了,就是让新加载的Bitmap复用已经废弃的Bitmap的内存空间,避免频繁分配和回收。
我记得在做一个图片编辑应用时,用户频繁切换滤镜,每次切换都要生成新的Bitmap。如果不复用,内存就像坐过山车一样,GC频繁触发,界面卡顿严重。后来用了inBitmap,内存波动小了很多。
使用inBitmap有几个前提条件:
- Android 3.0以上支持,但4.4之后才真正好用
- 复用的Bitmap必须可变(mutable)
- 新Bitmap的内存大小不能超过复用Bitmap的内存大小
- Android 4.4之前,两张Bitmap的宽高必须完全一致
- Android 4.4之后,只要新Bitmap的内存不超过复用Bitmap即可
代码示例:
// 创建一个可复用的Bitmap池
private static final Set<WeakReference<Bitmap>> reusableBitmaps =
new HashSet<>();
// 尝试复用
public static Bitmap decodeBitmapWithInBitmap(
Resources res, int resId, int reqWidth, int reqHeight) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inMutable = true; // 必须设置为可变
// 从池中找一个可复用的Bitmap
Bitmap reusable = findReusableBitmap(options);
if (reusable != null) {
options.inBitmap = reusable;
}
// 先解码尺寸信息
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
// 计算采样率
options.inSampleSize = calculateInSampleSize(
options, reqWidth, reqHeight);
// 真正解码
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
// 查找可复用的Bitmap
private static Bitmap findReusableBitmap(BitmapFactory.Options options) {
for (WeakReference<Bitmap> ref : reusableBitmaps) {
Bitmap bitmap = ref.get();
if (bitmap != null && bitmap.isMutable()) {
// Android 4.4之后只需要检查内存大小
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (bitmap.getAllocationByteCount() >=
options.outWidth * options.outHeight * 4) {
return bitmap;
}
} else {
// 4.4之前必须宽高完全一致
if (bitmap.getWidth() == options.outWidth &&
bitmap.getHeight() == options.outHeight) {
return bitmap;
}
}
}
}
return null;
}
避坑指南:我曾经在项目中直接用inBitmap复用不同尺寸的Bitmap,结果在Android 4.4以下的设备上崩溃了。后来加了版本判断才解决。另外,inBitmap不能用于inJustDecodeBounds模式,这点也容易忽略。
13.3 Bitmap压缩策略
压缩是减少Bitmap内存最直接的手段。但压缩不是一味地降低质量,而是要在视觉效果和内存占用之间找到平衡点。
我常用的压缩策略有三种:
13.3.1 尺寸压缩
通过采样率(inSampleSize)缩小图片的宽高。比如一张1920x1080的图片,如果只需要显示在480x270的ImageView里,采样率设为4,内存就从8MB降到0.5MB。
public static int calculateInSampleSize(
BitmapFactory.Options options, int reqWidth, int reqHeight) {
int width = options.outWidth;
int height = options.outHeight;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
int halfHeight = height / 2;
int halfWidth = width / 2;
while (halfHeight / inSampleSize >= reqHeight &&
halfWidth / inSampleSize >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
13.3.2 质量压缩
质量压缩改变的是图片的文件大小,而不是内存大小。它主要用于上传或保存场景。但要注意,质量压缩对PNG格式无效,PNG是无损压缩。
// 质量压缩,输出到字节流
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, baos);
byte[] data = baos.toByteArray();
13.3.3 色彩格式压缩
如果图片不需要透明度,用RGB_565代替ARGB_8888,内存直接减半。我做过一个相册应用,所有缩略图都用RGB_565,用户根本看不出区别。
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
Bitmap bitmap = BitmapFactory.decodeResource(res, resId, options);
个人经验:对于用户头像、列表缩略图这类小图,RGB_565完全够用。但如果是展示摄影作品、设计稿预览,还是用ARGB_8888吧,色彩过渡会更平滑。
13.4 缓存策略
缓存是避免重复加载Bitmap的关键。我一般用三级缓存:内存缓存、磁盘缓存、网络加载。
13.4.1 内存缓存
内存缓存用LruCache实现。它基于LRU算法,当缓存满了会淘汰最久未使用的条目。
// 获取应用的最大可用内存
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// 取1/8作为缓存大小
int cacheSize = maxMemory / 8;
LruCache<String, Bitmap> memoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
// 返回Bitmap的字节数
return bitmap.getByteCount() / 1024;
}
};
13.4.2 磁盘缓存
磁盘缓存用DiskLruCache。它把Bitmap压缩后存到文件系统,应用重启后还能用。
// 使用DiskLruCache
DiskLruCache diskCache = DiskLruCache.open(cacheDir, 1, 1, 50 * 1024 * 1024);
// 写入缓存
DiskLruCache.Editor editor = diskCache.edit(key);
OutputStream out = editor.newOutputStream(0);
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, out);
editor.commit();
// 读取缓存
DiskLruCache.Snapshot snapshot = diskCache.get(key);
InputStream in = snapshot.getInputStream(0);
Bitmap bitmap = BitmapFactory.decodeStream(in);
13.4.3 三级缓存流程
加载一张图片的完整流程:
- 先从内存缓存找,找到直接返回
- 没找到,从磁盘缓存找,找到后加载到内存缓存
- 还没找到,从网络下载,下载后同时写入内存缓存和磁盘缓存
这个流程说起来简单,但实现时要注意线程同步。我习惯用AsyncTask或者线程池来处理,避免在主线程做耗时操作。
总结一下:Bitmap内存管理,核心就是三件事——理解内存模型、善用复用机制、合理压缩和缓存。我在项目中见过太多OOM,其实90%的情况都是Bitmap没处理好。你只要把这几点做到位,内存问题基本就能解决大半。
避坑指南:我曾经在一个项目中,把所有图片都用ARGB_8888加载,结果在低端机上频繁OOM。后来改成根据ImageView的尺寸动态计算采样率,内存占用直接降了70%。记住,不要加载比显示尺寸更大的图片,这是最基本的优化。