Android Bitmap 创建流程深度分析

本文将分析从 Java 层 Bitmap.createBitmap 调用开始,经过 JNI 层 Bitmap_creator,最终到达 Native 层 allocateHeapBitmap 的完整创建流程,并深入探讨内存分配函数选择及性能耗时点。

1. Java 层:Bitmap.createBitmap

在 Java 层,用户通常通过 Bitmap.createBitmap(...) 静态方法创建位图。

核心代码路径: frameworks/base/graphics/java/android/graphics/Bitmap.java

以最常见的创建方法为例:

1
2
3
public static Bitmap createBitmap(int width, int height, @NonNull Config config, boolean hasAlpha) {
return createBitmap(null, width, height, config, hasAlpha);
}

经过一系列参数检查后,它会调用 native 方法:

1
2
Bitmap bm = nativeCreate(null, 0, width, width, height, config.nativeInt, true,
colorSpace == null ? 0 : colorSpace.getNativeInstance());

nativeCreate 是一个 JNI 方法,它将复杂的创建请求传递给 Native 层。


2. JNI 层:Bitmap_creator

nativeCreate 在 C++ 中对应 Bitmap_creator 函数。

核心代码路径: frameworks/base/libs/hwui/jni/Bitmap.cpp

2.1 关键逻辑分析

static jobject Bitmap_creator(...) 是 JNI 调用的入口点。其核心步骤如下:

  1. 参数映射:将 Java 的 Config 映射为 Skia 的 SkColorType
    1
    SkColorType colorType = GraphicsJNI::legacyBitmapConfigToColorType(configHandle);
  2. 色彩空间处理:获取 Native 层的色彩空间对象。
  3. 构建 SkBitmap 元数据:创建一个 SkBitmap 对象并设置其 SkImageInfo(包含宽高、颜色类型、Alpha 类型和色彩空间)。
  4. 调用 Native 分配器
    1
    sk_sp<Bitmap> nativeBitmap = Bitmap::allocateHeapBitmap(&bitmap);
  5. 包装与返回:分配成功后,通过 createBitmap (JNI helper) 将 Native 层的 Bitmap 指针包装成 Java 层的 Bitmap 对象返回。

3. Native 层:allocateHeapBitmap 与内存分配

3.1 内存分配实现

真正的堆内存分配发生在 Bitmap::allocateHeapBitmap(size_t size, ...) 中:

1
2
3
4
5
6
7
8
sk_sp<Bitmap> Bitmap::allocateHeapBitmap(size_t size, const SkImageInfo& info, size_t rowBytes) {
// 使用 calloc 分配内存
void* addr = calloc(size, 1);
if (!addr) {
return nullptr;
}
return sk_sp<Bitmap>(new Bitmap(addr, size, info, rowBytes));
}

3.2 calloc vs malloc:为什么选择 calloc?

在 Bitmap 创建中,Android 选择 calloc 而非 malloc 有以下考虑:

特性 malloc calloc
初始化 分配的内存内容是随机的(脏内存)。 分配后自动将内存清零。
性能 速度稍快,因为它只标记内存可用,不处理内容。 速度稍慢,因为需要遍历并清零内存页。
安全性 存在隐私泄露风险,可能读取到其他进程留下的残留数据。 更安全,确保初始状态为“干净”的透明或黑色。
Bitmap 意义 必须手动调用 memset 或填充像素,否则位图会有花屏。 天然满足需求,确保新建位图内容确定。

4. 深度疑难解答:为什么 calloc 慢,而后续操作快?

这是一个非常经典的问题:既然 calloc 内部也是在“清空”内存,那为什么它比我们手动调用 Canvas.drawColor 或者 eraseColor 慢得多?

4.1 “First-Touch”(首次触碰)惩罚

calloc 分配的内存通常是新鲜的虚拟内存。当你执行 calloc 时,内核并不会立即给你真正的物理内存。

  1. 内核零页分配:内核需要找到物理页。为了安全,内核必须亲自将这些物理页清零(防止你看到上一个 App 的隐私)。
  2. **缺页中断 (Page Faults)**:当 calloc 开始写入零值时,每跨越 4KB(一个内存页),硬件就会触发一次缺页中断,陷入内核,由内核分配物理页并建立映射。
  3. CPU 计算:这一切都是在 CPU 上串行完成的。

4.2 后续 Clear/Draw 为什么快?

当你已经创建好 Bitmap,调用 bitmap.eraseColor(Color.TRANSPARENT) 或在上面绘图时:

  1. 内存已驻留:物理内存页已经分配好了,映射关系已经建立,不再有缺页中断
  2. GPU 加速:如果开启了硬件加速,eraseColor 最终可能转换成一条 GPU 指令(如 OpenGL 的 glClear 或 Vulkan 的 Clear 指令)。GPU 拥有数百个核心和极高的内存带宽,清空几个 MB 的数据只需要几微秒。
  3. 缓存友好:即使是 CPU 绘图,此时内存已经由于之前的操作被“预热”在 CPU 缓存中,写入速度远高于触发内核行为。

4.3 总结:Bitmap 究竟耗时在哪里?

Bitmap 创建的性能瓶颈不在于数据的量,而在于 CPU 与内核的“交互成本”

  1. 内核页表锁定:内核分配内存时需要持有复杂的锁。
  2. 安全性强制清零:内核强制在 CPU 上同步清零新分配的物理页,这是无法通过硬件加速绕过的“安全成本”。
  3. 上下文切换:从用户态到内核态的频繁切换(由缺页中断引起)。

5. 结论

Android Bitmap 的创建流程是一个从上层策略到底层执行的转换过程。

  • 耗时根源:主要不在于 C++ 对象的创建,而在于大内存块的物理分配(缺页中断)与内核级清零操作
  • 优化建议:在高性能要求的场景下(如列表滚动),应尽量**重用 Bitmap (BitmapPool)**。重用已有 Bitmap 相当于重用了已经建立好映射关系的物理内存,避开了“首次触碰”带来的缺页中断和内核同步清零,从而获得极致的性能。
, ,