服务端与客户端实现:理解 Binder 基本流程

到了这一章,咱们就要真刀真枪地写代码了。Binder 通信不是玄学,它本质上就是 服务端暴露接口 → 客户端远程调用 这么一套流程。我个人习惯先把 AIDL 文件写好,再分别实现两端,这样结构最清晰。

1. 先定接口:AIDL 文件

你想想看,两个进程要通信,总得有个统一的“协议”吧?AIDL 就是干这个的。我用一个简单的计算器接口做例子:

// ICalculator.aidl
package com.example.binderdemo;

interface ICalculator {
    int add(int a, int b);
}

注意,这里的 int 是基本类型,直接支持。如果要传自定义对象,得实现 Parcelable,那是后话了。AIDL 编译后会生成对应的 Java 文件,里面包含 Stub 和 Proxy,咱们待会儿就会用到。

2. 服务端:继承 Stub,实现接口

服务端的核心是 Service。你需要在 onBind() 方法里返回一个 Binder 对象,这个对象就是客户端远程调用的“门把手”。

// CalculatorService.java
public class CalculatorService extends Service {

    private final ICalculator.Stub mBinder = new ICalculator.Stub() {
        @Override
        public int add(int a, int b) throws RemoteException {
            // 这里就是真正的业务逻辑
            return a + b;
        }
    };

    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }
}

避坑指南: 我曾经在早期项目里忘记在 AndroidManifest.xml 中声明这个 Service,结果客户端死活 bind 不上。记得加上 <service android:name=".CalculatorService" android:exported="true" />,否则外部进程访问不了。

这里我强调一点:mBinder 是一个匿名内部类,继承了 AIDL 生成的 ICalculator.Stub。AIDL 编译器会自动帮你处理好线程安全、跨进程数据序列化等等。说白了,你只需要关心业务逻辑怎么写就行。

3. 客户端:bindService,拿到代理

客户端要调用远程方法,第一步是绑定 Service。绑定成功后,会得到一个 IBinder,你需要用 ICalculator.Stub.asInterface() 把它“翻译”成你认识的接口。

// ClientActivity.java 片段
private ICalculator mCalculator;

private ServiceConnection mConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        // 关键一步:将 IBinder 转换为接口
        mCalculator = ICalculator.Stub.asInterface(service);
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {
        mCalculator = null;
    }
};

// 发起绑定
Intent intent = new Intent();
intent.setComponent(new ComponentName("com.example.binderdemo",
        "com.example.binderdemo.CalculatorService"));
bindService(intent, mConnection, Context.BIND_AUTO_CREATE);

// 调用远程方法(需要在子线程,因为 Binder 调用可能阻塞)
new Thread(() -> {
    try {
        int result = mCalculator.add(3, 5);
        Log.d("Client", "result = " + result);
    } catch (RemoteException e) {
        e.printStackTrace();
    }
}).start();

注意,onServiceConnected 是在主线程回调的。但远程调用本身应该放在子线程里做,避免阻塞 UI。我在项目中见过有人直接在回调里调用远程方法导致 ANR,后来改成异步才解决。

4. 完整流程梳理

为了让你有个全局概念,我画了一张简易流程图。你可以看到三个关键角色:服务端进程、Binder 驱动(内核)、客户端进程。箭头表示数据流动方向。

Binder 通信基本流程 服务端进程 实现 AIDL 接口 (继承 Stub) onBind() 返回 Stub Service 注册到 AMS 客户端进程 bindService() 获取 IBinder 代理 调用 proxy.add() Binder 驱动(内核) 1. 接收调用请求 2. 数据序列化/反序列化 3. 拷贝到目标进程 4. 唤醒目标线程 说明:虚线框表示进程边界,实心箭头表示调用/返回方向。数据实际通过内核驱动拷贝。

5. 关键点总结

环节 服务端 客户端
接口定义 实现 AIDL 接口方法 无
Binder 对象 继承 Stub 生成 Stub.asInterface() 转换
生命周期 Service,需声明 bind/unbind,注意解绑
线程模型 Binder 线程池处理 不要在 UI 线程调用

6. 一些经验之谈

  • 关于 Service 注册: Android 系统级服务会注册到 ServiceManager(现在叫 servicemanager 实例),但普通应用的服务通过 Intent 绑定,不需要你主动去注册。你只要写好 onBind() 就行了。
  • 关于异常处理: 远程调用可能抛出 RemoteException,比如服务端崩溃或者 Binder 连接断开。我习惯在每个远程调用外面包一层 try-catch,并且打印日志方便定位。
  • 小心内存泄漏: 客户端 bindService 后一定要在合适时机 unbindService(),否则 Service 会一直运行。我在项目中见过因为忘了 unBind 导致 Service 常驻,电量狂掉。

小提示: 如果你想调试 Binder 通信,可以在服务端和客户端分别打印线程 ID,观察是否在不同线程中执行。Binder 驱动会使用服务端进程的 Binder 线程池来执行远程方法,所以你的服务端代码必须考虑线程安全。

好了,写到这里你应该能跑通一个最简单的 Binder 示例了。把代码敲一遍,感受一下 IPC 的“透明”感——客户端调用方法就像本地调用一样,但背后其实跨了进程。理解了基本流程,后续咱们聊异常 Mock 和回放才有基础。