Android Bitmap 高级篇:Hardware Bitmap、HDR 与性能优化实战

深入解析 Hardware Bitmap 原理、HDR 图像支持、色彩空间管理,以及生产级性能优化方案

一、Hardware Bitmap 深度解析

1.1 Hardware Bitmap 原理

传统 Bitmap vs Hardware Bitmap

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
传统 Bitmap (ARGB_8888):
    ┌─────────────┐
    │  Java对象   │ ← 8-16 字节
    └─────┬───────┘
          │ mNativePtr
    ┌─────────────┐
    │Native SkBitmap│ ← 小元数据
    └─────┬───────┘
    ┌─────────────┐
    │像素数据(RAM)│ ← 1920×1080×4 = 8.3MB
    │CPU可读写    │
    └─────────────┘

Hardware Bitmap:
    ┌─────────────┐
    │  Java对象   │ ← 8-16 字节
    └─────┬───────┘
          │ mNativePtr
    ┌─────────────┐
    │Native SkBitmap│ ← 小元数据
    └─────┬───────┘
          │ GraphicBuffer 引用
    ┌─────────────┐
    │GPU纹理内存  │ ← 8.3MB
    │CPU不可访问  │ ← getPixel() 会崩溃!
    └─────────────┘

核心优势

  1. 零拷贝渲染:直接在 GPU 纹理内存,无需 CPU→GPU 拷贝
  2. 内存压力降低:不占用 RAM,释放给其他模块
  3. 渲染性能提升:避免上传纹理的开销

1.2 创建 Hardware Bitmap

方法一:从现有 Bitmap 转换

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class HardwareBitmapCreator {
    
    /**
     * 转换为 Hardware Bitmap (Android 8.0+)
     */
    @RequiresApi(Build.VERSION_CODES.O)
    public Bitmap toHardwareBitmap(Bitmap source) {
        // 检查前置条件
        if (source.getConfig() == Bitmap.Config.HARDWARE) {
            return source; // 已经是 Hardware Bitmap
        }
        
        if (source.isMutable()) {
            throw new IllegalArgumentException(
                "Cannot create hardware bitmap from mutable bitmap"
            );
        }
        
        // 创建 Hardware Bitmap
        Bitmap hardware = source.copy(Bitmap.Config.HARDWARE, false);
        
        // 可选:释放原 Bitmap
        source.recycle();
        
        return hardware;
    }
}

方法二:解码时直接创建

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class HardwareBitmapDecoder {
    
    /**
     * 解码为 Hardware Bitmap
     */
    @RequiresApi(Build.VERSION_CODES.O)
    public Bitmap decodeAsHardware(InputStream stream) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inPreferredConfig = Bitmap.Config.HARDWARE;
        
        return BitmapFactory.decodeStream(stream, null, options);
    }
    
    /**
     * 带降采样的 Hardware Bitmap 解码
     */
    @RequiresApi(Build.VERSION_CODES.O)
    public Bitmap decodeAsHardwareWithSampling(
            String path, int reqWidth, int reqHeight) {
        
        // 1. 获取尺寸
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFile(path, options);
        
        // 2. 计算采样率
        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
        
        // 3. 解码为 Hardware Bitmap
        options.inJustDecodeBounds = false;
        options.inPreferredConfig = Bitmap.Config.HARDWARE;
        
        return BitmapFactory.decodeFile(path, options);
    }
}

1.3 Hardware Bitmap 限制与陷阱

限制清单

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public class HardwareBitmapLimitations {
    
    /**
     * 演示 Hardware Bitmap 的各种限制
     */
    @RequiresApi(Build.VERSION_CODES.O)
    public void demonstrateLimitations() {
        Bitmap hwBitmap = createHardwareBitmap();
        
        // ❌ 限制 1:无法读取像素
        try {
            int pixel = hwBitmap.getPixel(0, 0);
        } catch (IllegalStateException e) {
            // 抛出异常:Cannot call getPixel on a hardware bitmap
        }
        
        // ❌ 限制 2:无法修改
        try {
            hwBitmap.setPixel(0, 0, Color.RED);
        } catch (IllegalStateException e) {
            // 抛出异常:Cannot modify an immutable bitmap
        }
        
        // ❌ 限制 3:无法用于 Canvas 绘制
        try {
            Canvas canvas = new Canvas(hwBitmap);
        } catch (IllegalStateException e) {
            // 抛出异常:Immutable bitmap passed to Canvas constructor
        }
        
        // ❌ 限制 4:无法 compress()
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            hwBitmap.compress(CompressFormat.JPEG, 90, baos);
        } catch (IllegalStateException e) {
            // 抛出异常:Cannot compress a hardware bitmap
        }
        
        // ❌ 限制 5:无法 copyPixelsToBuffer()
        try {
            ByteBuffer buffer = ByteBuffer.allocate(hwBitmap.getAllocationByteCount());
            hwBitmap.copyPixelsToBuffer(buffer);
        } catch (IllegalStateException e) {
            // 抛出异常:Cannot copy pixels from a hardware bitmap
        }
        
        // ✅ 可以做的操作:
        // 1. 在 ImageView 中显示
        imageView.setImageBitmap(hwBitmap);
        
        // 2. 在 OpenGL 纹理中使用
        GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, hwBitmap, 0);
        
        // 3. 获取基本信息
        int width = hwBitmap.getWidth();
        int height = hwBitmap.getHeight();
        Bitmap.Config config = hwBitmap.getConfig();
    }
}

