3. 图像解码与内存优化:Bitmap 复用池、Glide/Picasso 配置、RGB565 与 ARGB8888 选择策略
图像解码是 Android 多媒体应用中最耗内存的操作之一。一张 1920×1080 的图片以 ARGB8888 格式解码后占用约 7.9 MB(1920×1080×4 字节),而应用通常需要同时展示多张图片,内存压力急剧上升。本章围绕 Bitmap 复用池、Glide/Picasso 缓存配置 和 像素格式选择 三条主线,给出可直接落地的优化方案。
3.1 原理:Bitmap 内存模型与解码流程
Bitmap 在 Java Heap 中存储像素数据(Android 8.0 后像素数据移入 Native Heap,但受 Java 层引用管理)。单张 Bitmap 内存占用公式:
内存占用(字节) = 宽度 × 高度 × 每像素字节数
解码流程:资源文件(或网络/磁盘) → BitmapFactory 解析 → 生成 Bitmap 对象。其中 BitmapFactory.Options.inPreferredConfig 决定像素格式,inBitmap 字段控制复用行为。
| 像素格式 | 每像素位数 | 每像素字节 | 透明度支持 | 颜色精度 | 典型场景 |
|---|---|---|---|---|---|
| ARGB8888 | 32 bit | 4 字节 | ✅ 完整 8 位 Alpha | 最高(16.7M 色) | 照片、UI 图标、需要透明通道的图片 |
| RGB565 | 16 bit | 2 字节 | ❌ 不支持 | 中等(65K 色) | 缩略图、列表项、游戏背景、无需透明的图片 |
| ARGB4444 | 16 bit | 2 字节 | ✅ 4 位 Alpha | 低(已废弃) | 不推荐使用 |
经验指标:将 ARGB8888 切换为 RGB565 可节省 50% 的像素内存。对于 100 张 720p 列表缩略图,内存可从 ~280 MB 降至 ~140 MB。
3.2 Bitmap 复用池:原理与实现
核心思路:解码时通过 BitmapFactory.Options.inBitmap 传入一个已废弃但未释放的 Bitmap,让系统直接复用其像素内存,避免频繁分配与 GC 触发。复用池通常基于 LruCache 实现,以 (宽度、高度、像素格式) 为键缓存 Bitmap。
public class ImageReusePool {
private static final int DEFAULT_MAX_BYTES = 20 * 1024 * 1024; // 20 MB
private final LruCache<Key, Bitmap> cache;
public ImageReusePool() {
this(DEFAULT_MAX_BYTES);
}
public ImageReusePool(int maxBytes) {
cache = new LruCache<Key, Bitmap>(maxBytes) {
@Override
protected int sizeOf(Key key, Bitmap bitmap) {
return bitmap.getAllocationByteCount();
}
};
}
public Bitmap obtain(int width, int height, Bitmap.Config config) {
Key key = new Key(width, height, config);
Bitmap candidate = cache.get(key);
if (candidate != null && !candidate.isRecycled()
&& candidate.isMutable()
&& candidate.getWidth() == width
&& candidate.getHeight() == height) {
candidate.eraseColor(0); // 清除旧像素
return candidate;
}
return null; // 池中无匹配,由调用方创建新 Bitmap
}
public void recycle(Bitmap bitmap) {
if (bitmap == null || bitmap.isRecycled() || !bitmap.isMutable()) {
return;
}
Key key = new Key(bitmap.getWidth(), bitmap.getHeight(), bitmap.getConfig());
cache.put(key, bitmap);
}
private static class Key {
final int width;
final int height;
final Bitmap.Config config;
Key(int width, int height, Bitmap.Config config) {
this.width = width;
this.height = height;
this.config = config;
}
@Override
public boolean equals(Object o) { /* 标准实现 */ return true; }
@Override
public int hashCode() { return width * 31 + height * 31 + config.hashCode(); }
}
}
使用约束:只有 mutable 且尺寸、像素格式完全一致的 Bitmap 才能被复用。Android 4.4(API 19)之后放宽了尺寸限制——只要复用者的尺寸 ≤ 缓存 Bitmap 的尺寸即可,但格式仍需匹配。
3.3 Glide 与 Picasso 的缓存配置
两大图片加载框架内置了 Bitmap 复用机制,但默认参数不一定适合所有应用。以下配置可直接嵌入 Application 或 GlideModule 中。
| 框架 | 核心缓存组件 | 默认大小 | 优化方向 |
|---|---|---|---|
| Glide 4.x | LruBitmapPool + LruResourceCache | BitmapPool = 4MB(或 1/8 maxHeap) | 增大 BitmapPool 上限,减少 decode 次数 |
| Picasso 2.8+ | LruCache(内存缓存) | 可用 heap 的 15% | 降低缓存比例或改用自定义 LruCache |
3.3.1 Glide 自定义 BitmapPool
@GlideModule
public class OptimizedGlideModule extends AppGlideModule {
@Override
public void applyOptions(Context context, GlideBuilder builder) {
// 将 BitmapPool 扩大至 16 MB,适合图片密集列表
builder.setBitmapPool(new LruBitmapPool(16 * 1024 * 1024));
// 内存缓存设为 32 MB
builder.setMemoryCache(new LruResourceCache(32 * 1024 * 1024));
// 默认解码格式设为 RGB565(若无需透明度)
builder.setDefaultRequestOptions(
new RequestOptions().format(DecodeFormat.PREFER_RGB_565)
);
}
}
3.3.2 Picasso 内存缓存调整
// 在 Application 初始化时配置
Picasso.Builder builder = new Picasso.Builder(context);
// 使用 20 MB 固定缓存,避免占用过多 heap
builder.memoryCache(new LruCache(20 * 1024 * 1024));
// 关闭全局调试指示器(减少 CPU 开销)
builder.indicatorsEnabled(false);
Picasso.setSingletonBuilder(builder);
// 或直接替换单例
Picasso.setSingletonInstance(builder.build());
⚠ 注意:Picasso 默认将 所有图片 解码为 ARGB8888。若应用中大量图片不需要透明通道,建议通过
RequestCreator.config(Bitmap.Config.RGB_565) 在每个请求上单独指定,或 fork 源码修改默认值。
3.4 RGB565 与 ARGB8888 选择策略
决策依据按优先级排序:
- 是否需要透明度——需要 Alpha 通道的业务(如圆角、渐变遮罩、水印)必须使用 ARGB8888。
- 图片类型——照片、高清壁纸用 ARGB8888;缩略图、头像、列表图标用 RGB565。
- 内存水位——当应用已使用 >80% 可用 heap 时,将非关键区域降级为 RGB565。
- 显示密度——在 xhdpi 以下屏幕,RGB565 的色带肉眼几乎不可见,可放心使用。
3.5 调试与内存监控
使用以下工具验证优化效果:
- Android Studio Memory Profiler —— 观察 Bitmap 对象数量与 Native Heap 波动,定位频繁 GC 点。
- dumpsys meminfo —— 查看进程总 PSS 与 Graphics 部分:
adb shell dumpsys meminfo com.example.app | grep -E "Graphics|Bitmap|Total"
- Glide 自带调试 —— 启用
Glide.get(context).setMemoryCategory(MemoryCategory.LOW)模拟低内存环境。 - Bitmap 复用命中率埋点 —— 在复用池的
obtain()和recycle()中加计数器,输出日志:
Log.d("BitmapPool", "命中率: " + hitCount + "/" + totalRequest
+ " = " + (hitCount * 100 / Math.max(1, totalRequest)) + "%");
3.6 常见错误与解决方案
| 错误现象 | 根因 | 解决方案 |
|---|---|---|
| 复用后图片出现错乱像素 | 复用时未调用 eraseColor() 清除旧数据 |
在 obtain() 中执行 bitmap.eraseColor(0) |
调用 inBitmap 后解码失败 |
复用 Bitmap 的 isMutable() 为 false,或尺寸/格式不匹配 |
创建时使用 Bitmap.createBitmap(…, true) 确保 mutable |
| Glide 加载 RGB565 图片出现色带 | 图片本身有渐变区域,RGB565 精度不足 | 对渐变图片单独指定 ARGB8888,或使用 DecodeFormat.PREFER_ARGB_8888 |
| Picasso 内存缓存过大导致 GC 频繁 | 默认缓存比例(15% heap)在低端机上过高 | 固定为 10~20 MB,或使用 MemoryPolicy.NO_CACHE 实时生效 |
3.7 性能优化总结
- 优先使用 RGB565 —— 在列表、缩略图、非核心 UI 区域优先选择,减少 50% 像素内存。
- 构建 Bitmap 复用池 —— 通过
LruCache按 (宽, 高, 格式) 缓存可复用 Bitmap,避免频繁 GC。 - Glide 调整 BitmapPool 大小 —— 根据列表并发数计算,一般设为 16~32 MB。
- Picasso 固定缓存上限 —— 用
LruCache(20 MB)替代百分比配置,降低低端机压力。 - 统一降级策略 —— 在
Application.onCreate()中检测可用 heap,动态决定全局像素格式偏好。 - 监控复用命中率 —— 低于 60% 说明池大小或键设计不合理,需调优。
关键指标参考:优化后 Bitmap 相关 GC 频率应降低 70% 以上,Native Heap 峰值下降 30%~50%,列表滑动掉帧率 < 5%。