4、区域解码(BitmapRegionDecoder):只加载图片的可见区域

说实话,我第一次遇到超大图加载的问题,是在做一个地图预览功能的时候。一张卫星图 8000x6000 像素,直接 BitmapFactory 加载,OOM 了三次。后来我翻源码发现了 BitmapRegionDecoder,才明白——你根本不需要把整张图塞进内存。

BitmapRegionDecoder 的核心思路很简单:只解码图片的某个矩形区域。它不会把整张图片的像素数据加载到内存,而是根据你指定的 Rect,只读取那一块。说白了,就是「按需加载」。

适用场景:长图(微博、朋友圈)、地图瓦片、超大分辨率海报、医学影像局部查看。

4.1 基本用法

使用 BitmapRegionDecoder 只需要三步:

  1. 创建解码器实例
  2. 指定要解码的区域(Rect)
  3. 设置 BitmapFactory.Options 控制采样率
// 1. 创建解码器
InputStream is = getAssets().open("big_map.jpg");
BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is, false);

// 2. 定义要显示的矩形区域
Rect rect = new Rect(100, 200, 600, 800);

// 3. 设置解码参数
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 2;  // 采样率,控制输出大小
options.inPreferredConfig = Bitmap.Config.RGB_565;  // 节省一半内存

// 4. 解码指定区域
Bitmap bitmap = decoder.decodeRegion(rect, options);

// 用完记得释放
imageView.setImageBitmap(bitmap);
decoder.recycle();

嗯,这里要注意:newInstance 的第二个参数 isShareable 我一般传 false。传 true 在某些机型上会出问题,我踩过这个坑。

4.2 核心参数对比

参数 作用 我的建议
inSampleSize 采样率,2 表示宽高各缩 1/2 根据可见区域大小动态计算,不要写死
inPreferredConfig 像素格式,ARGB_8888 / RGB_565 非透明图用 RGB_565,内存减半
inBitmap 复用已有 Bitmap 内存 配合区域解码效果很好,减少 GC
inMutable 是否可复用 需要复用 inBitmap 时设为 true

小技巧:我习惯把 inSampleSize 设为 2 的幂次(1, 2, 4, 8...),这样解码效率最高。非 2 的幂次虽然也能用,但内部会做额外的缩放计算。

4.3 实战:长图局部加载

你想想看,一张 10000x20000 像素的长图,直接加载需要多少内存?
10000 × 20000 × 4 = 800MB。手机直接炸了。

但用 BitmapRegionDecoder,我们只加载屏幕可见的那一块。比如屏幕高度 2000px,采样率 2,那实际解码的像素只有:
10000 × 2000 ÷ 2 ÷ 2 = 5MB。差距 160 倍。

下面是我在一个阅读 App 里用的方案:

public class LongImageView extends View {
    private BitmapRegionDecoder mDecoder;
    private int mImageWidth, mImageHeight;
    private float mScale = 1f;
    private Rect mVisibleRect = new Rect();

    public LongImageView(Context context, InputStream is) {
        super(context);
        try {
            mDecoder = BitmapRegionDecoder.newInstance(is, false);
            mImageWidth = mDecoder.getWidth();
            mImageHeight = mDecoder.getHeight();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        // 计算当前可见区域(考虑滚动和缩放)
        int viewWidth = getWidth();
        int viewHeight = getHeight();

        // 这里简化处理,实际需要根据 scrollX/scrollY 计算
        int left = (int) (getScrollX() / mScale);
        int top = (int) (getScrollY() / mScale);
        int right = left + (int) (viewWidth / mScale);
        int bottom = top + (int) (viewHeight / mScale);

        // 边界保护
        left = Math.max(0, left);
        top = Math.max(0, top);
        right = Math.min(mImageWidth, right);
        bottom = Math.min(mImageHeight, bottom);

        mVisibleRect.set(left, top, right, bottom);

        // 动态计算采样率
        BitmapFactory.Options opts = new BitmapFactory.Options();
        opts.inSampleSize = calculateSampleSize(viewWidth, viewHeight);
        opts.inPreferredConfig = Bitmap.Config.RGB_565;

        // 解码并绘制
        Bitmap bitmap = mDecoder.decodeRegion(mVisibleRect, opts);
        canvas.drawBitmap(bitmap, 0, 0, null);
        bitmap.recycle();  // 用完立即回收
    }

    private int calculateSampleSize(int viewW, int viewH) {
        int sample = 1;
        int regionW = mVisibleRect.width();
        int regionH = mVisibleRect.height();
        while (regionW / sample > viewW * 1.5f 
               || regionH / sample > viewH * 1.5f) {
            sample *= 2;
        }
        return sample;
    }
}

我曾经踩过的坑:decodeRegion 返回的 Bitmap 一定要及时 recycle。在滑动过程中,每帧都会产生新的 Bitmap,如果不回收,内存会迅速涨上去。我建议用 inBitmap 复用,或者用对象池管理。

4.4 区域解码的完整流程

下面这张图展示了区域解码的核心流程:

BitmapRegionDecoder 区域解码流程 原始超大图 BitmapRegionDecoder.newInstance() 计算可见区域 Rect 设置 inSampleSize / inPreferredConfig 关键点 • 只解码可见区域 • 动态计算采样率 • 及时回收 Bitmap • 配合 inBitmap 复用 内存对比 整图加载:800MB 区域解码:5MB 节省:99.4%

4.5 性能优化要点

  • 采样率动态计算:不要用固定值。根据可见区域大小和 View 尺寸动态算,保证清晰度和内存的平衡。
  • 复用 Bitmap:用 inBitmap 复用上一帧的 Bitmap,减少内存分配和 GC 压力。我一般在滑动停止后才更新,滑动过程中用缓存。
  • 预加载相邻区域:用户滑动时,提前解码下一块区域。我习惯预加载当前可见区域上下各 1/3 屏的内容。
  • 线程切换:解码操作放在子线程,解码完切回主线程更新 UI。用 Handler 或协程都行。

我的个人习惯:在列表滑动场景下,我会配合 RecyclerView.OnScrollListener 做节流。滑动停止后才触发区域解码,滑动过程中只显示缓存或占位图。这样既流畅又省内存。

4.6 局限性

BitmapRegionDecoder 不是万能的。它有几个硬伤:

  • 只支持 JPEG 和 PNG:WebP 格式不支持区域解码。我遇到过设计师给了 WebP 长图,只能先转成 JPEG。
  • 不支持旋转:如果图片有 EXIF 旋转信息,解码出来的区域是原始方向的。需要自己做旋转处理。
  • 不支持缩放:它只负责解码,缩放需要你自己用 Matrix 或 Canvas 处理。
  • 文件格式限制:必须是「渐进式」或「顺序式」的 JPEG,有些特殊编码的图片会解码失败。

说白了,BitmapRegionDecoder 适合做「局部查看」的场景。如果你需要同时支持缩放、旋转、滤镜,那还是得用更重的方案,比如 GPU 加速的图片库。