实战:需要读取像素时的处理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class HardwareBitmapWorkarounds {
    
    /**
     * 临时转换为软件 Bitmap 进行处理
     */
    @RequiresApi(Build.VERSION_CODES.O)
    public Bitmap processHardwareBitmap(Bitmap hwBitmap) {
        // 1. 转换为 ARGB_8888(可读写)
        Bitmap softwareBitmap = hwBitmap.copy(Bitmap.Config.ARGB_8888, true);
        
        // 2. 处理像素
        for (int y = 0; y < softwareBitmap.getHeight(); y++) {
            for (int x = 0; x < softwareBitmap.getWidth(); x++) {
                int pixel = softwareBitmap.getPixel(x, y);
                // 处理...
                softwareBitmap.setPixel(x, y, processedPixel);
            }
        }
        
        // 3. 转回 Hardware Bitmap
        Bitmap result = softwareBitmap.copy(Bitmap.Config.HARDWARE, false);
        softwareBitmap.recycle();
        
        return result;
    }
    
    /**
     * 使用 Shader 在 GPU 上处理(更高效)
     */
    @RequiresApi(Build.VERSION_CODES.O)
    public Bitmap processWithShader(Bitmap hwBitmap, ColorMatrix colorMatrix) {
        // 创建结果 Bitmap
        Bitmap result = Bitmap.createBitmap(
            hwBitmap.getWidth(), 
            hwBitmap.getHeight(), 
            Bitmap.Config.HARDWARE
        );
        
        // 使用 Canvas + ColorFilter 在 GPU 处理
        Canvas canvas = new Canvas(result);
        Paint paint = new Paint();
        paint.setColorFilter(new ColorMatrixColorFilter(colorMatrix));
        canvas.drawBitmap(hwBitmap, 0, 0, paint);
        
        return result;
    }
}

1.4 性能基准测试

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class HardwareBitmapBenchmark {
    
    public void benchmarkRendering() {
        // 准备测试数据
        Bitmap softwareBitmap = BitmapFactory.decodeResource(
            getResources(), R.drawable.large_image
        );
        
        Bitmap hardwareBitmap = null;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            hardwareBitmap = softwareBitmap.copy(Bitmap.Config.HARDWARE, false);
        }
        
        // 测试场景:连续渲染 100 帧
        int frameCount = 100;
        
        // 1. Software Bitmap 渲染
        long startTime = System.nanoTime();
        for (int i = 0; i < frameCount; i++) {
            Canvas canvas = surfaceView.lockCanvas();
            canvas.drawBitmap(softwareBitmap, 0, 0, null);
            surfaceView.unlockCanvasAndPost(canvas);
        }
        long softwareTime = (System.nanoTime() - startTime) / 1_000_000;
        
        // 2. Hardware Bitmap 渲染
        if (hardwareBitmap != null) {
            startTime = System.nanoTime();
            for (int i = 0; i < frameCount; i++) {
                Canvas canvas = surfaceView.lockCanvas();
                canvas.drawBitmap(hardwareBitmap, 0, 0, null);
                surfaceView.unlockCanvasAndPost(canvas);
            }
            long hardwareTime = (System.nanoTime() - startTime) / 1_000_000;
            
            Log.d("Benchmark", String.format(
                "Software: %dms (%.1f fps)\nHardware: %dms (%.1f fps)\nImprovement: %.1f%%",
                softwareTime, 1000.0 * frameCount / softwareTime,
                hardwareTime, 1000.0 * frameCount / hardwareTime,
                (softwareTime - hardwareTime) * 100.0 / softwareTime
            ));
            
            // 典型结果(1920×1080 图片):
            // Software: 3500ms (28.6 fps)
            // Hardware: 1200ms (83.3 fps)
            // Improvement: 65.7%
        }
    }
}

二、HDR 图像支持(Gainmap)

2.1 HDR 图像原理

SDR vs HDR

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
SDR (Standard Dynamic Range):
    亮度范围: 0.1 - 100 nits
    色彩空间: sRGB
    位深: 8-bit (256 级)
    
    ┌───────────────────┐
    │   暗部细节丢失    │  0-10%
    ├───────────────────┤
    │   正常显示区域    │  10-90%
    ├───────────────────┤
    │   高光过曝        │  90-100%
    └───────────────────┘

