一、Camera Buffer 管理全景:从分配到底层流转
Camera 系统里,Buffer 管理是个绕不开的核心话题。说白了,每一帧图像数据都要在内存里找个地方住下来,然后被搬来搬去——从 Sensor 到 ISP,从 ISP 到 Preview/Surface,再从 Surface 到编码器。这一整套流程,全靠 Buffer 管理撑着。
我个人习惯把 Buffer 管理拆成四个层次来看:分配层(GraphicBuffer)、硬件抽象层(Gralloc HAL)、流转层(BufferQueue)、策略层(Dequeue/Queue + 复用)。下面我们一层一层剥开。
1.1 GraphicBuffer:Camera 的“内存单元”
GraphicBuffer 是 Android 里最基础的图像内存单元。它封装了宽、高、格式、 stride、usage 等信息,背后对应着一块真实的物理内存(或者 DMA-BUF)。
分配一个 GraphicBuffer,本质上就是告诉 Gralloc:“我要一块 1920x1080、NV12 格式、给 Camera 用的内存。” Gralloc 会去申请一块符合硬件要求的内存,然后返回一个句柄。
关键点:GraphicBuffer 的 usage 标志非常重要。Camera 场景下,我们通常要设置 GRALLOC_USAGE_SW_READ_OFTEN 或 GRALLOC_USAGE_HW_CAMERA_WRITE。这个标志决定了内存是走 CPU 还是走硬件通路。
我的经验:有一次我在调试预览卡顿,发现 GraphicBuffer 的 usage 没设对,导致每次都要从 GPU 内存拷贝到 CPU 内存,帧率直接掉了一半。后来改成 GRALLOC_USAGE_HW_TEXTURE + GRALLOC_USAGE_HW_CAMERA_WRITE,问题就解决了。
1.2 Gralloc HAL:硬件内存的“调度员”
Gralloc HAL 是 Android 图形内存管理的硬件抽象层。它负责两件事:分配内存和映射内存。
分配时,Gralloc 会根据 usage 和 format,决定用哪块硬件来分配内存。比如 Camera 写入的场景,通常会走 ION 或 DMA-BUF 分配器,保证内存是物理连续的,方便硬件 DMA 直接访问。
映射时,Gralloc 会把这块物理内存映射到进程的用户空间地址,让 CPU 也能读写。但注意——Camera 场景下,CPU 读写往往不是最优解,能走硬件就尽量走硬件。
避坑指南:我曾经在某个平台上发现,Gralloc 分配出来的内存 stride 和 width 不一致。Camera 硬件写的时候是按 stride 写的,但上层应用按 width 读,结果图像边缘出现了绿边。后来我强制要求上层用 stride 来配置硬件,才彻底解决。
1.3 BufferQueue:生产者和消费者的“桥梁”
BufferQueue 是 Android 里最经典的 Buffer 流转机制。Camera 作为生产者,把填好数据的 Buffer 丢进队列;SurfaceFlinger 或编码器作为消费者,从队列里取出 Buffer 去显示或编码。
BufferQueue 内部维护了一个 Buffer 池,默认通常是 4 个 Buffer。为什么是 4 个?因为 Camera 的 pipeline 需要:1 个正在被硬件写入、1 个正在被消费者读取、1 个在队列里等待、1 个空闲待分配。少了会丢帧,多了浪费内存。
核心流程:
- 生产者调用
dequeueBuffer()从队列里拿一个空闲 Buffer - 生产者往 Buffer 里填数据(比如 Camera 硬件写入)
- 生产者调用
queueBuffer()把 Buffer 还给队列,状态变成“已填充” - 消费者调用
acquireBuffer()拿到已填充的 Buffer - 消费者处理完数据后,调用
releaseBuffer()把 Buffer 放回空闲池
1.4 Dequeue / Queue 流程:一次完整的 Buffer 旅行
我们来看一次典型的 Camera 预览帧的 Buffer 流转:
// 伪代码:Camera HAL 侧
// 1. 从 BufferQueue 拿一个空闲 Buffer
buffer = bufferQueue->dequeueBuffer();
// 2. 把 Buffer 的 fd 传给 Camera 硬件
cameraHw->setOutputBuffer(buffer->fd);
// 3. 硬件开始写入(比如 Sensor 数据经过 ISP 后写入这块内存)
cameraHw->startCapture();
// 4. 等待硬件完成(中断或 poll)
waitForCaptureDone();
// 5. 把 Buffer 还给 BufferQueue,通知消费者
bufferQueue->queueBuffer(buffer);
消费者侧(SurfaceFlinger)的流程:
// 伪代码:SurfaceFlinger 侧
// 1. 监听 BufferQueue 的 onFrameAvailable 回调
// 2. 拿到已填充的 Buffer
buffer = bufferQueue->acquireBuffer();
// 3. 把 Buffer 交给 HWC 合成显示
hwc->setLayerBuffer(buffer);
// 4. 显示完成后,释放 Buffer
bufferQueue->releaseBuffer(buffer);
嗯,这里要注意:dequeue 和 queue 是成对出现的。如果生产者 dequeue 了 Buffer 但没 queue 回去,消费者那边就会一直等,最终导致 ANR 或掉帧。
1.5 内存复用策略:别让 Buffer 频繁分配
Camera 场景下,内存复用是性能的关键。你想想看,如果每帧都重新分配 GraphicBuffer,那内存带宽和分配开销会直接把帧率拖垮。
Android 的 BufferQueue 本身就支持内存复用:Buffer 一旦分配,就会在队列里循环使用,不会频繁释放和重新分配。只有当分辨率、格式或 usage 发生变化时,才会触发 Buffer 的重新分配。
另外,Camera HAL 层也有自己的内存复用策略。比如:
- Buffer 池化:HAL 内部维护一个 Buffer 池,提前分配好 N 个 Buffer,循环使用
- Stream 间复用:如果 Preview 和 Video 的分辨率相同,可以让它们共享同一块 Buffer,减少内存拷贝
- ZSL(零快门延迟)场景:环形 Buffer 池,不断覆盖旧帧,保证始终有最新的帧可用
我的建议:在实现 Camera HAL 时,尽量把 Buffer 池的大小做成可配置的。低端设备可能只需要 3 个 Buffer,高端设备可以给 6 个。Buffer 太少容易丢帧,太多又浪费内存。我曾经在一个项目里把 Buffer 数从 4 改成 6,帧率稳定性提升了 15%。
1.6 核心知识体系图
下面这张图总结了 Camera Buffer 管理的核心流转路径:
1.7 总结一下
Camera Buffer 管理,说白了就是三件事:谁分配、谁流转、谁复用。
- 分配:GraphicBuffer + Gralloc HAL,决定内存从哪里来、长什么样
- 流转:BufferQueue 的 Dequeue/Queue 机制,保证生产者和消费者之间高效协作
- 复用:池化、共享、环形 Buffer,避免频繁分配带来的性能损耗
我个人觉得,理解 Buffer 管理的关键不在于记住每个 API 的签名,而在于理解数据流的方向和生命周期。你只要搞清楚“谁在写、谁在读、写完了怎么通知、读完了怎么归还”,整个框架就通了。
最后提醒一句:千万不要在 Camera 的 callback 里做 GraphicBuffer 的分配或释放操作。我曾经见过一个项目,在 onFrameAvailable 回调里直接 new GraphicBuffer,结果内存碎片化严重,跑 10 分钟就 OOM 了。Buffer 一定要提前分配好,循环使用。
公众号:蓝海资料掘金营,微信deep3321