第二章:Pipeline深度解析
Camera HAL 里最核心的东西是什么?我个人觉得,就是 Pipeline。
说白了,整个相机系统就是一条流水线。从你按下快门,到最终拿到照片,中间经过的每一个环节,都是 Pipeline 的一部分。今天咱们就把它拆开看看,到底是怎么流转的。
2.1 Camera Pipeline 核心流程
先画个简单的图在脑子里:
App → Framework → HAL → Sensor → ISP → Buffer → 回显
嗯,就是这么一条线。但实际跑起来,远比这个复杂。
我习惯把 Pipeline 分成三段来看:
- 上层控制流:App 发 Request,Framework 做调度
- 中间数据流:HAL 驱动 Sensor 曝光,ISP 处理图像
- 底层 Buffer 流:图像数据在内存里流转,谁用谁拿
这三段缺一不可。你想想看,如果控制流慢了,App 点拍照要等半天;数据流慢了,预览卡成 PPT;Buffer 流出问题,直接闪退或者黑屏。
核心要点:Pipeline 的瓶颈往往不在某一个环节,而在环节之间的衔接处。我在项目中遇到过,Sensor 出图很快,但 ISP 处理跟不上,结果 Buffer 堆积,最后整个 Pipeline 被拖死。
2.2 Request 与 Result 的流转机制
这是 HAL 设计的精髓。Google 搞这套机制,说白了就是为了让上层和底层解耦。
Request 是什么?
App 说:「我要拍一张照片,参数是 ISO 400,曝光时间 1/100,输出 1080p。」
这个「要求」就被封装成一个 CameraCaptureRequest,扔给 HAL。
Result 是什么?
HAL 处理完后,回给 Framework 一个 CaptureResult,里面包含:
- 实际使用的参数(可能和 Request 不完全一样)
- 图像数据(通过 Buffer 传递)
- 一些统计信息(比如 AE 状态、AF 状态)
这里有个关键点:Request 和 Result 是一一对应的。你发了 3 个 Request,就一定会收到 3 个 Result,顺序可能乱,但数量不会少。
我的经验:调试的时候,经常遇到 Result 回不来。我一般先检查 Request 有没有正确下发。有一次折腾了两天,最后发现是 Framework 那边把 Request 的 stream 配错了,导致 HAL 根本没法处理。
来看一段伪代码,理解一下流转过程:
// Framework 下发 Request
void processCaptureRequest(CameraCaptureRequest request) {
// 1. 解析 Request 中的参数
int iso = request.get(CaptureRequest.SENSOR_SENSITIVITY);
int expo = request.get(CaptureRequest.SENSOR_EXPOSURE_TIME);
// 2. 配置 Sensor
sensor.setIso(iso);
sensor.setExposure(expo);
// 3. 触发曝光
sensor.startCapture();
// 4. 等待曝光完成,拿到原始数据
RawImage raw = sensor.waitForCaptureDone();
// 5. 经过 ISP 处理
ProcessedImage img = isp.process(raw);
// 6. 填充 Result
CameraCaptureResult result = new CameraCaptureResult();
result.set(CaptureResult.SENSOR_SENSITIVITY, iso);
result.set(CaptureResult.SENSOR_EXPOSURE_TIME, expo);
result.setImage(img);
// 7. 回调给 Framework
notifyCaptureResult(result);
}
你看,流程很清晰。但实际代码里,第 4 步和第 5 步之间,往往藏着性能陷阱。
2.3 Stream 与 Buffer 管理
Stream 是什么?你可以把它理解成「一条数据通道」。App 说我要预览,那就开一条预览 Stream;我要拍照,再开一条拍照 Stream;我要录像,可能还要开一条编码 Stream。
每条 Stream 都有自己的 Buffer 池。Buffer 就是存放图像数据的内存块。
这里有个常见的坑:Buffer 分配和回收。
我曾经遇到过一个 case:预览帧率突然掉到 15fps,查了半天,发现是 Buffer 分配太慢。每次都要从 ION 分配新内存,而 ION 分配本身就有延迟。
避坑指南:千万不要在关键路径上动态分配 Buffer。我建议的做法是:
- 初始化时预分配好 Buffer 池
- 运行时只做「借」和「还」的操作
- Buffer 用完立刻归还,不要持有太久
来看一个 Buffer 管理的典型实现:
class StreamBufferPool {
std::queue<buffer_handle_t> freeBuffers;
std::mutex lock;
public:
// 预分配 N 个 Buffer
void init(int count) {
for (int i = 0; i < count; i++) {
buffer_handle_t buf = allocFromION();
freeBuffers.push(buf);
}
}
// 借一个 Buffer
buffer_handle_t acquire() {
std::lock_guard<std::mutex> guard(lock);
if (freeBuffers.empty()) {
// 这里要小心!Buffer 耗尽说明 Pipeline 堵了
return nullptr;
}
buffer_handle_t buf = freeBuffers.front();
freeBuffers.pop();
return buf;
}
// 还一个 Buffer
void release(buffer_handle_t buf) {
std::lock_guard<std::mutex> guard(lock);
freeBuffers.push(buf);
}
};
嗯,这里要注意:acquire 返回 nullptr 的时候,千万别硬撑。我见过有人直接在这里阻塞等待,结果整个 Pipeline 卡死。
2.4 多 Stream 的同步问题
实际项目中,往往不止一条 Stream。预览、拍照、录像同时跑,这时候就要处理同步。
举个例子:
- 预览 Stream:要求低延迟,30fps,1080p
- 拍照 Stream:要求高画质,但可以慢一点
- 录像 Stream:要求稳定帧率,编码友好
这三条 Stream 共享同一个 Sensor 输出。Sensor 一次曝光只能出一帧原始数据,但三条 Stream 都需要自己的处理结果。
我的做法是:Sensor 只出一帧,然后复制给多条 Stream。但复制本身也有开销,尤其是大分辨率的时候。
性能优化点:如果预览和录像分辨率一样,可以直接共享 Buffer,不用复制。我上一个项目就是这么干的,帧率从 24fps 提到了 30fps。
2.5 延迟优化实战
最后聊点实际的。Pipeline 延迟怎么优化?我总结了几条经验:
- 减少 Buffer 拷贝:能共享就共享,能引用就引用
- 预分配资源:初始化时把能准备的都准备好
- 异步处理:不要在主线程里做耗时操作
- 合理设置 Buffer 数量:太少会等,太多会浪费内存
- 监控 Pipeline 状态:实时看 Buffer 使用率、帧率、延迟
我曾经在一个项目里,把预览延迟从 120ms 降到了 45ms。怎么做的?其实就是把 ISP 处理从同步改成了异步,然后 Buffer 池从 4 个扩到了 8 个。就这么简单,但效果立竿见影。
小技巧:调试的时候,可以在关键节点打时间戳。比如 Sensor 曝光完成打一个,ISP 处理完打一个,Buffer 交付打一个。这样一眼就能看出哪里最慢。
好了,Pipeline 的核心内容就这些。下一章咱们聊聊更具体的性能调优手段,比如如何用 Perfetto 抓 trace 分析延迟。