HDR (High Dynamic Range):
    亮度范围: 0.01 - 10,000 nits
    色彩空间: Display P3 / Rec.2020
    位深: 10-bit+ (1024+ 级)
    
    ┌───────────────────┐
    │   暗部细节保留    │  0-10%
    ├───────────────────┤
    │   正常显示区域    │  10-70%
    ├───────────────────┤
    │   高光细节保留    │  70-100%
    └───────────────────┘

Gainmap 技术

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
传统 HDR 存储:
    基础图像(SDR) + 额外的 HDR 数据
    文件大小: 基础图 × 2

Gainmap 方案(Google 提出):
    基础图像(SDR) + Gainmap(压缩的增益信息)
    文件大小: 基础图 × 1.2-1.3
    
    解码流程:
    SDR 图像 + Gainmap × 显示器亮度因子 = HDR 图像

2.2 检测与解码 HDR 图像

检测 Gainmap

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) // Android 14
public class HdrImageDetector {
    
    /**
     * 检测图片是否包含 Gainmap
     */
    public boolean hasGainmap(String imagePath) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFile(imagePath, options);
        
        // 检查 MIME 类型中的 HDR 标记
        String mimeType = options.outMimeType;
        return mimeType != null && mimeType.contains("image/avif") 
            || mimeType.contains("image/heif");
    }
    
    /**
     * 解码 HDR 图像
     */
    public Bitmap decodeHdrImage(String path) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        
        // 启用 Gainmap 解码
        options.inPreferredConfig = Bitmap.Config.RGBA_F16; // 支持 HDR
        
        Bitmap bitmap = BitmapFactory.decodeFile(path, options);
        
        // 检查是否包含 Gainmap
        if (bitmap != null && bitmap.hasGainmap()) {
            Gainmap gainmap = bitmap.getGainmap();
            Log.d("HDR", "Gainmap info: " + gainmap.toString());
        }
        
        return bitmap;
    }
}

Gainmap API

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public class GainmapProcessor {
    
    /**
     * 提取 Gainmap
     */
    public Bitmap extractGainmap(Bitmap hdrBitmap) {
        if (!hdrBitmap.hasGainmap()) {
            return null;
        }
        
        Gainmap gainmap = hdrBitmap.getGainmap();
        
        // 获取 Gainmap 的 Bitmap 数据
        Bitmap gainmapBitmap = gainmap.getGainmapContents();
        
        // 获取 Gainmap 元数据
        Gainmap.Metadata metadata = gainmap.getMetadata();
        float[] ratioMin = metadata.getRatioMin(); // [R, G, B]
        float[] ratioMax = metadata.getRatioMax();
        float[] gamma = metadata.getGamma();
        
        Log.d("Gainmap", String.format(
            "Size: %dx%d, RatioMin: [%.2f, %.2f, %.2f], RatioMax: [%.2f, %.2f, %.2f]",
            gainmapBitmap.getWidth(), gainmapBitmap.getHeight(),
            ratioMin[0], ratioMin[1], ratioMin[2],
            ratioMax[0], ratioMax[1], ratioMax[2]
        ));
        
        return gainmapBitmap;
    }
    
    /**
     * 创建带 Gainmap 的图像
     */
    public Bitmap createHdrBitmap(Bitmap sdrBase, Bitmap gainmapBitmap) {
        // 创建 Gainmap 对象
        Gainmap.Metadata metadata = new Gainmap.Metadata.Builder()
            .setRatioMin(new float[]{1.0f, 1.0f, 1.0f})
            .setRatioMax(new float[]{8.0f, 8.0f, 8.0f})
            .setGamma(new float[]{1.0f, 1.0f, 1.0f})
            .setEpsilonSdr(new float[]{0.0f, 0.0f, 0.0f})
            .setEpsilonHdr(new float[]{0.0f, 0.0f, 0.0f})
            .setDisplayRatioForFullHdr(1.0f)
            .setMinDisplayRatioForHdrTransition(1.0f)
            .build();
        
        Gainmap gainmap = new Gainmap(gainmapBitmap, metadata);
        
        // 将 Gainmap 附加到基础图像
        Bitmap result = sdrBase.copy(Bitmap.Config.ARGB_8888, true);
        result.setGainmap(gainmap);
        
        return result;
    }
}

