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 的色带肉眼几乎不可见,可放心使用。
Bitmap 解码与内存优化决策流程 Bitmap 解码与内存优化决策流程 原始图像 BitmapFactory 解码 像素格式决策 ARGB8888(32位) RGB565(16位) Bitmap 复用池 内存分配与显示 选择策略提醒 需透明 → ARGB8888 无透明 + 内存敏感 → 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 性能优化总结

  1. 优先使用 RGB565 —— 在列表、缩略图、非核心 UI 区域优先选择,减少 50% 像素内存。
  2. 构建 Bitmap 复用池 —— 通过 LruCache 按 (宽, 高, 格式) 缓存可复用 Bitmap,避免频繁 GC。
  3. Glide 调整 BitmapPool 大小 —— 根据列表并发数计算,一般设为 16~32 MB。
  4. Picasso 固定缓存上限 —— 用 LruCache(20 MB) 替代百分比配置,降低低端机压力。
  5. 统一降级策略 —— 在 Application.onCreate() 中检测可用 heap,动态决定全局像素格式偏好。
  6. 监控复用命中率 —— 低于 60% 说明池大小或键设计不合理,需调优。
关键指标参考:优化后 Bitmap 相关 GC 频率应降低 70% 以上,Native Heap 峰值下降 30%~50%,列表滑动掉帧率 < 5%。