第二章: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 延迟怎么优化?我总结了几条经验:

  1. 减少 Buffer 拷贝:能共享就共享,能引用就引用
  2. 预分配资源:初始化时把能准备的都准备好
  3. 异步处理:不要在主线程里做耗时操作
  4. 合理设置 Buffer 数量:太少会等,太多会浪费内存
  5. 监控 Pipeline 状态:实时看 Buffer 使用率、帧率、延迟

我曾经在一个项目里,把预览延迟从 120ms 降到了 45ms。怎么做的?其实就是把 ISP 处理从同步改成了异步,然后 Buffer 池从 4 个扩到了 8 个。就这么简单,但效果立竿见影。

小技巧:调试的时候,可以在关键节点打时间戳。比如 Sensor 曝光完成打一个,ISP 处理完打一个,Buffer 交付打一个。这样一眼就能看出哪里最慢。

好了,Pipeline 的核心内容就这些。下一章咱们聊聊更具体的性能调优手段,比如如何用 Perfetto 抓 trace 分析延迟。