2.3 HDR 显示适配

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public class HdrDisplayAdapter {
    
    /**
     * 检测设备是否支持 HDR
     */
    public boolean isHdrDisplaySupported(Context context) {
        Display display = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE))
            .getDefaultDisplay();
        
        Display.HdrCapabilities hdrCapabilities = display.getHdrCapabilities();
        int[] supportedHdrTypes = hdrCapabilities.getSupportedHdrTypes();
        
        // 检查支持的 HDR 类型
        for (int type : supportedHdrTypes) {
            if (type == Display.HdrCapabilities.HDR_TYPE_HDR10 ||
                type == Display.HdrCapabilities.HDR_TYPE_HDR10_PLUS ||
                type == Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * 根据显示器特性调整 Gainmap
     */
    public void adjustForDisplay(ImageView imageView, Bitmap hdrBitmap) {
        if (!hdrBitmap.hasGainmap()) {
            imageView.setImageBitmap(hdrBitmap);
            return;
        }
        
        Display display = imageView.getDisplay();
        if (display == null) {
            imageView.setImageBitmap(hdrBitmap);
            return;
        }
        
        Display.HdrCapabilities hdrCapabilities = display.getHdrCapabilities();
        float maxLuminance = hdrCapabilities.getDesiredMaxLuminance();
        float minLuminance = hdrCapabilities.getDesiredMinLuminance();
        
        // 根据显示器亮度范围调整 Gainmap 参数
        Gainmap gainmap = hdrBitmap.getGainmap();
        Gainmap.Metadata metadata = gainmap.getMetadata();
        
        float displayRatio = maxLuminance / 100.0f; // SDR 基准 100 nits
        
        Gainmap.Metadata adjusted = new Gainmap.Metadata.Builder(metadata)
            .setDisplayRatioForFullHdr(displayRatio)
            .build();
        
        Gainmap adjustedGainmap = new Gainmap(gainmap.getGainmapContents(), adjusted);
        
        Bitmap result = hdrBitmap.copy(Bitmap.Config.RGBA_F16, false);
        result.setGainmap(adjustedGainmap);
        
        imageView.setImageBitmap(result);
    }
}

三、色彩空间管理

3.1 色彩空间概述

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
常见色彩空间对比:

sRGB (Standard RGB):
    色域: 小
    用途: 网页、SDR 显示
    覆盖率: ~35% CIE 1931
    
Display P3:
    色域: 中
    用途: iPhone、HDR 显示
    覆盖率: ~45% CIE 1931
    比 sRGB 多 25% 色彩
    
Adobe RGB:
    色域: 中大
    用途: 摄影、印刷
    覆盖率: ~50% CIE 1931
    
Rec.2020 (BT.2020):
    色域: 大
    用途: 4K/8K HDR 视频
    覆盖率: ~75% CIE 1931

3.2 ColorSpace API

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
@RequiresApi(Build.VERSION_CODES.O)
public class ColorSpaceManager {
    
    /**
     * 获取 Bitmap 的色彩空间
     */
    public void inspectColorSpace(Bitmap bitmap) {
        ColorSpace colorSpace = bitmap.getColorSpace();
        
        if (colorSpace != null) {
            Log.d("ColorSpace", "Name: " + colorSpace.getName());
            Log.d("ColorSpace", "Model: " + colorSpace.getModel());
            Log.d("ColorSpace", "Is WideGamut: " + colorSpace.isWideGamut());
            
            // 获取色域覆盖率
            if (colorSpace instanceof ColorSpace.Rgb) {
                ColorSpace.Rgb rgb = (ColorSpace.Rgb) colorSpace;
                float[] whitePoint = rgb.getWhitePoint();
                float[] primaries = rgb.getPrimaries();
                
                Log.d("ColorSpace", String.format(
                    "White Point: [%.4f, %.4f]\nPrimaries: R[%.4f, %.4f] G[%.4f, %.4f] B[%.4f, %.4f]",
                    whitePoint[0], whitePoint[1],
                    primaries[0], primaries[1],
                    primaries[2], primaries[3],
                    primaries[4], primaries[5]
                ));
            }
        }
    }
    
    /**
     * 转换色彩空间
     */
    public Bitmap convertColorSpace(Bitmap source, ColorSpace target) {
        ColorSpace sourceCS = source.getColorSpace();
        
        if (sourceCS != null && sourceCS.equals(target)) {
            return source; // 已经是目标色彩空间
        }
        
        // 创建结果 Bitmap
        Bitmap result = Bitmap.createBitmap(
            source.getWidth(),
            source.getHeight(),
            source.getConfig(),
            source.hasAlpha(),
            target
        );
        
        // 使用 Canvas 进行色彩空间转换
        Canvas canvas = new Canvas(result);
        canvas.drawBitmap(source, 0, 0, null);
        
        return result;
    }
    
    /**
     * 解码时指定色彩空间
     */
    public Bitmap decodeWithColorSpace(String path, ColorSpace.Named colorSpace) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inPreferredColorSpace = ColorSpace.get(colorSpace);
        
        return BitmapFactory.decodeFile(path, options);
    }
}

3.3 广色域显示适配

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
@RequiresApi(Build.VERSION_CODES.O)
public class WideGamutAdapter {
    
