3. Binder通信机制(下):AIDL原理与使用、Binder对象传递、死亡通知机制、Binder线程池管理

好,我们接着聊Binder。上一章我们把Binder的底层原理、驱动模型、内存映射都过了一遍。说实话,那些东西确实有点硬核。但这一章,我会带你从“会用”到“懂原理”,把AIDL、对象传递、死亡通知、线程池这些实战中天天打交道的东西讲透。

我个人习惯是:先搞清楚“它帮我解决了什么问题”,再去看“它怎么实现的”。这样学起来不累,而且记得牢。

3.1 AIDL:Binder的“语法糖”

AIDL全称是Android Interface Definition Language。说白了,它就是帮你自动生成Binder通信的样板代码。你想想看,如果没有AIDL,你要手写Binder、写Stub、写Proxy、处理Parcel的读写……那得多痛苦。

我记得刚入行那会儿,有个老项目就是手写的Binder,代码量巨大,而且容易出错。后来Google推了AIDL,大家才从“搬砖”里解放出来。

3.1.1 AIDL的基本使用

我们先看一个最简单的例子。假设我们要定义一个远程服务,提供“加法”功能。

第一步:定义AIDL接口

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

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

注意,AIDL文件里只支持基本数据类型、String、CharSequence、List、Map,以及Parcelable对象。如果你要传自定义对象,必须实现Parcelable接口。

第二步:实现Stub

编译后,系统会生成一个ICalculator.java文件。里面有一个Stub抽象类,你需要在服务端继承它。

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;
    }
}

第三步:客户端绑定服务

private ICalculator mCalculator;

private ServiceConnection mConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        mCalculator = ICalculator.Stub.asInterface(service);
        try {
            int result = mCalculator.add(3, 5);
            Log.d("TAG", "result: " + result);
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

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

你看,整个过程非常清晰。AIDL帮你把Binder的序列化、反序列化、跨进程调用都封装好了。

小提示: 我个人习惯把AIDL文件放在独立的module里,这样服务端和客户端可以共用同一份接口定义,避免不一致。

3.1.2 AIDL的底层原理

AIDL到底做了什么?我带你看看生成的Java代码就明白了。

生成的ICalculator.java里,核心是两个内部类:Stub和Proxy。

  • Stub:服务端使用。它继承自Binder,实现了ICalculator接口。当客户端调用transact()时,Stub的onTransact()会被调用,解析Parcel数据,调用真正的add()方法。
  • Proxy:客户端使用。它封装了Binder引用,调用add()时,会把参数写入Parcel,然后调用transact()发送给服务端,再读取返回结果。

说白了,AIDL就是帮你写好了这些“脏活累活”。你只需要关心业务逻辑就好。

3.2 Binder对象传递

跨进程传递对象,是Binder通信里最常用的场景之一。但这里有个坑:你传的对象,在另一个进程里是“复制品”,不是同一个对象。

为什么会这样?因为Binder通信本质上是跨进程的,两个进程的内存空间是隔离的。你传过去的对象,必须经过序列化和反序列化。

3.2.1 如何传递自定义对象

假设我们要传递一个User对象。

第一步:让User实现Parcelable

public class User implements Parcelable {
    private String name;
    private int age;

    // 必须有一个CREATOR
    public static final Creator<User> CREATOR = new Creator<User>() {
        @Override
        public User createFromParcel(Parcel in) {
            return new User(in);
        }

        @Override
        public User[] newArray(int size) {
            return new User[size];
        }
    };

    protected User(Parcel in) {
        name = in.readString();
        age = in.readInt();
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(name);
        dest.writeInt(age);
    }

    @Override
    public int describeContents() {
        return 0;
    }
}

第二步:在AIDL中声明

// IUserManager.aidl
package com.example.binder;
parcelable User;

interface IUserManager {
    User getUser(String name);
    void addUser(User user);
}
注意: 我曾经踩过一个坑——在AIDL里传大对象。比如传一个包含Bitmap的User对象,序列化开销非常大,容易导致ANR。建议只传轻量级的数据,或者用文件描述符(FD)传递大文件。

3.2.2 Binder对象作为参数传递

有时候,我们需要传递一个Binder对象本身。比如,服务端想给客户端一个回调接口。

在AIDL里,你可以直接传IBinder类型,或者传一个AIDL接口类型。

// ICallback.aidl
package com.example.binder;

interface ICallback {
    void onResult(int code);
}

// IDataService.aidl
import com.example.binder.ICallback;

interface IDataService {
    void registerCallback(ICallback callback);
    void unregisterCallback(ICallback callback);
}

这里有个关键点:客户端传过去的ICallback对象,实际上是一个Binder代理。服务端调用onResult()时,会跨进程调用到客户端的实现。这就是Binder对象传递的精髓——你传的不是数据,而是一个“远程调用能力”。

3.3 死亡通知机制

Binder通信是跨进程的。如果服务端进程挂了,客户端还在傻傻地等,那就会出问题。死亡通知机制就是用来解决这个问题的。

说白了,就是让客户端知道:“嘿,服务端挂了,你别等了。”

3.3.1 如何注册死亡通知

客户端可以通过Binder的linkToDeath()方法注册一个DeathRecipient。

private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
    @Override
    public void binderDied() {
        Log.e("TAG", "服务端挂了,准备重连");
        // 在这里做重连逻辑
        mCalculator = null;
        bindService();
    }
};

