14、Bitmap与图片优化:BitmapFactory.Options配置、inBitmap复用、图片解码线程优化

图片优化这个话题,说实话,是Android性能优化里绕不开的一座山。我见过太多应用因为图片加载不当,直接卡成PPT。今天我们就来聊聊Bitmap的三大核心优化点:Options配置、inBitmap复用、还有解码线程。

14.1 BitmapFactory.Options:你配置对了吗?

很多人用BitmapFactory加载图片,直接调decodeFile就完事了。嗯,这样用其实问题很大。我早期做项目时也这么干,结果一张4K壁纸直接OOM,那叫一个惨。

Options的核心配置,我列个表给你看:

参数 作用 建议值
inSampleSize 采样率,控制图片缩小倍数 2的幂次,如2、4、8
inJustDecodeBounds 只解码图片宽高,不加载像素 true(先读尺寸)
inPreferredConfig 像素格式 ARGB_8888或RGB_565
inDither 是否抖动 false(除非有渐变需求)
inScaled 是否按屏幕密度缩放 false(手动控制更安全)

我个人习惯的做法是:先设inJustDecodeBounds为true,拿到图片的原始宽高。然后根据目标控件的尺寸,计算出合适的inSampleSize。最后再设回false,真正解码。

核心思路:先量尺寸,再裁剪,最后加载。别一上来就全量解码。

BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.drawable.large_image, options);

int imageWidth = options.outWidth;
int imageHeight = options.outHeight;

// 计算采样率
options.inSampleSize = calculateSampleSize(imageWidth, imageHeight, targetWidth, targetHeight);
options.inJustDecodeBounds = false;
options.inPreferredConfig = Bitmap.Config.RGB_565; // 非透明图用565省一半内存

Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.large_image, options);

这里有个坑:inSampleSize必须是2的幂次,否则系统会向下取整到最近的2的幂。我曾经踩过这个坑,设了3,结果系统按2处理,图片还是偏大。

14.2 inBitmap复用:别让GC频繁打工

你想想看,每次加载新图片都重新分配内存,旧图片又等着GC回收。频繁的GC会导致卡顿,尤其是大图场景。

inBitmap这个参数,说白了就是告诉BitmapFactory:嘿,别新建内存了,用这块现成的。前提是复用的Bitmap必须满足两个条件:

  • 新图片的字节数不能超过复用Bitmap的字节数
  • Android 4.4以上要求新旧Bitmap的inSampleSize必须一致

小技巧:我一般维护一个Bitmap缓存池,把不再使用的Bitmap放进去。下次加载时优先从池子里取。这样内存分配次数能减少60%以上。

Bitmap reusableBitmap = Bitmap.createBitmap(1024, 1024, Bitmap.Config.ARGB_8888);

BitmapFactory.Options options = new BitmapFactory.Options();
options.inMutable = true; // 必须设为true
options.inBitmap = reusableBitmap;

// 尝试复用
Bitmap result = BitmapFactory.decodeFile("/sdcard/photo.jpg", options);
if (result == null) {
    // 复用失败,走正常流程
    options.inBitmap = null;
    result = BitmapFactory.decodeFile("/sdcard/photo.jpg", options);
}

注意,inBitmap复用不是100%成功的。比如新旧图片的尺寸差异太大,或者硬件不支持,就会返回null。所以一定要做fallback处理。

14.3 图片解码线程优化:别堵在主线程

这个其实是最容易被忽视的点。很多人把图片解码放在主线程,觉得decode很快。但一张10MB的图片,解码时间可能达到50-100ms。如果列表里有20张图,主线程直接卡死。

我建议的做法是:

  • 使用专用的解码线程池,线程数控制在2-4个
  • 解码完成后通过Handler或协程切回主线程
  • 如果使用Glide或Coil,它们内部已经做了线程管理,但如果你自己写图片加载库,这个必须注意

警告:千万不要在RecyclerView的onBindViewHolder里直接解码图片。我曾经见过一个项目,在bind时decode,结果滑动时一帧解码十几张图,帧率直接掉到个位数。

// 使用线程池解码
ExecutorService decoderPool = Executors.newFixedThreadPool(3);

decoderPool.execute(() -> {
    Bitmap bitmap = decodeBitmapSafely(path, targetWidth, targetHeight);
    runOnUiThread(() -> imageView.setImageBitmap(bitmap));
});

这里有个细节:解码线程的优先级可以设低一点,避免和UI线程抢CPU。我习惯设成THREAD_PRIORITY_BACKGROUND。

14.4 知识体系总览

下面这张图,是我梳理的图片优化核心流程。你可以对照着检查自己的项目:

Bitmap图片优化核心流程 1. 尺寸预读 inJustDecodeBounds=true 2. 计算采样率 inSampleSize=2的幂次 3. 尝试复用 inBitmap复用池 成功? 直接解码复用 零内存分配 回退 新建Bitmap 4. 线程解码 后台线程池执行 ✅ 内存安全 + 流畅加载

你看,整个流程其实就四步:先读尺寸,再算采样率,然后尝试复用内存,最后放到后台线程解码。每一步都有对应的优化点。

14.5 避坑指南

最后分享几个我踩过的坑:

  • inBitmap和inSampleSize冲突:Android 4.4以下,如果inSampleSize不同,复用会失败。我建议只在API 19以上使用inBitmap。
  • RGB_565的坑:节省一半内存没错,但如果有透明度通道,图片会变黑。我一般先判断图片是否有alpha通道,再决定用哪个配置。
  • 解码线程数:不是越多越好。我曾经设了8个线程,结果CPU满载,UI反而更卡。2-4个线程是最优解。

总结一下:图片优化的本质是内存换时间,或者时间换内存。inBitmap是内存换时间(复用省去分配),inSampleSize是时间换内存(多一次解码计算,但省内存)。你要根据场景做权衡。

好了,这一章的内容就到这里。下一章我们会聊到更进阶的图片缓存策略,包括LRU缓存和磁盘缓存的配合使用。


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