第4章 HIDL客户端开发:获取服务、调用接口与生命周期管理
客户端开发,说白了就是怎么去找到HIDL服务,然后调用它暴露出来的接口。很多刚接触HIDL的同学,以为服务端写完就万事大吉了。其实客户端这边坑更多。我早期做项目时,就因为在服务获取阶段没处理好,导致整个系统启动卡死——嗯,那次教训挺深刻的。
4.1 获取HIDL服务的两种方式
Android提供了两种获取HIDL服务的方式:默认服务和指定实例名服务。大部分场景下我们用默认服务就够了,但遇到多实例场景,就得用指定实例名。
| 获取方式 | API调用 | 适用场景 |
|---|---|---|
| 默认服务 | IService::getService() |
单实例、系统唯一服务 |
| 指定实例名 | IService::getService("instance_name") |
多实例、多硬件通道 |
核心要点:getService()是阻塞调用。如果服务还没注册,客户端会一直等下去。我建议在非关键路径上使用tryWait()或设置超时。
来看一个典型的客户端获取代码:
// 获取默认服务
sp<IHelloService> service = IHelloService::getService();
if (service == nullptr) {
ALOGE("获取IHelloService失败");
return;
}
// 获取指定实例的服务
sp<IHelloService> service2 = IHelloService::getService("camera0");
if (service2 == nullptr) {
ALOGE("获取camera0实例失败");
return;
}
这里有个细节:getService()内部会尝试多次连接。默认重试次数是4次,每次间隔1秒。如果4秒内服务还没起来,就会返回nullptr。我曾经遇到一个场景——服务端因为权限问题启动慢了,客户端这边等了4秒就放弃了。后来我把重试次数调到了10次,问题就解决了。
4.2 调用HIDL接口方法
拿到服务对象后,调用接口方法其实和调用普通C++对象差不多。但HIDL的调用是跨进程的,这里有几个性能相关的点要注意。
我个人习惯把频繁调用的接口参数尽量设计成基本类型或固定大小的结构体。避免传递大块数据,尤其是频繁传递。来看一个实际例子:
// 获取服务
sp<ICameraService> cameraService = ICameraService::getService();
if (cameraService == nullptr) {
ALOGE("无法获取Camera服务");
return;
}
// 调用接口方法
CameraStatus status;
int32_t result = cameraService->openCamera("back", &status);
if (result != OK) {
ALOGE("打开摄像头失败: %d", result);
return;
}
// 带回调的调用
sp<ICameraCallback> callback = new CameraCallback();
cameraService->setCallback(callback);
性能提示:HIDL调用一次跨进程通信大约耗时50-200微秒。如果在一个循环里频繁调用,建议把多个参数打包成一次调用,或者使用批量接口。
为什么会有这个耗时?因为每次调用都要经过binder驱动,涉及数据序列化、内核上下文切换、权限检查。你想想看,如果每帧图像处理都要调用几十次HIDL接口,那性能肯定扛不住。
4.3 处理回调与死亡通知
回调是HIDL客户端开发中最容易出问题的地方。我见过太多因为回调没处理好导致的野指针、死锁问题。
4.3.1 回调注册与生命周期
回调对象必须保证在服务端使用期间不被销毁。很多同学在栈上创建回调对象,函数返回后对象就析构了,服务端再调用回调时直接崩溃。
// 正确的做法:使用sp管理回调生命周期
class MyCallback : public IHelloCallback {
public:
// 实现回调接口
Return<void> onEvent(const HelloEvent& event) override {
ALOGI("收到事件: %s", event.description.c_str());
return Void();
}
};
// 注册回调
sp<MyCallback> cb = new MyCallback();
service->registerCallback(cb);
警告:不要在回调函数里做耗时操作。回调是在服务端的binder线程池中执行的,阻塞回调会阻塞整个服务端的binder处理。我曾经遇到一个案例,回调里加了日志写入操作,导致服务端响应延迟飙升。
4.3.2 死亡通知机制
服务端可能随时挂掉。如果客户端不处理死亡通知,就会一直持有无效的服务代理,调用时要么卡死要么崩溃。
// 注册死亡通知
class DeathRecipient : public hardware::hidl_death_recipient {
public:
void serviceDied(uint64_t cookie, const wp<::android::hidl::base::V1_0::IBase>& who) override {
ALOGE("服务挂了!cookie: %llu", cookie);
// 在这里做重连或恢复操作
reconnectService();
}
};
sp<DeathRecipient> deathRecipient = new DeathRecipient();
Return<bool> result = service->linkToDeath(deathRecipient, 0);
if (!result) {
ALOGE("注册死亡通知失败");
}
我个人习惯在死亡通知回调里做三件事:
- 标记服务状态为不可用
- 释放相关资源
- 启动重连定时器
注意,死亡通知回调也是在binder线程中执行的,同样不能做耗时操作。
4.4 客户端与服务端生命周期管理
生命周期管理是HIDL开发中最容易被忽视的部分。客户端和服务端的启动顺序、销毁顺序,稍有不慎就会出问题。
| 场景 | 问题 | 解决方案 |
|---|---|---|
| 客户端先启动 | getService()阻塞或超时 | 使用tryWait()或增加重试次数 |
| 服务端先销毁 | 客户端调用崩溃 | 注册死亡通知,及时清理 |
| 客户端先销毁 | 服务端回调野指针 | 客户端在析构时主动注销回调 |
| 服务重启 | 客户端持有旧代理 | 死亡通知后重新获取服务 |
来看一个完整的生命周期管理示例:
class ClientManager {
public:
bool init() {
// 1. 获取服务
mService = IMyService::getService();
if (mService == nullptr) return false;
// 2. 注册死亡通知
mDeathRecipient = new MyDeathRecipient(this);
mService->linkToDeath(mDeathRecipient, 0);
// 3. 注册回调
mCallback = new MyCallback(this);
mService->registerCallback(mCallback);
return true;
}
void shutdown() {
// 1. 先注销回调
if (mService != nullptr) {
mService->unregisterCallback(mCallback);
}
// 2. 解除死亡通知
if (mService != nullptr && mDeathRecipient != nullptr) {
mService->unlinkToDeath(mDeathRecipient);
}
// 3. 清理资源
mCallback.clear();
mDeathRecipient.clear();
mService.clear();
}
private:
sp<IMyService> mService;
sp<MyDeathRecipient> mDeathRecipient;
sp<MyCallback> mCallback;
};
最佳实践:客户端析构时一定要先注销回调,再解除死亡通知,最后释放服务代理。顺序反了可能导致回调被调用时访问已释放的内存。
我曾经在项目中遇到一个诡异的问题:服务端重启后,客户端调用接口返回了错误码,但没触发死亡通知。后来发现是因为客户端和服务端在同一进程里,binder不会触发死亡通知。嗯,这个坑踩得挺疼的。
4.5 本章知识体系
下面这张图总结了HIDL客户端开发的核心流程和关键节点:
这张图把客户端开发的三个核心阶段串起来了。从获取服务开始,到接口调用,再到生命周期管理,每一步都有对应的注意事项。我个人建议你在实际开发中,按照这个流程来设计客户端的代码结构。
调试技巧:可以用lshal命令查看当前系统中有哪些HIDL服务在运行。如果客户端获取不到服务,先用这个命令确认服务端是否正常注册了。
好了,HIDL客户端开发的核心内容就这些。记住三个关键点:服务获取要处理超时、回调函数不能阻塞、生命周期要双向管理。把这些搞明白了,客户端开发就不会出大问题。