    /**
     * 检测设备是否支持广色域
     */
    public boolean isWideGamutSupported(Context context) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            Display display = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE))
                .getDefaultDisplay();
            
            return display.isWideColorGamut();
        }
        return false;
    }
    
    /**
     * 为广色域显示器优化图片
     */
    public Bitmap optimizeForWideGamut(Bitmap source) {
        // 1. 检查源图片色彩空间
        ColorSpace sourceCS = source.getColorSpace();
        if (sourceCS == null) {
            sourceCS = ColorSpace.get(ColorSpace.Named.SRGB);
        }
        
        // 2. 如果已经是广色域,直接返回
        if (sourceCS.isWideGamut()) {
            return source;
        }
        
        // 3. 转换为 Display P3
        ColorSpace targetCS = ColorSpace.get(ColorSpace.Named.DISPLAY_P3);
        
        Bitmap result = Bitmap.createBitmap(
            source.getWidth(),
            source.getHeight(),
            Bitmap.Config.ARGB_8888,
            source.hasAlpha(),
            targetCS
        );
        
        Canvas canvas = new Canvas(result);
        canvas.drawBitmap(source, 0, 0, null);
        
        return result;
    }
    
    /**
     * 根据显示器能力自动选择色彩空间
     */
    public ColorSpace selectOptimalColorSpace(Context context) {
        if (!isWideGamutSupported(context)) {
            return ColorSpace.get(ColorSpace.Named.SRGB);
        }
        
        // 检查显示器支持的色彩空间
        // 这里简化处理,实际应查询 Display.getPreferredWideGamutColorSpace()
        return ColorSpace.get(ColorSpace.Named.DISPLAY_P3);
    }
}

四、性能优化实战

4.1 大图滚动优化

方案:分块渲染 + 预加载

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
public class TiledBitmapView extends View {
    
    private BitmapRegionDecoder mDecoder;
    private final int TILE_SIZE = 512; // 瓦片尺寸
    
    // LRU 瓦片缓存
    private final LruCache<String, Bitmap> mTileCache;
    
    // 预加载队列
    private final ExecutorService mPreloadExecutor;
    
    public TiledBitmapView(Context context) {
        super(context);
        
        // 初始化缓存(16MB)
        int cacheSize = 16 * 1024 * 1024;
        mTileCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                return value.getAllocationByteCount();
            }
        };
        
        // 初始化预加载线程池
        mPreloadExecutor = Executors.newFixedThreadPool(2);
    }
    
    public void setImage(InputStream stream) throws IOException {
        mDecoder = BitmapRegionDecoder.newInstance(stream, false);
        invalidate();
    }
    
    @Override
    protected void onDraw(Canvas canvas) {
        if (mDecoder == null) return;
        
        // 计算可见区域
        Rect visibleRect = getVisibleRect();
        
        // 计算需要的瓦片
        int startTileX = visibleRect.left / TILE_SIZE;
        int endTileX = (visibleRect.right + TILE_SIZE - 1) / TILE_SIZE;
        int startTileY = visibleRect.top / TILE_SIZE;
        int endTileY = (visibleRect.bottom + TILE_SIZE - 1) / TILE_SIZE;
        
        // 渲染可见瓦片
        for (int ty = startTileY; ty < endTileY; ty++) {
            for (int tx = startTileX; tx < endTileX; tx++) {
                Bitmap tile = getTile(tx, ty);
                if (tile != null) {
                    canvas.drawBitmap(tile, tx * TILE_SIZE, ty * TILE_SIZE, null);
                }
            }
        }
        
        // 预加载周边瓦片
        preloadSurroundingTiles(startTileX, endTileX, startTileY, endTileY);
    }
    
    private Bitmap getTile(int tx, int ty) {
        String key = tx + "_" + ty;
        Bitmap cached = mTileCache.get(key);
        
        if (cached != null) {
            return cached;
        }
        
        // 解码瓦片
        Rect tileRect = new Rect(
            tx * TILE_SIZE,
            ty * TILE_SIZE,
            (tx + 1) * TILE_SIZE,
            (ty + 1) * TILE_SIZE
        );
        
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inPreferredConfig = Bitmap.Config.RGB_565; // 节省内存
        
        Bitmap tile = mDecoder.decodeRegion(tileRect, options);
        
        if (tile != null) {
            mTileCache.put(key, tile);
        }
        
        return tile;
    }
    
    private void preloadSurroundingTiles(int startX, int endX, int startY, int endY) {
        // 预加载周边 1 圈瓦片
        for (int ty = startY - 1; ty <= endY; ty++) {
            for (int tx = startX - 1; tx <= endX; tx++) {
                if (tx < startX || tx >= endX || ty < startY || ty >= endY) {
                    preloadTile(tx, ty);
                }
            }
        }
    }
    
    private void preloadTile(int tx, int ty) {
        String key = tx + "_" + ty;
        
        if (mTileCache.get(key) != null) {
            return; // 已缓存
        }
        
        mPreloadExecutor.execute(() -> getTile(tx, ty));
    }
}

