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 持续循环 高吞吐、编码对齐、无丢帧
关键结论: Preview 追求低延迟,Buffer 数量不宜过多(3~5 个即可),否则会引入不必要的内存拷贝延迟;Capture 追求单帧画质,Buffer 数量最少(1~2 个),但必须保证足够的内存对齐以支持 JPEG 编码;Video 追求高吞吐,Buffer 数量最多(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 拷贝。
实战建议: 在 UVC 设备上,Preview 流应直接映射 UVC 的 isochronous 端点 Buffer 到 Gralloc Buffer,跳过中间的内存拷贝。这通常需要在 HAL 层实现一个 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,避免每次拍照都重新分配。
避坑指南: 不要在 Capture 流中使用 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 流程包含以下步骤,每一步都可能成为瓶颈:

  1. Framework 调用 process_capture_request() 或 set_streams()。
  2. HAL 解析流配置,分配 Buffer 池。(如果尚未分配)
  3. HAL 调用 UVC 驱动 VIDIOC_S_FMT 设置格式。
  4. HAL 调用 VIDIOC_REQBUFS 请求 Buffer。
  5. HAL 调用 VIDIOC_QBUF 将空 Buffer 入队。
  6. HAL 调用 VIDIOC_STREAMON 启动 UVC 传输。
  7. UVC 驱动启动 isochronous 传输,开始填充 Buffer。
  8. HAL 通过 VIDIOC_DQBUF 获取填充好的 Buffer,并回调 Framework。
优化策略: 将步骤 2~5 提前到 configure_streams() 阶段完成(预分配 + 预入队),这样在 Stream On 时只需执行步骤 6~8,将延迟从 50ms 降低到 5ms 以内。

下图展示了优化后的 Stream On 流程:

优化后的 Stream On 流程 优化后的 Stream On 流程(预分配 + 预入队) configure_streams() 预分配 Buffer + 预入队 Stream On 请求 VIDIOC_STREAMON UVC 传输启动 Isochronous 延迟对比 优化前:50ms(包含 Buffer 分配 + 格式协商 + 入队 + Stream On) 优化后:5ms(仅 Stream On 命令 + UVC 硬件响应)

4.2.2 Stream Off 流程:优雅停止与资源回收

Stream Off 流程同样需要优化,以避免残留 Buffer 导致的内存泄漏或下一次 Stream On 失败。标准流程如下:

  1. Framework 调用 flush() 或 close()。
  2. HAL 调用 VIDIOC_STREAMOFF 停止 UVC 传输。
  3. HAL 调用 VIDIOC_DQBUF 回收所有未处理的 Buffer。(直到返回 EAGAIN)
  4. HAL 调用 VIDIOC_REQBUFS(0) 释放所有 Buffer。
  5. HAL 清理内部状态,通知 Framework 流已关闭。
常见问题: 如果步骤 3 没有正确执行(即没有回收所有 Buffer),UVC 驱动会认为 Buffer 仍被用户空间持有,导致下一次 VIDIOC_REQBUFS 失败(返回 -EBUSY)。务必在 Stream Off 时循环调用 DQBUF 直到 EAGAIN。

4.2.3 流切换优化:Preview ↔ Video 的无缝切换

在录制视频时切换到拍照,或从拍照返回预览,通常需要重新配置流。优化目标是 避免重新分配 Buffer,实现无缝切换。

  • 共享 Buffer 池: 如果 Preview 和 Video 使用相同的分辨率和格式,让它们共享同一个 Buffer 池。切换时只需修改 Stream 类型,无需重新分配。
  • 延迟释放: 在切换流时,不要立即释放旧流的 Buffer,而是将其标记为“待释放”,等待新流稳定后再回收。
  • UVC 端点复用: 如果 UVC 设备支持多个端点,可以为 Preview 和 Video 分配不同的端点,避免切换时的带宽重新协商。

下图展示了共享 Buffer 池的流切换模型:

共享 Buffer 池的流切换模型 共享 Buffer 池的流切换模型 共享 Buffer 池 8 个 NV12 Buffer 1920x1080 DMA-BUF 导出 Preview 流 3 个 Buffer 循环 Video 流 5 个 Buffer 循环 切换优化 1. 切换时 Buffer 池不变 2. 仅修改 Stream 类型 3. 无需重新分配内存 4. 切换延迟 < 1ms

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 池,避免重新分配,实现无缝切换。