第三章 Camera HAL模块加载:HIDL服务注册与发现、CameraProvider实现、模块初始化流程

好,咱们进入第三章。这一章讲的是Camera HAL启动的“第一公里”——模块加载。说白了,就是系统怎么找到你的HAL库,怎么把它拉起来,然后怎么让上层知道“嘿,我准备好了”。

这部分我当年刚接手时也踩过不少坑。你想想看,如果HAL都加载不起来,后面所有的工作都是白搭。所以这一章,咱们把每个环节掰开揉碎了讲清楚。

3.1 HIDL服务注册与发现

Android从8.0开始全面推行Treble架构,HAL和framework彻底解耦。怎么解耦的?靠的就是HIDL。

HIDL的全称是HAL Interface Definition Language。它定义了一套标准的通信接口。CameraProvider作为一个HIDL服务,需要向系统注册自己,然后framework才能发现它、调用它。

注册的过程,我习惯把它比作“开店”。你开了一家店(CameraProvider),你得在工商局注册(hwservicemanager),然后顾客(framework)才能找到你的店址,进来买东西。

3.1.1 服务注册流程

CameraProvider的注册,核心代码在 camera/provider/2.4/default/CameraProvider.cpp 里。关键函数是 registerAsService()

// 典型的注册代码
sp<ICameraProvider> provider = new CameraProvider();
status_t status = provider->registerAsService("default");
if (status != OK) {
    ALOGE("Failed to register CameraProvider service: %d", status);
    return -1;
}
ALOGI("CameraProvider service registered successfully");

这里有个细节要注意。registerAsService的参数是实例名。如果你只做单摄像头方案,用"default"就够了。但如果你做的是多摄像头或者外挂摄像头,我建议你给每个provider起个有意义的名称,比如"external/0"、"internal/0"。

注意: 服务名称一旦确定,不要轻易更改。因为framework端是通过这个名称来查找服务的。改名称意味着framework也要同步修改,否则就找不到你的HAL了。

3.1.2 服务发现机制

服务注册好了,framework怎么找到它?通过 ICameraProvider::getService()

// framework端查找服务
sp<ICameraProvider> provider = ICameraProvider::getService("default");
if (provider == nullptr) {
    ALOGE("Failed to get CameraProvider service");
    return;
}

嗯,这里有个坑。getService默认是阻塞的,它会一直等,直到服务出现或者超时。默认超时时间是5秒。如果你的HAL启动比较慢,比如需要初始化传感器或者加载固件,5秒可能不够。

我曾经在一个项目上遇到过,外挂USB摄像头初始化需要8秒,结果framework那边等了5秒就超时报错了。后来怎么解决的?在HAL端先快速注册一个空服务,等初始化完成后再更新真正的服务接口。或者,你也可以在framework端把超时时间调长一点。

小技巧: 调试阶段可以用 lshal 命令查看服务是否注册成功。输入 adb shell lshal | grep camera,如果看到你的provider,说明注册成功了。

3.2 CameraProvider实现

CameraProvider是HAL的入口点。它负责管理所有Camera设备。一个Provider可以管理多个Camera设备,比如前置、后置、广角、微距等等。

3.2.1 Provider接口定义

ICameraProvider.hal定义了Provider需要实现的接口。核心方法就几个:

方法名 功能 说明
getCameraIdList 获取所有Camera ID列表 返回当前Provider管理的所有摄像头ID
getCameraDeviceInterface 获取Camera设备接口 返回指定ID的ICameraDevice对象
setCallback 设置回调 用于通知framework状态变化

我个人习惯,在实现Provider时,先把getCameraIdList写好。因为framework第一个调用的就是它。如果这个接口返回空列表,framework会认为没有摄像头,直接跳过。

3.2.2 多Camera管理

实际项目中,一个Provider管理多个Camera是常态。我建议用map来管理:

// 内部管理结构
std::map<std::string, std::shared_ptr<CameraDevice>> mCameraDevices;

// 初始化时添加设备
mCameraDevices["0"] = std::make_shared<CameraDevice>(0, "back");
mCameraDevices["1"] = std::make_shared<CameraDevice>(1, "front");
mCameraDevices["2"] = std::make_shared<CameraDevice>(2, "external");

这里要注意Camera ID的命名规范。Android推荐用数字字符串,比如"0"、"1"、"2"。但如果你做的是外挂摄像头,也可以用"external/0"这种格式。不过,我建议你统一用数字,兼容性最好。

3.3 模块初始化流程

