一、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_OFTENGRALLOC_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 管理的核心流转路径:

Camera Buffer 管理核心流转路径 Camera HAL(生产者) dequeueBuffer() BufferQueue [空闲池] [已填充] [等待] queueBuffer() SurfaceFlinger(消费者) acquireBuffer() releaseBuffer() Gralloc HAL alloc() / free() 物理内存 ION / DMA-BUF 内存复用策略 • Buffer 池化 • Stream 间复用 • ZSL 环形 Buffer • 延迟分配 • 共享内存 图: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