2、Android Camera 架构基础:Camera HAL3、Camera Device、Camera Provider、Camera Service 的层次结构与数据流
Android Camera 系统自 Android 8.0(O)起完成了从传统 HAL1 到 Treble 架构下的 HAL3 全面迁移,其核心设计目标是将 相机硬件抽象层(HAL)、框架服务(Camera Service) 与 应用进程(App) 解耦,同时为上层提供统一的、支持多摄像头并发与高级后处理的编程接口。理解这一层次结构,是后续深入 JPEG 编码器集成、硬件编码器调优以及 HAL 层软件编码回退机制的前提。本章将系统拆解 Camera HAL3、Camera Device、Camera Provider 与 Camera Service 四者的职责边界、通信协议(Binder/HIDL/AIDL)以及从应用请求到帧输出的完整数据流。
上图展示了 Android Camera 在 Treble 架构下的核心分层。需要特别强调的是,Camera Provider 进程 是 Android 8.0 之后引入的关键中间层,它将 HAL 实现从 cameraserver 进程中剥离,使得 Vendor 实现的 HAL 可以在独立进程中运行,从而支持系统框架与 Vendor 实现之间的版本独立更新。JPEG 编码器无论是硬件 ISP 内置还是独立编码器 IP,均位于 HAL 层之下,由 HAL 统一管理。
2.1 核心原理:四层职责边界与接口契约
Android Camera 架构自上而下可划分为四个逻辑层次,每一层都有严格的接口定义与职责边界。理解这些边界是进行编码器集成与调试的基础。
| 层次 | 进程/组件 | 核心职责 | 关键接口 | 与 JPEG 编码的关系 |
|---|---|---|---|---|
| 应用层 | App (Java/Kotlin) | 通过 CameraManager 获取相机列表,创建 CameraDevice,提交 CaptureRequest,接收 CaptureResult 与 Image | CameraManager, CameraDevice, CaptureRequest, ImageReader | 仅消费 JPEG 或 YUV 格式的 Image,不感知编码细节 |
| 框架层 | CameraService (native) | 管理 CameraDeviceClient 会话,协调多个客户端访问,分发请求,回调结果与错误 | ICameraService, ICameraDeviceUser, ICameraDeviceCallback | 透传 JPEG 格式的 buffer,不参与编码逻辑 |
| Provider 层 | Camera Provider 进程 | 枚举 HAL 设备,创建/销毁 CameraDevice,转发框架请求至 HAL,转发 HAL 回调至框架 | ICameraProvider, ICameraDevice, ICameraDeviceCallback (HIDL/AIDL) | 作为桥梁,传递 JPEG 编码相关的 stream 配置与 buffer |
| HAL 层 | Vendor HAL (camera3) | 实现 camera3_device_ops,管理 ISP pipeline,处理 capture request,填充输出 buffer(含 JPEG 编码) | camera3_device_t, camera3_stream_t, camera3_capture_request_t | JPEG 编码器集成点:HAL 负责调用硬件编码器或软件编码器生成 JPEG |
在 HAL3 架构下,camera3_device_t 是 HAL 暴露给上层的主要设备对象。它包含一组操作函数指针(ops),如 initialize()、configure_streams()、process_capture_request() 等。其中 configure_streams() 是编码器集成的关键入口,上层在此告知 HAL 需要配置的 stream 集合(如预览 YUV、拍照 JPEG、RAW),HAL 需要根据这些 stream 的格式与大小决定是否启用硬件 JPEG 编码器,以及如何分配内部 buffer。
process_capture_request() 是同步调用,但 HAL 内部可以异步处理。对于 JPEG 编码请求,HAL 通常会在收到 request 后,将 YUV 数据送入硬件编码器,并在编码完成后通过 process_capture_result() 回调返回填充了 JPEG 数据的 buffer。这个异步过程是保证相机连拍性能的关键。
2.2 判断方法:如何定位编码器集成层次
在实际工程中,当需要集成新的 JPEG 编码器或调试编码问题时,首先需要判断问题发生在哪一层。以下是一个系统化的定位流程:
- 检查 HAL 版本与接口:确认设备使用的是 HAL3 还是 HAL1。通过
adb shell dumpsys media.camera查看当前相机 HAL 版本。HAL3 支持多 stream 并发与 per-frame 控制,是编码器集成的基础。 - 确认 Provider 进程状态:执行
adb shell ps -A | grep camera,查看camera.provider进程是否存在。如果 Provider 进程崩溃,HAL 无法被枚举,上层会报CAMERA_DISCONNECTED错误。 - 抓取 HAL 层日志:在
process_capture_request()与process_capture_result()中增加 ALOGV/ALOGD 日志,打印 request ID、stream 格式、buffer 地址与大小。通过对比请求与返回的 buffer 内容,可以判断编码器是否被正确调用。 - 验证 JPEG 编码器输出:使用
adb pull获取 JPEG 文件,检查文件头(FFD8)与 EXIF 信息。如果文件头正确但图像花屏,可能是编码器输入 YUV 数据格式不匹配(如 NV12 vs NV21);如果文件头错误,则编码器可能未正确初始化。 - 使用 systrace 分析时序:在编码器集成后,使用 systrace 抓取
processCaptureRequest到onCaptureCompleted的耗时。正常硬件编码应在 30ms 内完成 1080p JPEG 编码,若超过 100ms 则可能存在 buffer 等待或编码器时钟未开启的问题。
camera3_stream_t 的 format 字段相关。当上层请求 HAL_PIXEL_FORMAT_BLOB(JPEG)格式的 stream 时,HAL 需要确保该 stream 的 max_buffers 至少为 2,以支持双缓冲流水线,避免编码器等待 buffer 释放而阻塞。
2.3 案例拆解:JPEG 编码器在 HAL3 中的集成路径
假设某设备使用高通骁龙平台,需要将第三方硬件 JPEG 编码器(通过 V4L2 M2M 接口)集成到 HAL3 中。以下是完整的集成路径与关键决策点:
背景与约束:平台已有 ISP 输出 NV12 格式的 YUV 数据,但 ISP 内置 JPEG 编码器不支持 48MP 高分辨率输出(受限于时钟频率与功耗)。因此需要外接一个支持 48MP 的硬件编码器,通过 V4L2 内存到内存(M2M)设备节点 /dev/video-jpeg0 访问。
初始现象:在 HAL 中直接调用 V4L2 编码器时,发现拍照预览正常,但高分辨率拍照时出现 ENOBUFS(无可用 buffer)错误,且系统 CPU 占用率飙升至 80%。
分析过程:
- 通过
v4l2-ctl --list-formats-ext -d /dev/video-jpeg0确认编码器支持 NV12 输入与 JPEG 输出,且支持 48MP 尺寸。 - 检查 HAL 中
configure_streams()的调用,发现上层同时配置了 YUV 预览 stream 与 JPEG 拍照 stream。HAL 为 JPEG stream 分配了 4 个 buffer,但 V4L2 编码器驱动内部只维护了 2 个 capture buffer,导致请求积压。 - 进一步分析发现,HAL 在
process_capture_request()中同步等待 V4L2 编码完成,阻塞了后续请求的处理。
实际操作步骤:
// 1. 在 configure_streams 中,为 JPEG stream 设置 max_buffers = 2
if (stream->format == HAL_PIXEL_FORMAT_BLOB) {
stream->max_buffers = 2;
stream->usage |= GRALLOC_USAGE_HW_CAMERA_WRITE;
}
// 2. 在 process_capture_request 中,将编码操作放入工作线程
// 避免阻塞 HAL 主线程
pthread_create(&encoder_thread, NULL, jpeg_encode_worker, request);
// 3. 在 jpeg_encode_worker 中,使用 V4L2 多平面 API
struct v4l2_buffer buf = {0};
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
buf.memory = V4L2_MEMORY_DMABUF;
buf.m.planes[0].m.fd = input_dmabuf_fd; // YUV buffer
buf.m.planes[0].length = yuv_size;
ioctl(fd, VIDIOC_QBUF, &buf);
// 4. 等待编码完成,获取输出 buffer
select(fd + 1, &fds, NULL, NULL, &timeout);
ioctl(fd, VIDIOC_DQBUF, &buf);
// 将 buf.m.planes[0].m.fd 填充到 camera3_stream_buffer 中返回
结果对比:修改后,48MP 拍照耗时从 1.2s 降至 350ms,CPU 占用率降至 15%。连拍速度从 0.5fps 提升至 2fps。
风险与代价:引入工作线程后,需要处理线程同步与生命周期管理。在 HAL 卸载时,必须确保编码线程退出并释放所有 V4L2 buffer,否则会导致内核驱动崩溃。
dma_buf_sync 或 ion_sync 操作保证数据可见性。
2.4 常见误区与总结复盘
在 Camera HAL3 架构下进行编码器集成时,工程师常陷入以下误区:
- 误区一:将 JPEG 编码器视为独立模块。实际上,编码器必须与 HAL 的 stream 配置、buffer 管理、request 生命周期紧密耦合。编码器的输入 buffer 必须来自 ISP 的输出,且需要与 HAL 的 gralloc buffer 共享。
- 误区二:忽略 Provider 进程的隔离性。在 Treble 架构下,HAL 运行在独立的 Provider 进程中,不能直接访问 cameraserver 的内存。所有 buffer 传递必须通过 HIDL/AIDL 接口的句柄传递,且需要处理跨进程的 buffer 同步。
- 误区三:同步调用编码器。在
process_capture_request()中同步等待编码完成会严重阻塞 HAL 的请求处理流水线,导致预览卡顿。正确做法是使用异步工作队列,并利用camera3_callback_ops中的notify()上报请求状态。 - 误区四:忽视 JPEG 编码器的功耗与温升。硬件编码器在高分辨率连续拍照时会产生显著热量,HAL 需要监控编码器温度,并在过热时降级到软件编码或降低帧率。