3、采样加载(InSampleSize):如何通过降采样减少图片加载到内存中的尺寸,避免加载原图
好,咱们接着聊。上一章讲了图片在内存里到底是怎么「住」下的,你知道了 Bitmap 占多大,跟分辨率、色彩格式直接挂钩。那问题来了——如果一张图是 4000x3000 的,ImageView 只显示 400x300,你还把原图塞进内存,是不是有点傻?
说白了,这就是典型的「杀鸡用牛刀」。我早年做第一个电商项目时就踩过这个坑。商品详情页的轮播图,每张都是相机拍的 1200 万像素原图,直接加载,结果 OOM 来得比双十一的快递还快。后来才明白,降采样才是正解。
什么是降采样?
降采样,也叫下采样。意思就是,在解码图片的时候,直接告诉解码器:「你别把每个像素都解出来,给我按比例缩一缩再放进内存。」
举个例子:一张 4000x3000 的图,如果按 2:1 降采样,解码后就是 2000x1500。内存占用直接降到原来的四分之一。如果按 4:1,那就是 1000x750,内存只有原来的十六分之一。
核心结论:降采样不是在加载完原图后再做缩放,而是在解码阶段就「偷懒」,只生成你需要的那个小尺寸 Bitmap。内存里从来就没有出现过原图尺寸的数据。
Android 里的降采样工具:BitmapFactory.Options
Android 原生 SDK 给了我们一个很趁手的工具——BitmapFactory.Options。关键参数就一个:inSampleSize。
它的规则很简单:
inSampleSize = 1:不解码,原图尺寸。inSampleSize = 2:宽高各缩小为 1/2,内存变为 1/4。inSampleSize = 4:宽高各缩小为 1/4,内存变为 1/16。- 必须是 2 的幂(2, 4, 8, 16...),否则解码器会向下取整到最近的 2 的幂。
嗯,这里要注意:inSampleSize 只接受整数,而且是 2 的幂。你传个 3 进去,它实际按 2 处理。传个 5,按 4 处理。这是 Android 底层实现决定的,咱们记住就行。
实战:两步走策略
我个人习惯把降采样加载分成两个步骤:
- 第一步:获取原图尺寸(不加载像素)
- 第二步:计算合适的 inSampleSize,加载缩略图
为什么要分两步?因为你不先知道原图多大,就没法算该缩多少。总不能瞎猜一个值吧?
第一步:只读尺寸,不读像素
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.drawable.my_image, options);
int imageWidth = options.outWidth;
int imageHeight = options.outHeight;
String imageType = options.outMimeType;
关键就在 inJustDecodeBounds = true。这个开关一打开,解码器只解析图片的头部信息(宽、高、MIME 类型),不会分配像素内存。速度极快,几乎不占内存。
小技巧:我经常在列表滑动场景里用这个技巧。先快速扫一遍所有图片的尺寸,再决定每个 item 该用多大的采样率。避免在滑动过程中频繁触发大内存分配。
第二步:计算采样率,加载缩略图
public static int calculateInSampleSize(
BitmapFactory.Options options, int reqWidth, int reqHeight) {
// 原始图片的宽高
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
// 不断翻倍,直到缩略图尺寸小于目标尺寸
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
这个算法很朴实:从 1 开始,不断翻倍,直到缩略图比目标尺寸小。注意,它保证的是「缩略图至少有一边小于等于目标尺寸」,而不是精确匹配。因为 inSampleSize 只能取 2 的幂,没法做到完全精确。
拿到采样率后,真正加载:
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = calculateInSampleSize(options, 200, 200);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.my_image, options);
imageView.setImageBitmap(bitmap);
搞定。内存里只有一张 200x200 左右的 Bitmap,而不是 4000x3000 的庞然大物。
采样率计算对照表
为了方便你直观理解,我列个表:
| 原图尺寸 | 目标尺寸 | inSampleSize | 解码后尺寸 | 内存节省比例 |
|---|---|---|---|---|
| 4000x3000 | 200x200 | 16 | 250x188 | 99.6% |
| 4000x3000 | 800x600 | 4 | 1000x750 | 93.75% |
| 1920x1080 | 480x270 | 4 | 480x270 | 93.75% |
| 1920x1080 | 960x540 | 2 | 960x540 | 75% |
你看,一张 4000x3000 的图,如果只需要 200x200 的缩略图,用 inSampleSize = 16,内存占用从原来的 48MB(ARGB_8888)直接降到约 188KB。这差距,你想想看。
降采样 vs 加载后缩放
很多新手容易搞混这两个概念。我专门对比一下:
| 对比维度 | 降采样(inSampleSize) | 加载后缩放(Matrix/Scale) |
|---|---|---|
| 内存峰值 | 低(只存缩略图) | 高(先存原图,再缩放) |
| 解码速度 | 快(像素少) | 慢(像素多) |
| 图片质量 | 略低(硬采样) | 高(可插值) |
| 适用场景 | 缩略图、列表 item | 需要高质量缩放的场景 |
说白了,降采样是用一点点质量换大量的内存。对于列表里的缩略图、头像、小图,这点质量损失肉眼根本看不出来。但内存节省是实打实的。
我曾经踩过的坑:有次做图片编辑功能,我用了降采样加载原图做滤镜预览。结果滤镜效果在缩略图上看着还行,一应用到原图就完全不对了。后来才意识到——降采样会丢失高频细节,做图像处理时必须用原图或高质量缩放,不能用降采样。
降采样的知识体系
我把这一章的核心逻辑画了张图,方便你整体把握:
几个实用建议
- 目标尺寸怎么定?我一般直接用 ImageView 的宽高。如果 ImageView 还没测量完,可以用
ViewTreeObserver等布局完成后再加载。 - 要不要考虑屏幕密度?要。如果你用
decodeResource,系统会自动根据 density 缩放。建议用decodeStream或decodeFileDescriptor来避免这层缩放,自己控制采样率。 - 列表滑动时怎么办?配合内存缓存和磁盘缓存一起用。降采样虽然省内存,但解码还是有一定开销。频繁解码会影响滑动流畅度。