3. 回调接口声明与注册:oneway 关键字作用、回调接口定义规则、服务端注册回调接口方法
Binder 跨进程回调的本质是:服务端持有客户端传递的 Binder 引用,在需要时反向调用客户端接口。本章聚焦回调接口的声明(AIDL)与服务端注册逻辑,重点剖析 oneway 关键字如何改变调用语义,并给出完整定义规则与注册实现。
3.1 oneway 关键字的作用
oneway 修饰 Binder 接口方法后,调用方(客户端或服务端)发送请求即返回,不等待对端处理完毕,也不接收返回值或异常。它解决了两类痛点:
- 避免因对端耗时操作阻塞调用线程(如 UI 线程回调)。
- 降低死锁风险(如服务端在回调中又同步调用客户端,而客户端正等待服务端回复)。
关键语义:oneway 方法必须返回
void,不可抛出异常(调用方无法感知远端异常),且 Binder 驱动可能合并多个 oneway 调用,不保证顺序(除非使用 FLAG_ONEWAY 的 IBinder.FLAG_ONEWAY 标志结合事务池)。| 特性 | 同步调用(默认) | oneway 异步调用 |
|---|---|---|
| 调用方阻塞 | 是,等待 BC_TRANSACTION 完成 | 否,立即返回 |
| 返回值/异常 | 支持,可被接收 | 不支持,必须 void |
| Binder 事务池 | 每笔独立,顺序保证 | 可能合并,不保序 |
| 典型场景 | 获取数据、带结果的 RPC | 回调通知、事件推送 |
3.2 回调接口定义规则
回调接口必须采用 AIDL 定义,且必须位于服务端 AIDL 文件或公用模块中。定义时需遵守以下规则:
- 接口名以
Callback后缀结尾,便于识别。 - 所有方法必须使用
oneway修饰(回调通常为异步通知,无需返回值)。 - 参数类型只能使用 Binder 支持的基础类型、
String、CharSequence、List、Map、Parcelable;禁止使用FileDescriptor以外的特殊类型。 - 参数不可包含 out 或 inout 限定符(oneway 方法不允许输出参数)。
- 若需要传递复杂对象,必须实现
Parcelable接口并在 AIDL 文件中显式声明。
常见陷阱:如果回调方法未声明
oneway,且服务端在 Binder 线程池中调用该回调,可能因服务端线程繁忙或死锁导致 ANR。务必对所有回调接口方法添加 oneway。// IMyServiceCallback.aidl
package com.example.binder;
oneway interface IMyServiceCallback {
void onNavigationComplete(String routeId);
void onError(int errorCode, String errorMsg);
}
3.3 服务端注册回调接口方法
服务端需要对外暴露一个 registerCallback 和 unregisterCallback 方法,用于客户端提交自己的回调代理。典型实现包括:
- 使用
RemoteCallbackList<IMyServiceCallback>管理多个回调,自动处理 Binder 死亡通知。 - 在注册时通过
linkToDeath监听客户端进程终止,避免悬空回调。 - 回调注册方法本身不应为
oneway(通常需要原子性保证)。
// IMyService.aidl
interface IMyService {
void registerCallback(IMyServiceCallback callback);
void unregisterCallback(IMyServiceCallback callback);
void navigateTo(String destination);
}
// 服务端实现片段
public class NavigationService extends IMyService.Stub {
private final RemoteCallbackList<IMyServiceCallback> mCallbacks
= new RemoteCallbackList<>();
@Override
public void registerCallback(IMyServiceCallback callback) throws RemoteException {
mCallbacks.register(callback);
// 可选:监听客户端死亡
callback.asBinder().linkToDeath(() -> {
mCallbacks.unregister(callback);
Log.d("NavService", "Callback died, auto-unregistered");
}, 0);
}
@Override
public void unregisterCallback(IMyServiceCallback callback) throws RemoteException {
mCallbacks.unregister(callback);
callback.asBinder().unlinkToDeath(mDeathRecipient, 0);
}
@Override
public void navigateTo(String destination) throws RemoteException {
// 模拟导航流程
int n = mCallbacks.beginBroadcast();
for (int i = 0; i < n; i++) {
try {
mCallbacks.getBroadcastItem(i).onNavigationComplete(destination);
} catch (RemoteException e) {
// 单个回调异常不影响其他
Log.w("NavService", "Callback failed: " + e.getMessage());
}
}
mCallbacks.finishBroadcast();
}
}
3.4 回调注册的生命周期管理
在服务端使用 RemoteCallbackList 能自动处理:
- 客户端 Binder 进程死亡时自动移除对应回调。
beginBroadcast/finishBroadcast保证遍历期间列表不被修改。- 支持多线程并发注册/注销,无需额外同步锁。
推荐做法:统一在
registerCallback 中通过 linkToDeath 注册死亡回调,在 unregisterCallback 中解绑,确保服务端不会持有无效 Binder 引用。3.5 oneway 对回调序列化与反序列化的影响
由于 oneway 调用不等待结果,Binder 驱动会将参数序列化到共享内存后立即返回。回调接口中的 Parcelable 对象必须确保:
- 实现
writeToParcel/readFromParcel正确读取所有字段。 - 成员变量不支持跨进程传递活对象(如 Handler、File),仅支持扁平化数据。
- 若回调参数中包含
List<Parcelable>,需使用in限定符(默认),确保序列化方向正确。
// NavigationResult.aidl
package com.example.binder;
parcelable NavigationResult {
String routeId;
int status;
long timestamp;
}
// AIDL 回调接口中使用包裹对象
oneway interface IMyServiceCallback {
void onNavigationComplete(NavigationResult result);
}
3.6 内联 SVG:回调注册与通知流程
3.7 多线程安全与注册陷阱
| 陷阱 | 后果 | 解决方案 |
|---|---|---|
回调方法未加 oneway | 服务端线程池阻塞,可能 ANR | 所有回调接口方法强制 oneway |
注册后未 linkToDeath | 服务端持有死 Binder,回调时 DeadObjectException | 注册时调用 callback.asBinder().linkToDeath() |
在 broadcast 回调中修改 RemoteCallbackList | 并发修改异常或死锁 | 仅允许在 begin/finishBroadcast 外部读写注册 |
多个线程同时 register/unregister | 竞争条件,回调列表损坏 | RemoteCallbackList 已内置 ThreadSafe,直接使用 |
3.8 调试与日志建议
在开发阶段,建议在服务端注册与回调广播处添加详细日志:
// 注册日志
Log.d("NavCallback", "registerCallback: proxy=" + callback.hashCode()
+ " pid=" + Binder.getCallingPid());
// 广播时记录目标数量
int count = mCallbacks.beginBroadcast();
Log.v("NavCallback", "broadcast to " + count + " clients");
for (int i = 0; i < count; i++) {
try {
mCallbacks.getBroadcastItem(i).onNavigationComplete(routeId);
} catch (RemoteException e) {
Log.e("NavCallback", "client " + i + " failed: " + e);
}
}
mCallbacks.finishBroadcast();
同时可在 linkToDeath 死亡回调中记录进程退出:
callback.asBinder().linkToDeath(() -> {
Log.w("NavCallback", "Client died, auto removing callback");
mCallbacks.unregister(callback);
}, 0);
3.9 稳定性压测要点
- 使用
ThreadPoolExecutor模拟多个客户端频繁注册/注销,检查RemoteCallbackList是否泄漏。 - 在 oneway 回调中注入延迟(
Thread.sleep(100)),观察服务端线程池是否被占满。 - 通过
adb shell dumpsys binder查看 Binder 节点引用计数,确保无死节点残留。