4. Camera HAL 初始化流程:HAL Module 加载、open() 调用链、initialize() 与 configure_streams()

好,咱们今天来聊聊 Camera HAL 的初始化流程。说实话,这部分是很多刚接触 HAL 层的同学最容易懵圈的地方。我自己当年第一次看这块代码时,也是对着调用链发了好一会儿呆。其实说白了,初始化就干三件事:把 HAL 模块从 so 库里拽出来、打开具体的 camera 设备、然后告诉它你要干嘛。

4.1 HAL Module 加载:从 hw_get_module() 说起

一切从 hw_get_module() 开始。这个函数是 Android HAL 层的标准入口,Camera HAL 也不例外。它会根据你传入的 HAL_MODULE_INFO_SYM 去查找对应的 .so 文件。

// 典型的加载流程
hw_get_module(CAMERA_HARDWARE_MODULE_ID, (const hw_module_t**)&camera_module);

这里有个关键点:CAMERA_HARDWARE_MODULE_ID 的值是 "camera"。系统会去 /vendor/lib/hw//system/lib/hw/ 下找 camera.<platform>.so 这个文件。嗯,这里要注意,不同厂商的命名规则可能略有差异,但核心逻辑是一样的。

核心数据结构:加载完成后,你会得到一个 hw_module_t 指针。但 Camera HAL 实际用的是它的扩展结构 camera_module_t,里面包含了 get_number_of_cameras()get_camera_info() 这些关键方法。

我个人习惯在加载后立刻检查返回值。曾经有个项目,因为 so 文件权限没配好,hw_get_module 返回了 -ENOENT,排查了半天才发现是 selinux 策略的问题。你想想看,一个文件权限问题能让你怀疑人生。

4.2 open() 调用链:从 Framework 到 HAL 的握手

模块加载完了,接下来要打开具体的 camera 设备。调用链是这样的:

  1. Framework 层调用 CameraService::connect()
  2. 往下走到 Camera2Client::initialize()
  3. 最终调用 camera_module->common.methods->open()

这个 open() 函数签名长这样:

int (*open)(const struct hw_module_t* module, const char* id,
            struct hw_device_t** device);

注意第二个参数 id,它对应的是 camera 的 ID,比如 "0" 代表后置摄像头,"1" 代表前置。我在项目中遇到过一个问题:某个设备上报了 3 个 camera,但 ID 不是连续的 0、1、2,而是 0、1、3。结果 Framework 层遍历时直接崩了。所以,ID 必须从 0 开始连续递增,这是硬性要求。

避坑指南:我曾经在 open() 里忘记初始化 pthread_mutex,导致多线程并发打开 camera 时死锁。排查了整整两天才发现是锁的问题。建议你在 open() 里就把所有互斥体初始化好,别偷懒。

open() 成功后,你会得到一个 hw_device_t 指针,实际使用时要强转为 camera3_device_t。这个结构体里包含了所有操作 camera 的函数指针,比如 initialize()configure_streams() 等。

4.3 initialize():建立 HAL 与 Framework 的通信桥梁

open() 只是拿到了设备句柄,真正的初始化在 initialize() 里完成。这个函数是 Camera HAL3 引入的,它的作用说白了就是:让 HAL 知道 Framework 长什么样。

int initialize(const struct camera3_device *device,
               const camera3_callback_ops_t *callback_ops);

第二个参数 callback_ops 是关键。它是一组回调函数指针,HAL 通过它来通知 Framework:

  • 有新的 capture result 了
  • 有错误发生了
  • buffer 已经处理完了

我个人建议在 initialize() 里把 callback_ops 保存下来,同时做一些一次性的初始化工作,比如:

  • 分配 metadata 缓冲区
  • 初始化 sensor 驱动
  • 创建工作线程池

小技巧:initialize() 可能会被多次调用吗?理论上不会,但我在某个厂商的代码里见过因为异常流程导致重复调用的情况。建议加个状态机保护,防止重复初始化。

4.4 configure_streams():告诉 HAL 你要拍什么

这是初始化流程里最复杂的一步。Framework 通过 configure_streams() 告诉 HAL:我要用哪些数据流,每个流的分辨率、格式、用途是什么。

int configure_streams(const struct camera3_device *device,
                      camera3_stream_configuration_t *stream_list);

camera3_stream_configuration_t 结构体里包含了一个 camera3_stream_t 数组。每个 stream 都有这些属性:

属性 说明 常见值
stream_type 数据流类型 CAMERA3_STREAM_OUTPUT(输出)、CAMERA3_STREAM_INPUT(输入)
width/height 分辨率 1920x1080、1280x720 等
format 像素格式 HAL_PIXEL_FORMAT_YCbCr_420_888、HAL_PIXEL_FORMAT_BLOB 等
usage 使用场景标记 GRALLOC_USAGE_HW_CAMERA_WRITE、GRALLOC_USAGE_HW_TEXTURE 等
max_buffers HAL 需要的最大 buffer 数 通常由 HAL 在 configure 时填写

这里有个容易踩的坑:usage 标记。Framework 会设置 GRALLOC_USAGE_HW_CAMERA_WRITE,但如果你要直接送显,还得加上 GRALLOC_USAGE_HW_TEXTURE。我曾经因为漏了后一个标记,导致预览画面黑屏——buffer 写进去了,但 GPU 读不出来。

configure_streams() 的返回值也很讲究:

  • 0:成功,所有 stream 都接受了
  • -EINVAL:参数无效,比如分辨率不支持
  • -ENODEV:设备异常,需要重新 open

关键点:configure_streams() 可能会被多次调用。比如从拍照模式切换到录像模式时,Framework 会重新配置 stream。HAL 需要正确处理这种情况,释放旧的 stream 资源,分配新的。

嗯,说到多次调用,我记得有个项目里,Framework 在切换模式时连续调了两次 configure_streams(),中间间隔只有几毫秒。第一次还没处理完,第二次就来了。结果我的 HAL 在释放旧 buffer 时和新 buffer 分配冲突了,直接 crash。后来我加了个 pending 队列,才把这个问题解决掉。

4.5 初始化流程总结

咱们把整个流程串起来看:

  1. hw_get_module():加载 so 库,拿到 module 指针
  2. open():打开具体 camera 设备,拿到 device 指针
  3. initialize():注册回调,建立通信通道
  4. configure_streams():配置数据流,分配 buffer

这四步走完,camera 就准备好干活了。之后就是 request 的提交和 result 的回调,那是另一个话题。

个人建议:调试初始化流程时,最好在每一步都加 log,打印关键参数和返回值。我习惯用 ALOGV 打详细日志,用 ALOGE 打错误日志。这样出问题时,看一眼 log 就能定位到是哪一步挂了。

最后说一句:初始化流程看似简单,但每个环节都有坑。特别是 configure_streams(),它直接决定了后续的预览、拍照、录像能不能正常工作。我建议你在写 HAL 时,把 stream 的合法性检查做充分,别等到运行时才发现问题。