4.2 列表滚动优化

方案:分层加载 + 优先级队列

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
public class OptimizedImageLoader {
    
    // 优先级队列
    private final PriorityBlockingQueue<LoadTask> mTaskQueue;
    private final ExecutorService mExecutor;
    
    // Bitmap 池
    private final LruBitmapPool mBitmapPool;
    
    public OptimizedImageLoader() {
        mTaskQueue = new PriorityBlockingQueue<>(100, (t1, t2) -> 
            Integer.compare(t2.priority, t1.priority) // 高优先级优先
        );
        
        mExecutor = Executors.newFixedThreadPool(3, r -> {
            Thread t = new Thread(r);
            t.setPriority(Thread.NORM_PRIORITY - 1); // 降低优先级
            return t;
        });
        
        mBitmapPool = new LruBitmapPool(20 * 1024 * 1024);
        
        startWorkers();
    }
    
    /**
     * 加载图片(带优先级)
     */
    public void loadImage(String url, ImageView target, int priority) {
        // 取消之前的任务
        Object tag = target.getTag();
        if (tag instanceof LoadTask) {
            ((LoadTask) tag).cancel();
        }
        
        // 创建新任务
        LoadTask task = new LoadTask(url, target, priority);
        target.setTag(task);
        
        mTaskQueue.offer(task);
    }
    
    /**
     * 分层加载:占位图 → 缩略图 → 原图
     */
    public void loadImageProgressive(String url, ImageView target) {
        // 1. 立即显示占位图
        target.setImageResource(R.drawable.placeholder);
        
        // 2. 加载缩略图(高优先级)
        loadImage(getThumbnailUrl(url), target, Priority.HIGH);
        
        // 3. 加载原图(低优先级)
        loadImage(url, target, Priority.NORMAL);
    }
    
    private void startWorkers() {
        for (int i = 0; i < 3; i++) {
            mExecutor.execute(() -> {
                while (true) {
                    try {
                        LoadTask task = mTaskQueue.take();
                        
                        if (!task.isCancelled()) {
                            Bitmap bitmap = decodeWithPool(task.url, task.target);
                            
                            if (!task.isCancelled() && bitmap != null) {
                                new Handler(Looper.getMainLooper()).post(() -> 
                                    task.target.setImageBitmap(bitmap)
                                );
                            }
                        }
                    } catch (InterruptedException e) {
                        break;
                    }
                }
            });
        }
    }
    
    private Bitmap decodeWithPool(String url, ImageView target) {
        // 使用 BitmapPool 复用
        int targetWidth = target.getWidth();
        int targetHeight = target.getHeight();
        
        if (targetWidth <= 0 || targetHeight <= 0) {
            targetWidth = 800;
            targetHeight = 600;
        }
        
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFile(url, options);
        
        options.inSampleSize = calculateInSampleSize(options, targetWidth, targetHeight);
        options.inBitmap = mBitmapPool.getDirty(
            options.outWidth / options.inSampleSize,
            options.outHeight / options.inSampleSize,
            Bitmap.Config.ARGB_8888
        );
        options.inMutable = true;
        options.inJustDecodeBounds = false;
        
        return BitmapFactory.decodeFile(url, options);
    }
    
    interface Priority {
        int LOW = 0;
        int NORMAL = 1;
        int HIGH = 2;
    }
    
    private static class LoadTask {
        final String url;
        final ImageView target;
        final int priority;
        volatile boolean cancelled;
        
        LoadTask(String url, ImageView target, int priority) {
            this.url = url;
            this.target = target;
            this.priority = priority;
        }
        
        void cancel() {
            cancelled = true;
        }
        
        boolean isCancelled() {
            return cancelled || target.getTag() != this;
        }
    }
}

4.3 内存压力响应

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
public class BitmapMemoryManager implements ComponentCallbacks2 {
    
    private final List<LruBitmapPool> mBitmapPools = new ArrayList<>();
    private final List<WeakReference<Bitmap>> mTrackedBitmaps = new ArrayList<>();
    
    public void registerPool(LruBitmapPool pool) {
        mBitmapPools.add(pool);
    }
    
    public void trackBitmap(Bitmap bitmap) {
        mTrackedBitmaps.add(new WeakReference<>(bitmap));
    }
    
