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 三级缓存流程

加载一张图片的完整流程:

  1. 先从内存缓存找,找到直接返回
  2. 没找到,从磁盘缓存找,找到后加载到内存缓存
  3. 还没找到,从网络下载,下载后同时写入内存缓存和磁盘缓存

这个流程说起来简单,但实现时要注意线程同步。我习惯用AsyncTask或者线程池来处理,避免在主线程做耗时操作。

总结一下:Bitmap内存管理,核心就是三件事——理解内存模型、善用复用机制、合理压缩和缓存。我在项目中见过太多OOM,其实90%的情况都是Bitmap没处理好。你只要把这几点做到位,内存问题基本就能解决大半。

Bitmap内存管理知识体系 Bitmap内存管理 内存模型 • Java堆 vs Native堆 • 像素格式:ARGB_8888等 • 内存计算公式 inBitmap复用 • 复用废弃Bitmap内存 • 版本差异处理 • 可变Bitmap要求 压缩策略 • 尺寸压缩(采样率) • 质量压缩(JPEG) • 色彩格式压缩 缓存策略 • 内存缓存(LruCache) • 磁盘缓存(DiskLruCache) • 三级缓存流程 核心目标 减少内存占用 → 避免OOM → 提升应用稳定性 实践建议 根据场景选择合适的策略,不要一刀切。列表缩略图用RGB_565 + 尺寸压缩,大图展示用ARGB_8888 + 三级缓存

避坑指南:我曾经在一个项目中,把所有图片都用ARGB_8888加载,结果在低端机上频繁OOM。后来改成根据ImageView的尺寸动态计算采样率,内存占用直接降了70%。记住,不要加载比显示尺寸更大的图片,这是最基本的优化。

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