4. HAL 层数据流模型:三种流模式的 Buffer 分配与 Stream 优化
Camera HAL 层的数据流模型是连接 Framework 与底层硬件的核心枢纽。针对 Preview(预览)、Capture(拍照)、Video(录像)三种典型流模式,Buffer 的分配策略与 Stream On/Off 流程直接决定了系统的内存占用、启动延迟和帧率稳定性。本章将深入剖析这三种流模式下的 Buffer 管理机制,并给出经过验证的优化方案。
4.1 三种流模式的 Buffer 分配策略
不同流模式对 Buffer 的数量、大小、格式和生命周期有着截然不同的需求。错误的分配策略会导致内存浪费、帧率抖动或拍照黑屏。下表总结了三种流模式的核心差异:
| 流模式 | 典型分辨率 | Buffer 格式 | Buffer 数量 | 生命周期 | 关键约束 |
|---|---|---|---|---|---|
| Preview | 1920x1080 / 1280x720 | NV12 / YUYV | 3 ~ 5 | 持续循环 | 低延迟、低功耗、连续帧 |
| Capture | 全分辨率 (e.g., 4000x3000) | JPEG / RAW10 | 1 ~ 2 | 单次触发 | 高画质、大内存、一次性 |
| Video | 1920x1080 / 3840x2160 | NV12 / H.264 | 6 ~ 10 | 持续循环 | 高吞吐、编码对齐、无丢帧 |
4.1.1 Preview 流:环形 Buffer 池与零拷贝策略
Preview 流是用户最常感知的流模式,其核心目标是 低延迟 和 帧率稳定。推荐使用 环形 Buffer 池 配合 Gralloc 硬件缓冲区 实现零拷贝。
- Buffer 数量: 固定为 4 个(3 个用于 Dequeue,1 个用于 Display 合成)。
- 分配时机: 在
configure_streams()阶段一次性分配,避免运行时动态分配。 - 生命周期: 从 Stream On 到 Stream Off,Buffer 始终在 HAL 与 Display 之间循环。
- 优化点: 使用
V4L2_MEMORY_DMABUF导入 DMA-BUF,避免 CPU 拷贝。
UvcBufferMapper 模块。
4.1.2 Capture 流:大块连续内存与 JPEG 对齐
Capture 流需要处理全分辨率图像,Buffer 大小通常是 Preview 的 4~6 倍。分配策略必须考虑 JPEG 编码器的对齐要求(通常为 16 字节或 64 字节对齐)。
- Buffer 数量: 2 个(一个用于 Sensor 填充,一个用于 JPEG 编码)。
- 分配时机: 在
process_capture_request()触发时按需分配,拍照完成后立即释放。 - 内存类型: 优先使用 物理连续内存(如 ION/CAMA 的
SECURE或CONTIG类型),避免 Scatter-Gather 带来的 DMA 开销。 - 优化点: 预分配一个 JPEG 编码工作 Buffer,避免每次拍照都重新分配。
V4L2_MEMORY_MMAP 模式,因为 MMAP 的 Buffer 通常不是物理连续的,会导致 JPEG 硬件编码器 DMA 失败。务必使用 V4L2_MEMORY_DMABUF 配合 ION 分配器。
4.1.3 Video 流:深度 Buffer 队列与编码器背压
Video 流需要处理持续的编码任务,Buffer 队列深度直接影响编码器的稳定性。如果 Buffer 数量不足,编码器会因等待输入而丢帧。
- Buffer 数量: 8 个(4 个用于 HAL 填充,4 个用于编码器消费)。
- 分配时机: 在
configure_streams()阶段一次性分配,与 Preview 流共享 Buffer 池(如果分辨率相同)。 - 生命周期: 从 Stream On 到 Stream Off,Buffer 在 HAL 与 Video Codec 之间循环。
- 优化点: 实现 背压感知 机制:当编码器队列满时,HAL 应主动跳过帧(drop frame)而不是阻塞 UVC 传输。
4.2 Stream On/Off 流程优化
Stream On/Off 是 HAL 层最耗时的操作之一,尤其是在 UVC 设备上,涉及 USB 带宽协商、端点配置和中断处理。优化这两个流程可以显著缩短相机启动时间和切换延迟。
4.2.1 Stream On 流程:从 Framework 到 UVC 端点的完整链路
一个典型的 Stream On 流程包含以下步骤,每一步都可能成为瓶颈:
- Framework 调用
process_capture_request()或set_streams()。 - HAL 解析流配置,分配 Buffer 池。(如果尚未分配)
- HAL 调用 UVC 驱动
VIDIOC_S_FMT设置格式。 - HAL 调用
VIDIOC_REQBUFS请求 Buffer。 - HAL 调用
VIDIOC_QBUF将空 Buffer 入队。 - HAL 调用
VIDIOC_STREAMON启动 UVC 传输。 - UVC 驱动启动 isochronous 传输,开始填充 Buffer。
- HAL 通过
VIDIOC_DQBUF获取填充好的 Buffer,并回调 Framework。
configure_streams() 阶段完成(预分配 + 预入队),这样在 Stream On 时只需执行步骤 6~8,将延迟从 50ms 降低到 5ms 以内。
下图展示了优化后的 Stream On 流程:
4.2.2 Stream Off 流程:优雅停止与资源回收
Stream Off 流程同样需要优化,以避免残留 Buffer 导致的内存泄漏或下一次 Stream On 失败。标准流程如下:
- Framework 调用
flush()或close()。 - HAL 调用
VIDIOC_STREAMOFF停止 UVC 传输。 - HAL 调用
VIDIOC_DQBUF回收所有未处理的 Buffer。(直到返回 EAGAIN) - HAL 调用
VIDIOC_REQBUFS(0)释放所有 Buffer。 - HAL 清理内部状态,通知 Framework 流已关闭。
VIDIOC_REQBUFS 失败(返回 -EBUSY)。务必在 Stream Off 时循环调用 DQBUF 直到 EAGAIN。
4.2.3 流切换优化:Preview ↔ Video 的无缝切换
在录制视频时切换到拍照,或从拍照返回预览,通常需要重新配置流。优化目标是 避免重新分配 Buffer,实现无缝切换。
- 共享 Buffer 池: 如果 Preview 和 Video 使用相同的分辨率和格式,让它们共享同一个 Buffer 池。切换时只需修改 Stream 类型,无需重新分配。
- 延迟释放: 在切换流时,不要立即释放旧流的 Buffer,而是将其标记为“待释放”,等待新流稳定后再回收。
- UVC 端点复用: 如果 UVC 设备支持多个端点,可以为 Preview 和 Video 分配不同的端点,避免切换时的带宽重新协商。
下图展示了共享 Buffer 池的流切换模型:
4.3 实战:UVC 设备上的 Buffer 分配代码示例
以下代码展示了在 HAL 层为 Preview 流分配 Buffer 的核心逻辑,使用了 V4L2_MEMORY_DMABUF 模式:
// HAL 层 Preview 流 Buffer 分配示例
int UvcHal::allocatePreviewBuffers(uint32_t width, uint32_t height) {
struct v4l2_requestbuffers req = {0};
req.count = PREVIEW_BUFFER_COUNT; // 4 个 Buffer
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_DMABUF;
if (ioctl(mFd, VIDIOC_REQBUFS, &req) < 0) {
ALOGE("REQBUFS failed: %s", strerror(errno));
return -1;
}
// 分配 Gralloc Buffer 并导出 DMA-BUF
for (uint32_t i = 0; i < req.count; i++) {
buffer_handle_t grallocBuffer;
int ret = mGrallocAlloc->allocate(
width, height, HAL_PIXEL_FORMAT_YCbCr_420_888,
GRALLOC_USAGE_HW_CAMERA_WRITE | GRALLOC_USAGE_HW_TEXTURE,
&grallocBuffer);
if (ret != 0) {
ALOGE("Gralloc allocate failed");
return -1;
}
// 获取 DMA-BUF fd
int dmaBufFd = mGrallocMapper->exportDmaBuf(grallocBuffer);
if (dmaBufFd < 0) {
ALOGE("Export DMA-BUF failed");
return -1;
}
// 将 DMA-BUF 入队到 UVC 驱动
struct v4l2_buffer buf = {0};
struct v4l2_plane planes[1] = {0};
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_DMABUF;
buf.index = i;
buf.length = 1;
buf.m.planes = planes;
buf.m.planes[0].m.fd = dmaBufFd;
buf.m.planes[0].length = width * height * 3 / 2; // NV12 大小
if (ioctl(mFd, VIDIOC_QBUF, &buf) < 0) {
ALOGE("QBUF failed: %s", strerror(errno));
close(dmaBufFd);
return -1;
}
mPreviewBuffers[i] = grallocBuffer;
}
return 0;
}
configure_streams() 阶段完成了 Buffer 分配和入队。当 Stream On 时,HAL 只需调用 VIDIOC_STREAMON,UVC 驱动会立即开始填充已经入队的 Buffer,实现了接近零延迟的启动。
4.4 本章小结
- Preview 流: 使用 3~5 个 Buffer 的环形池,零拷贝策略,预分配 + 预入队。
- Capture 流: 使用 1~2 个物理连续 Buffer,按需分配,注意 JPEG 对齐。
- Video 流: 使用 6~10 个 Buffer 的深度队列,实现背压感知,避免丢帧。
- Stream On 优化: 将 Buffer 分配和入队提前到
configure_streams(),延迟从 50ms 降至 5ms。 - Stream Off 优化: 循环调用
DQBUF直到 EAGAIN,确保所有 Buffer 被回收。 - 流切换优化: 共享 Buffer 池,避免重新分配,实现无缝切换。