5、Bitmap内存优化:从计算到缓存的全链路实践

Bitmap 优化,几乎是每个 Android 开发者都绕不开的坎。我记得刚入行那会儿,一个列表加载几张高清图就直接 OOM,老板拿着崩溃日志找我谈话的场景还历历在目。今天咱们就把 Bitmap 内存这块彻底聊透。

5.1 Bitmap 内存到底怎么算?

很多人以为 Bitmap 占多大内存,就是图片文件的大小。其实完全不是一回事。文件大小是压缩后的,而内存里是解压后的原始像素数据。

计算公式其实很简单:

Bitmap 内存 ≈ 宽度 × 高度 × 每像素字节数

每像素字节数取决于 Bitmap 的配置:

Config 每像素字节 说明
ALPHA_8 1 只有透明度,没有颜色
RGB_565 2 没有透明度,颜色精度低
ARGB_4444 2 已废弃,不推荐
ARGB_8888 4 默认配置,质量最高

举个例子,一张 1920×1080 的图片,用 ARGB_8888 加载:

1920 × 1080 × 4 = 8,294,400 字节 ≈ 7.9 MB

一张图片就吃掉将近 8MB,你想想看,如果列表里同时加载十几张,内存不爆才怪。

关键认知:Bitmap 内存只与图片的像素尺寸和色彩格式有关,与文件大小(KB/MB)没有直接关系。

5.2 inSampleSize:采样压缩,降维打击

既然内存大头在像素数量,那最简单的思路就是——减少像素数量。这就是 inSampleSize 干的事。

inSampleSize 是 BitmapFactory.Options 里的一个参数。设置为 2,宽高各缩一半,像素总量变成原来的 1/4。设置为 4,变成 1/16。

BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 2; // 宽高各缩一半
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.large_image, options);

那 inSampleSize 设多少合适?我个人的习惯是,先拿到图片的原始尺寸,再根据目标控件的尺寸来计算。

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;
}

小技巧:inSampleSize 官方建议使用 2 的幂次(1, 2, 4, 8...),这样解码效率最高。非 2 的幂次虽然也能用,但会向下取整到最近的 2 的幂次。

5.3 inBitmap:复用内存,减少分配

inSampleSize 解决了「少加载」的问题,但频繁创建和销毁 Bitmap 依然会产生大量 GC。inBitmap 就是用来复用已有 Bitmap 内存的。

说白了,就是你把一块已经用过的 Bitmap 内存,重新分配给新的 Bitmap 使用。避免重复申请和释放。

BitmapFactory.Options options = new BitmapFactory.Options();
options.inMutable = true; // 必须设置为可变
options.inBitmap = reusableBitmap; // 传入要复用的 Bitmap
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.new_image, options);

这里有几个坑,我曾经踩过:

  • 复用 Bitmap 的尺寸必须 >= 新 Bitmap 的尺寸(Android 4.4 之前要求严格相等)
  • 复用 Bitmap 的 Config 必须与新 Bitmap 一致
  • inBitmap 必须设置 inMutable = true

注意:inBitmap 在 Android 4.4(API 19)之后放宽了限制,只要复用 Bitmap 的字节数 >= 新 Bitmap 的字节数即可,不再要求宽高完全一致。但建议还是尽量匹配,避免浪费。

5.4 LruCache 与 DiskLruCache:双缓存策略

内存缓存和磁盘缓存,是图片加载框架的基石。LruCache 管内存,DiskLruCache 管磁盘。

LruCache 的核心思想是「最近最少使用」。当缓存满了,优先淘汰最久没被访问的那一项。

int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8; // 通常分配 1/8 的应用内存

LruCache<String, Bitmap> memoryCache = new LruCache<String, Bitmap>(cacheSize) {
    @Override
    protected int sizeOf(String key, Bitmap bitmap) {
        return bitmap.getByteCount() / 1024; // 以 KB 为单位
    }
};

我一般建议把缓存大小设为应用最大内存的 1/8 到 1/4。太大容易挤占其他业务的内存,太小又起不到缓存效果。

DiskLruCache 则是把图片缓存到磁盘上。虽然读取速度比内存慢,但胜在容量大、持久化。

// DiskLruCache 的使用相对复杂,需要自己管理文件目录和版本号
// 这里只展示核心逻辑
DiskLruCache diskCache = DiskLruCache.open(cacheDir, appVersion, valueCount, maxSize);
DiskLruCache.Editor editor = diskCache.edit(key);
// 写入缓存
OutputStream out = editor.newOutputStream(0);
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, out);
editor.commit();

最佳实践:内存缓存 + 磁盘缓存 + 网络加载,三级缓存架构。先查内存,再查磁盘,最后走网络。这样用户体验最好,流量消耗也最小。

5.5 Glide 与 Picasso 的缓存策略

实际项目中,很少有人手写缓存了。Glide 和 Picasso 已经帮我们封装好了。但了解它们的策略,能帮我们更好地选型和调优。

特性 Glide Picasso
内存缓存 LruCache,默认 1/8 应用内存 LruCache,默认 1/7 应用内存
磁盘缓存 支持,默认 250MB 支持,默认 50MB
缓存策略 根据 ImageView 尺寸缓存不同分辨率 缓存原始尺寸
Bitmap 复用 内置 inBitmap 复用池 不支持

我个人更倾向于 Glide。原因很简单:它默认会根据 ImageView 的尺寸来缓存对应分辨率的图片,而不是缓存原图。这意味着内存占用更小,加载速度更快。

而且 Glide 内部实现了 Bitmap 复用池,不需要我们自己操心 inBitmap 的逻辑。这一点在列表快速滑动时特别明显,GC 次数明显减少。

// Glide 使用示例
Glide.with(context)
     .load(url)
     .override(300, 300) // 指定目标尺寸
     .diskCacheStrategy(DiskCacheStrategy.ALL) // 缓存原图和转换后的图
     .into(imageView);

避坑指南:我曾经在一个项目中直接用 Picasso 加载大图列表,结果内存飙升。后来换成 Glide 并指定 override 尺寸,内存直接降了 60%。如果你的列表图片尺寸不统一,Glide 的按需缓存优势会更明显。

5.6 本章知识体系总览

下面这张图,把 Bitmap 内存优化的核心链路串起来了:

Bitmap 内存优化全链路 图片加载请求 ① inSampleSize 采样压缩 减少像素总量,降低内存占用 ② inBitmap 内存复用 复用已有 Bitmap 内存,减少 GC ③ 缓存策略:LruCache + DiskLruCache 内存缓存(快速) → 磁盘缓存(持久) → 网络加载(兜底) Glide / Picasso 框架落地

从图片加载请求开始,先经过 inSampleSize 采样压缩,减少像素总量;再通过 inBitmap 复用已有内存,减少分配开销;最后利用 LruCache 和 DiskLruCache 构建多级缓存,避免重复解码。这些底层能力,最终由 Glide 或 Picasso 这样的框架统一封装,提供给开发者使用。

嗯,Bitmap 内存优化这块,说白了就是三件事:少加载、复用、缓存。把这三点做到位,OOM 基本就离你远去了。


公众号:蓝海资料掘金营,微信deep3321