好了,前面铺垫了那么多,现在咱们看看完整的初始化流程。从系统启动到HAL就绪,大致分这么几步:

  1. hwservicemanager启动:这是HIDL服务的“工商局”,必须先启动。
  2. CameraProvider进程启动:init.rc中配置的service被拉起。
  3. HAL库加载:通过dlopen加载camera..so。
  4. HAL模块初始化:调用hal_module_struct.common.open()。
  5. Provider注册:调用registerAsService()。
  6. framework发现服务:通过getService()获取Provider。

这里面,第三步和第四步最容易出问题。我详细说说。

3.3.1 HAL库加载细节

HAL库的加载路径是固定的。Android会去 /vendor/lib/hw/ 或者 /vendor/lib64/hw/ 下找 camera.<variant>.so。variant通常对应你的硬件平台,比如 camera.sdm845.so

加载的核心代码在 CameraProvider.cppinitialize() 方法里:

// 加载HAL库
void* handle = dlopen("camera.sdm845.so", RTLD_NOW);
if (handle == nullptr) {
    ALOGE("Failed to dlopen HAL library: %s", dlerror());
    return;
}

// 获取HAL模块结构体
hal_module_struct_t* module = (hal_module_struct_t*)dlsym(handle, "HAL_MODULE_INFO_SYM");
if (module == nullptr) {
    ALOGE("Failed to get HAL module info: %s", dlerror());
    dlclose(handle);
    return;
}

这里有个关键点:RTLD_NOW 表示立即解析所有符号。如果库里有未定义的符号,dlopen会直接失败。我建议调试阶段先用 RTLD_LAZY,等稳定了再改成 RTLD_NOW

重点: HAL_MODULE_INFO_SYM 这个符号名是固定的,不能改。它是HAL模块的入口点。如果编译出来的so里没有这个符号,dlopen能成功,但dlsym会返回null。

3.3.2 模块初始化顺序

拿到HAL模块结构体后,接下来调用open方法。open方法会返回一个camera_device_t结构体,里面包含了所有操作函数指针。

// 打开Camera设备
camera_device_t* device = nullptr;
status_t status = module->common.open(&module->common, "0", (hw_device_t**)&device);
if (status != OK) {
    ALOGE("Failed to open camera device: %d", status);
    return;
}

open方法的第二个参数是Camera ID。这里要注意,open方法内部会做很多初始化工作,比如:

  • 加载传感器驱动
  • 初始化ISP
  • 分配内存池
  • 启动预览线程

这些操作如果放在open里做,会导致open耗时很长。我建议把耗时操作放到第一次调用startPreview或者startCapture时再做。open只做必要的初始化,比如分配资源、注册回调等。

我曾经在一个项目上,open方法里做了传感器校准,耗时3秒。结果framework那边以为HAL卡死了,直接kill了进程。后来我把校准挪到了第一次拍照时做,问题就解决了。

3.3.3 初始化失败处理

初始化过程中,任何一步失败都要妥善处理。我的经验是:

  • dlopen失败:检查so文件是否存在,权限是否正确。用 adb shell ls -l /vendor/lib/hw/camera.*.so 查看。
  • dlsym失败:检查HAL_MODULE_INFO_SYM是否导出。用 adb shell readelf -s camera.*.so | grep HAL_MODULE 查看。
  • open失败:检查硬件是否正常,驱动是否加载。用 adb shell dmesg | grep camera 查看内核日志。

注意: 初始化失败后,一定要释放已分配的资源。比如dlopen成功但dlsym失败,要调用dlclose。否则会造成资源泄漏。

3.4 实战经验总结

说了这么多,我总结几条实战经验:

  1. 服务注册要尽早:Provider进程启动后,尽快注册服务。哪怕设备还没准备好,先注册一个空服务,等准备好了再更新。
  2. 日志要打全:每个关键步骤都打日志。我习惯用ALOGI打正常流程,ALOGE打错误流程。调试时用 adb logcat -s CameraProvider 过滤。
  3. 超时处理要谨慎:framework端的getService默认5秒超时。如果你的初始化超过5秒,要么优化初始化流程,要么修改超时时间。
  4. 多Camera要并行初始化:如果Provider管理多个Camera,建议用线程池并行初始化,不要串行。串行的话,3个摄像头每个初始化2秒,总共6秒,用户早就不耐烦了。

嗯,这一章的内容就到这里。模块加载是整个HAL的基石,基础打好了,后面的开发才会顺利。下一章咱们聊聊Camera设备的具体操作,比如打开、关闭、配置流等。

记住,HAL开发没有捷径,但可以少走弯路。多打日志,多测试,多思考。遇到问题不要慌,先看日志,再查代码,最后问同事。实在不行,翻翻我的手册,说不定能找到答案。

调试小工具: 推荐用 adb shell dumpsys media.camera 查看Camera服务的状态。如果能看到你的Provider和Camera设备,说明初始化成功了。


公众号:蓝海资料掘金营,微信deep3321