本文将分析从 Java 层 Bitmap.createBitmap 调用开始,经过 JNI 层 Bitmap_creator,最终到达 Native 层 allocateHeapBitmap 的完整创建流程,并深入探讨内存分配函数选择及性能耗时点。
1. Java 层:Bitmap.createBitmap
在 Java 层,用户通常通过 Bitmap.createBitmap(...) 静态方法创建位图。
核心代码路径: frameworks/base/graphics/java/android/graphics/Bitmap.java
以最常见的创建方法为例:
1 | public static Bitmap createBitmap(int width, int height, Config config, boolean hasAlpha) { |
经过一系列参数检查后,它会调用 native 方法:
1 | Bitmap bm = nativeCreate(null, 0, width, width, height, config.nativeInt, true, |
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 调用的入口点。其核心步骤如下:
- 参数映射:将 Java 的
Config映射为 Skia 的SkColorType。1
SkColorType colorType = GraphicsJNI::legacyBitmapConfigToColorType(configHandle);
- 色彩空间处理:获取 Native 层的色彩空间对象。
- 构建 SkBitmap 元数据:创建一个
SkBitmap对象并设置其SkImageInfo(包含宽高、颜色类型、Alpha 类型和色彩空间)。 - 调用 Native 分配器:
1
sk_sp<Bitmap> nativeBitmap = Bitmap::allocateHeapBitmap(&bitmap);
- 包装与返回:分配成功后,通过
createBitmap(JNI helper) 将 Native 层的Bitmap指针包装成 Java 层的Bitmap对象返回。
3. Native 层:allocateHeapBitmap 与内存分配
3.1 内存分配实现
真正的堆内存分配发生在 Bitmap::allocateHeapBitmap(size_t size, ...) 中:
1 | sk_sp<Bitmap> Bitmap::allocateHeapBitmap(size_t size, const SkImageInfo& info, size_t 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 时,内核并不会立即给你真正的物理内存。
- 内核零页分配:内核需要找到物理页。为了安全,内核必须亲自将这些物理页清零(防止你看到上一个 App 的隐私)。
- **缺页中断 (Page Faults)**:当
calloc开始写入零值时,每跨越 4KB(一个内存页),硬件就会触发一次缺页中断,陷入内核,由内核分配物理页并建立映射。 - CPU 计算:这一切都是在 CPU 上串行完成的。
4.2 后续 Clear/Draw 为什么快?
当你已经创建好 Bitmap,调用 bitmap.eraseColor(Color.TRANSPARENT) 或在上面绘图时:
- 内存已驻留:物理内存页已经分配好了,映射关系已经建立,不再有缺页中断。
- GPU 加速:如果开启了硬件加速,
eraseColor最终可能转换成一条 GPU 指令(如 OpenGL 的glClear或 Vulkan 的 Clear 指令)。GPU 拥有数百个核心和极高的内存带宽,清空几个 MB 的数据只需要几微秒。 - 缓存友好:即使是 CPU 绘图,此时内存已经由于之前的操作被“预热”在 CPU 缓存中,写入速度远高于触发内核行为。
4.3 总结:Bitmap 究竟耗时在哪里?
Bitmap 创建的性能瓶颈不在于数据的量,而在于 CPU 与内核的“交互成本”:
- 内核页表锁定:内核分配内存时需要持有复杂的锁。
- 安全性强制清零:内核强制在 CPU 上同步清零新分配的物理页,这是无法通过硬件加速绕过的“安全成本”。
- 上下文切换:从用户态到内核态的频繁切换(由缺页中断引起)。
5. 结论
Android Bitmap 的创建流程是一个从上层策略到底层执行的转换过程。
- 耗时根源:主要不在于 C++ 对象的创建,而在于大内存块的物理分配(缺页中断)与内核级清零操作。
- 优化建议:在高性能要求的场景下(如列表滚动),应尽量**重用 Bitmap (BitmapPool)**。重用已有 Bitmap 相当于重用了已经建立好映射关系的物理内存,避开了“首次触碰”带来的缺页中断和内核同步清零,从而获得极致的性能。