    @Override
    public void onTrimMemory(int level) {
        switch (level) {
            case TRIM_MEMORY_UI_HIDDEN:
                // UI 不可见,适度清理
                trimBitmapPools(0.5f);
                break;
                
            case TRIM_MEMORY_BACKGROUND:
                // 进入后台,清理 50%
                trimBitmapPools(0.5f);
                clearUnusedBitmaps();
                break;
                
            case TRIM_MEMORY_MODERATE:
                // 内存紧张,清理 70%
                trimBitmapPools(0.3f);
                clearUnusedBitmaps();
                break;
                
            case TRIM_MEMORY_RUNNING_CRITICAL:
            case TRIM_MEMORY_COMPLETE:
                // 极度紧张,全部清空
                clearAllBitmapPools();
                clearUnusedBitmaps();
                System.gc(); // 强制 GC
                break;
        }
    }
    
    private void trimBitmapPools(float retainFraction) {
        for (LruBitmapPool pool : mBitmapPools) {
            pool.trimToSize((long) (pool.getMaxSize() * retainFraction));
        }
    }
    
    private void clearAllBitmapPools() {
        for (LruBitmapPool pool : mBitmapPools) {
            pool.clearMemory();
        }
    }
    
    private void clearUnusedBitmaps() {
        Iterator<WeakReference<Bitmap>> iterator = mTrackedBitmaps.iterator();
        int recycledCount = 0;
        
        while (iterator.hasNext()) {
            Bitmap bitmap = iterator.next().get();
            
            if (bitmap == null) {
                iterator.remove();
            } else if (!bitmap.isRecycled() && bitmap.isMutable()) {
                // 释放未使用的 Bitmap
                bitmap.recycle();
                recycledCount++;
                iterator.remove();
            }
        }
        
        Log.d("MemoryManager", "Recycled " + recycledCount + " bitmaps");
    }
    
    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        // 处理配置变化
    }
    
    @Override
    public void onLowMemory() {
        // 等同于 TRIM_MEMORY_COMPLETE
        onTrimMemory(TRIM_MEMORY_COMPLETE);
    }
}

五、Bitmap 安全实践

5.1 防止 OOM

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
public class BitmapOomPrevention {
    
    /**
     * 安全解码:带内存检查
     */
    public Bitmap safeDecodeBitmap(String path, int maxMemoryMB) {
        // 1. 获取图片尺寸
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFile(path, options);
        
        // 2. 计算所需内存
        int width = options.outWidth;
        int height = options.outHeight;
        int requiredMemory = width * height * 4; // ARGB_8888
        
        // 3. 检查可用内存
        Runtime runtime = Runtime.getRuntime();
        long availableMemory = runtime.maxMemory() - 
            (runtime.totalMemory() - runtime.freeMemory());
        
        if (requiredMemory > availableMemory) {
            // 计算安全的采样率
            int sampleSize = 1;
            while (requiredMemory / (sampleSize * sampleSize) > availableMemory / 2) {
                sampleSize *= 2;
            }
            
            options.inSampleSize = sampleSize;
            Log.w("OOM", String.format(
                "Image too large (%dx%d, %.2fMB), using sampleSize=%d",
                width, height, requiredMemory / 1024.0 / 1024.0, sampleSize
            ));
        }
        
        // 4. 解码
        options.inJustDecodeBounds = false;
        options.inPreferredConfig = Bitmap.Config.ARGB_8888;
        
        return BitmapFactory.decodeFile(path, options);
    }
    
    /**
     * 捕获 OOM 并降级处理
     */
    public Bitmap decodeWithFallback(String path) {
        try {
            return BitmapFactory.decodeFile(path);
        } catch (OutOfMemoryError e) {
            Log.e("OOM", "First attempt failed, trying with RGB_565", e);
            
            // 降级到 RGB_565
            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inPreferredConfig = Bitmap.Config.RGB_565;
            
            try {
                return BitmapFactory.decodeFile(path, options);
            } catch (OutOfMemoryError e2) {
                Log.e("OOM", "Second attempt failed, trying with sample size", e2);
                
                // 进一步降采样
                options.inSampleSize = 2;
                return BitmapFactory.decodeFile(path, options);
            }
        }
    }
}

5.2 防止内存泄漏

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
public class BitmapLeakPrevention {
    
    /**
     * 使用 WeakReference 避免泄漏
     */
    public static class BitmapCache {
        private final Map<String, WeakReference<Bitmap>> cache = new HashMap<>();
        
        public void put(String key, Bitmap bitmap) {
            cache.put(key, new WeakReference<>(bitmap));
        }
        
        public Bitmap get(String key) {
            WeakReference<Bitmap> ref = cache.get(key);
            if (ref != null) {
                Bitmap bitmap = ref.get();
                if (bitmap != null && !bitmap.isRecycled()) {
                    return bitmap;
                } else {
                    cache.remove(key);
                }
            }
            return null;
        }
    }
    
