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 知识体系总览
下面这张图,是我梳理的图片优化核心流程。你可以对照着检查自己的项目:
你看,整个流程其实就四步:先读尺寸,再算采样率,然后尝试复用内存,最后放到后台线程解码。每一步都有对应的优化点。
14.5 避坑指南
最后分享几个我踩过的坑:
- inBitmap和inSampleSize冲突:Android 4.4以下,如果inSampleSize不同,复用会失败。我建议只在API 19以上使用inBitmap。
- RGB_565的坑:节省一半内存没错,但如果有透明度通道,图片会变黑。我一般先判断图片是否有alpha通道,再决定用哪个配置。
- 解码线程数:不是越多越好。我曾经设了8个线程,结果CPU满载,UI反而更卡。2-4个线程是最优解。
总结一下:图片优化的本质是内存换时间,或者时间换内存。inBitmap是内存换时间(复用省去分配),inSampleSize是时间换内存(多一次解码计算,但省内存)。你要根据场景做权衡。
好了,这一章的内容就到这里。下一章我们会聊到更进阶的图片缓存策略,包括LRU缓存和磁盘缓存的配合使用。
公众号:蓝海资料掘金营,微信deep3321