// 在onServiceConnected里注册
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
    try {
        service.linkToDeath(mDeathRecipient, 0);
        mCalculator = ICalculator.Stub.asInterface(service);
    } catch (RemoteException e) {
        e.printStackTrace();
    }
}

当服务端进程意外终止时,binderDied()会被回调。你可以在里面做重连、清理资源等操作。

小技巧: 我建议在binderDied()里不要做耗时操作,比如直接bindService()。因为死亡回调是在Binder线程里执行的,阻塞它会影响其他Binder调用。最好post到主线程再处理。

3.3.2 死亡通知的底层原理

底层是怎么实现的?Binder驱动会维护一个“死亡通知链表”。当服务端Binder实体死亡时,驱动会遍历这个链表,通知所有注册了死亡通知的客户端代理。

嗯,这里要注意:死亡通知是一次性的。如果你重连了,需要重新注册。

3.4 Binder线程池管理

Binder通信是同步的。客户端调用transact()后,会阻塞等待服务端返回。如果服务端处理慢,客户端线程就会一直卡着。

那服务端是怎么处理多个并发请求的呢?答案是:Binder线程池。

3.4.1 Binder线程池的工作原理

每个进程都有一个Binder线程池。默认情况下,线程池的大小是16个线程(不同Android版本可能略有差异)。当有Binder请求进来时,驱动会从线程池里取一个空闲线程来处理。

如果所有线程都在忙,新的请求会排队等待。如果排队时间太长,客户端就会触发ANR。

我记得有一次线上问题,就是服务端的一个Binder方法里做了网络请求,导致线程池被占满,其他Binder调用全部超时。最后定位到问题,把网络请求异步化就解决了。

3.4.2 如何优化Binder线程池

虽然我们不能直接修改Binder线程池的大小(那是系统控制的),但我们可以从代码层面优化:

  • 不要在Binder方法里做耗时操作:比如网络请求、数据库写入、大文件读写。这些应该放到后台线程去做。
  • 使用oneway关键字:如果客户端不需要等待结果,可以在AIDL方法前加oneway。这样调用会立即返回,不会阻塞客户端。
oneway void notifyEvent(String event);
  • 合理设计接口粒度:不要一个方法做太多事。拆分成多个小方法,可以减少单个Binder调用的耗时。
避坑指南: 我曾经在一个项目里,看到有人把Binder线程池当“万能线程池”用。在Binder方法里直接new Thread()做异步任务。这其实很危险,因为Binder线程本身是有限的,你把它占用了,其他请求就进不来了。正确的做法是:Binder方法只做“接收请求”和“分发任务”,真正的耗时操作交给专门的线程池。

3.5 本章小结

这一章我们聊了AIDL的原理与使用、Binder对象传递、死亡通知机制、Binder线程池管理。这些都是日常开发中高频接触的知识点。

我个人觉得,理解Binder通信的关键在于:跨进程的本质是“数据复制”和“远程调用”。AIDL帮你封装了这些细节,但底层原理你还是要懂,不然出了问题很难排查。

最后,送你一句话:Binder是Android的“血管”,理解它,你才能真正驾驭Android系统。

核心要点回顾:
  • AIDL是Binder的语法糖,自动生成Stub和Proxy
  • 跨进程传递对象需要实现Parcelable
  • 死亡通知机制用于检测服务端是否存活
  • Binder线程池默认16个线程,不要在Binder方法里做耗时操作
Binder通信机制(下)知识体系 AIDL原理与使用 Binder对象传递 死亡通知机制 Binder线程池 Stub/Proxy Parcelable 序列化 Binder引用 linkToDeath 重连机制 16线程 oneway 核心:跨进程的本质是“数据复制”和“远程调用” 避免在Binder方法里做耗时操作,防止线程池耗尽 死亡通知是一次性的,重连后需要重新注册 AIDL自动生成Stub和Proxy,简化Binder开发

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