Android开发中不缩放Bitmap且避免OOM的高效压缩方案
问题根因
你当前代码触发OOM是两个实现错误导致的,和是否缩放Bitmap没有直接关系:
MediaStore.Images.Media.getBitmap()默认会以ARGB_8888格式完整解码全尺寸原图,单张4000*3000分辨率的图解码后就会占用近48MB内存,大尺寸原图这一步就已经逼近内存阈值。- *PNG是无损压缩格式,
compress()方法传入的quality参数对PNG格式完全无效,不管你传0还是100,最终输出的都是和原始像素数据体积基本一致的无损内容,你写的compress(Bitmap.CompressFormat.PNG, 50, ...)等于没做任何压缩。后续执行toByteArray()时需要申请一块和压缩前数据差不多大的连续内存,很容易触发OOM。
不重缩放Bitmap的压缩落地方案
以下方案完全不修改Bitmap的原始宽高尺寸,同时可以把内存占用压到安全线内:
- 第一步:解码阶段优化内存占用,不缩放也能砍半内存
弃用直接调用getBitmap()的写法,改用BitmapFactory自定义解码配置,保持inSampleSize=1(即1:1采样不缩放),不需要透明通道的场景把像素存储格式改成RGB_565,单个像素内存占用从4字节降到2字节,直接把Bitmap本身的内存砍半。
如果需要保留图片透明通道,把配置改回// 和现有代码风格一致的Java实现 BitmapFactory.Options options = new BitmapFactory.Options(); options.inPreferredConfig = Bitmap.Config.RGB_565; // 无透明通道场景使用,内存直接减半 options.inSampleSize = 1; // 采样率为1=完全不缩放,保留原始尺寸 InputStream inputStream = getContentResolver().openInputStream(fileUri); Bitmap compressedImageFile = BitmapFactory.decodeStream(inputStream, null, options); inputStream.close();ARGB_8888即可,后续压缩步骤依然可以大幅降低输出体积。 - 第二步:替换无效的PNG压缩格式
不要用PNG做质量压缩,根据系统版本选择对应有损压缩格式,quality参数设为50-70区间肉眼几乎看不出画质差异,输出体积可以降到原PNG的1/10甚至更低:- Android 12(API31)及以上:用
Bitmap.CompressFormat.WEBP_LOSSY,同画质下体积比JPEG小30%,还支持透明通道 - 低版本兼容:用
Bitmap.CompressFormat.JPEG,全版本系统兼容性最好
- Android 12(API31)及以上:用
- 第三步:避免全量字节数组的内存申请
不要把压缩后的内容全部攒在ByteArrayOutputStream里最后转成字节数组——这个操作需要申请和压缩后文件等大的连续内存,就算压缩后只剩几MB,连续内存不足也会触发OOM。
如果你后续是要存本地、上传服务端,直接把压缩流输出到目标载体即可,全程只需要几KB的缓冲区,完全不会占大内存:
如果后续逻辑必须拿到字节数组,可以给// 示例:直接压缩输出到本地文件,不在内存中留存全量数据 File targetFile = new File(getCacheDir(), "compressed_img"); FileOutputStream fos = new FileOutputStream(targetFile); // 直接写入文件流,不需要ByteArrayOutputStream做中转 compressedImageFile.compress(Bitmap.CompressFormat.JPEG, 60, fos); fos.flush(); fos.close(); // 用完及时回收Bitmap内存 compressedImageFile.recycle();ByteArrayOutputStream初始化时提前设置合理的初始容量,避免反复扩容产生内存碎片,同时拿到字节数组用完后及时释放引用,帮助GC回收。
注意:如果你要求必须保留无损画质,那不存在可以大幅降低内存占用的方案——无损压缩的压缩比上限极低,全尺寸无损图的像素数据本身就会占用固定大小的内存,要全量加载必然要占对应内存空间,这种场景要么接受有损压缩,要么必须做尺寸缩放。

内容的提问来源于stack exchange,提问作者Bitwise DEVS
相关产品推荐
相关产品推荐

