BitmapFactory详解:Options、inSampleSize与inJustDecodeBounds

说到Bitmap加载,绕不开的就是BitmapFactory。这个类就像一把瑞士军刀,功能很全,但用不好也容易伤到手。今天我就把它的几个核心参数掰开揉碎了讲清楚。

BitmapFactory.Options——解码的“控制面板”

先说说Options这个类。它本质上就是一个配置容器,你往里面塞参数,BitmapFactory就按你的意思去解码。我刚开始接触时觉得它很啰嗦,后来才发现——没有Options,你根本没法控制内存

来看一个最基础的用法:

BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.photo, options);

这里我指定了RGB_565格式,每个像素只占2字节。相比默认的ARGB_8888(4字节/像素),内存直接砍半。嗯,代价就是颜色精度下降,但很多场景下肉眼根本看不出来。

我的习惯: 如果图片不需要透明度,我永远用RGB_565。比如用户头像、商品缩略图,省一半内存它不香吗?

inSampleSize——缩放的魔法数字

这个参数太重要了。说白了,它就是告诉解码器:“别把原图全塞进来,给我按比例缩小”。

举个例子:一张1920x1080的图片,你ImageView只有480x270。如果不做采样,内存里会塞进一张全尺寸的Bitmap。但如果你设置inSampleSize = 4,解码出来的就是480x270,内存占用只有原来的1/16。

BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 4;  // 宽高各缩小为1/4
Bitmap bitmap = BitmapFactory.decodeFile("/sdcard/photo.jpg", options);
注意: inSampleSize必须是2的幂次方。2、4、8、16...如果你设成3,解码器会向下取整到2。我曾经踩过这个坑,设了个3,结果图片比预期大了一倍。

那怎么计算合适的采样值呢?我一般这么干:

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;
}

这段代码的逻辑很简单:不断尝试2倍缩放,直到宽高都小于目标尺寸。你想想看,如果原图4000x3000,目标800x600,那inSampleSize就是4,解码出来1000x750,稍微大一点但可以接受。

inJustDecodeBounds——先“偷看”再解码

这个参数是我个人最喜欢的。它就像一个侦察兵,先摸清敌情再决定怎么打。

设置inJustDecodeBounds = true时,解码器只读取图片的宽高、格式等信息,不加载像素数据。内存占用几乎为零。

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

// 此时options里已经有宽高了
int imageWidth = options.outWidth;
int imageHeight = options.outHeight;

// 根据宽高计算采样值
options.inSampleSize = calculateInSampleSize(options, 200, 200);

// 正式解码
options.inJustDecodeBounds = false;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.photo, options);

这个流程我几乎每个项目都用。先预读,再计算采样,最后正式解码。三步走,内存稳稳的。

避坑指南: 我曾经在预读后忘记把inJustDecodeBounds设回false,结果decode出来一直是null。排查了半天才发现是这个小问题。所以记得:预读完一定要重置这个标志位。

完整的工作流

把上面几个知识点串起来,就是一个标准的Bitmap加载流程:

  1. 创建Options对象
  2. 设置inJustDecodeBounds = true,预读宽高
  3. 根据目标尺寸计算inSampleSize
  4. 设置inJustDecodeBounds = false
  5. 设置inPreferredConfig等参数
  6. 调用decode方法,拿到缩放过后的Bitmap

你想想看,如果不做这些预处理,直接加载一张4000x3000的图片,内存占用就是4000*3000*4 = 48MB。而经过采样后,可能只需要3MB。这差距,够明显吧?

其他实用参数

参数名 作用 我的建议
inMutable 是否允许修改Bitmap像素 不需要修改时设为false,性能更好
inBitmap 复用已有的Bitmap内存 配合缓存池使用,减少GC压力
inDensity 图片的原始密度 从资源加载时系统会自动处理
inTargetDensity 目标屏幕密度 用于自动缩放,注意别重复缩放

说实话,这些参数我平时用得不多。但inBitmap我强烈建议你了解一下——它能让Bitmap复用,避免频繁创建和回收。后面讲缓存策略时我会详细展开。

一个小技巧: 如果你从网络加载图片,建议先用inJustDecodeBounds获取宽高,再根据ImageView的尺寸计算采样值。这样不管服务器返回多大尺寸的图,客户端都能优雅处理。

好了,BitmapFactory的核心用法就这些。说白了就是三件事:控制格式、控制尺寸、预读信息。把这三点吃透了,Bitmap加载这块你就掌握了八成。下一章我们聊聊缓存策略,那才是真正考验设计能力的地方。