    /**
     * ImageView 泄漏检测
     */
    public void detectImageViewLeaks(Activity activity) {
        ViewGroup root = (ViewGroup) activity.findViewById(android.R.id.content);
        List<ImageView> imageViews = findAllImageViews(root);
        
        for (ImageView iv : imageViews) {
            Drawable drawable = iv.getDrawable();
            if (drawable instanceof BitmapDrawable) {
                Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
                
                if (bitmap != null && !bitmap.isRecycled()) {
                    long size = bitmap.getAllocationByteCount();
                    if (size > 5 * 1024 * 1024) { // 5MB+
                        Log.w("Leak", String.format(
                            "Large bitmap in ImageView: %.2fMB at %s",
                            size / 1024.0 / 1024.0,
                            iv.getTag()
                        ));
                    }
                }
            }
        }
    }
    
    private List<ImageView> findAllImageViews(View view) {
        List<ImageView> result = new ArrayList<>();
        
        if (view instanceof ImageView) {
            result.add((ImageView) view);
        } else if (view instanceof ViewGroup) {
            ViewGroup group = (ViewGroup) view;
            for (int i = 0; i < group.getChildCount(); i++) {
                result.addAll(findAllImageViews(group.getChildAt(i)));
            }
        }
        
        return result;
    }
}

六、性能监控面板

6.1 实时监控 UI

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
public class BitmapMonitorView extends View {
    
    private final BitmapMemoryManager memoryManager;
    private final Paint textPaint;
    
    private long totalBitmapMemory;
    private int bitmapCount;
    private long javaHeapUsed;
    private long nativeHeapUsed;
    
    public BitmapMonitorView(Context context, BitmapMemoryManager manager) {
        super(context);
        this.memoryManager = manager;
        
        textPaint = new Paint();
        textPaint.setColor(Color.WHITE);
        textPaint.setTextSize(24);
        textPaint.setAntiAlias(true);
        
        // 定时刷新
        Handler handler = new Handler(Looper.getMainLooper());
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                updateStats();
                invalidate();
                handler.postDelayed(this, 1000); // 每秒刷新
            }
        }, 1000);
    }
    
    private void updateStats() {
        // Java 堆
        Runtime runtime = Runtime.getRuntime();
        javaHeapUsed = runtime.totalMemory() - runtime.freeMemory();
        
        // Native 堆
        nativeHeapUsed = Debug.getNativeHeapAllocatedSize();
        
        // Bitmap 统计
        totalBitmapMemory = memoryManager.getTotalBitmapMemory();
        bitmapCount = memoryManager.getBitmapCount();
    }
    
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        
        int y = 50;
        int lineHeight = 40;
        
        // 背景
        canvas.drawColor(0x80000000);
        
        // 显示统计信息
        canvas.drawText(String.format("Java Heap: %.1f MB", 
            javaHeapUsed / 1024.0 / 1024.0), 20, y, textPaint);
        y += lineHeight;
        
        canvas.drawText(String.format("Native Heap: %.1f MB", 
            nativeHeapUsed / 1024.0 / 1024.0), 20, y, textPaint);
        y += lineHeight;
        
        canvas.drawText(String.format("Bitmap Memory: %.1f MB (%d bitmaps)", 
            totalBitmapMemory / 1024.0 / 1024.0, bitmapCount), 20, y, textPaint);
        y += lineHeight;
        
        // 池状态
        for (LruBitmapPool pool : memoryManager.getPools()) {
            canvas.drawText(String.format("Pool: %.1f/%.1f MB", 
                pool.getCurrentSize() / 1024.0 / 1024.0,
                pool.getMaxSize() / 1024.0 / 1024.0), 20, y, textPaint);
            y += lineHeight;
        }
    }
}

七、总结与最佳实践

7.1 技术选型指南

场景 推荐方案 原因
静态展示(不需读写) Hardware Bitmap 渲染性能最优
需要像素处理 ARGB_8888 支持读写操作
列表缩略图 RGB_565 + BitmapPool 内存占用低
超大图查看 BitmapRegionDecoder 分块加载
HDR 照片 Gainmap + RGBA_F16 保留动态范围
广色域显示 Display P3 色彩更鲜艳

7.2 性能优化 Checklist

  • 启用 Hardware Bitmap:Android 8.0+ 静态图片
  • 使用 BitmapPool:减少 GC 压力
  • 降采样加载:缩略图场景必须
  • 分块渲染:超大图场景
  • 响应内存警告:实现 onTrimMemory()
  • 监控内存泄漏:使用 LeakCanary
  • 优化列表滚动:优先级队列 + 预加载
  • 支持 HDR:Android 14+ 高端设备

参考资料

  1. Android Developers - Hardware Bitmaps
  2. Android 14 - Gainmap API
  3. ColorSpace API Documentation
  4. Google I/O - Advanced Android Graphics
  5. Wide Color Gamut Guide
使用 Hugo 构建
主题 StackJimmy 设计