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 文件或公用模块中。定义时需遵守以下规则:

  1. 接口名以 Callback 后缀结尾,便于识别。
  2. 所有方法必须使用 oneway 修饰(回调通常为异步通知,无需返回值)。
  3. 参数类型只能使用 Binder 支持的基础类型、String、CharSequence、List、Map、Parcelable;禁止使用 FileDescriptor 以外的特殊类型。
  4. 参数不可包含 out 或 inout 限定符(oneway 方法不允许输出参数)。
  5. 若需要传递复杂对象,必须实现 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:回调注册与通知流程

回调注册与 oneway 通知流程 客户端进程 (注册回调代理) 服务端进程 (RemoteCallbackList) ① registerCallback(proxy) ③ 广播回调 ② navigateTo 同步调用 ④ oneway 回调 onNavigationComplete ⑤ 异步处理结果 (客户端死亡时自动 unregister via linkToDeath) 关键点:回调通知全部使用 oneway,服务端不等待客户端处理完成;RemoteCallbackList 自动处理并发与死亡回收。

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 节点引用计数,确保无死节点残留。