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的序列化、反序列化、跨进程调用都封装好了。
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);
}
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()会被回调。你可以在里面做重连、清理资源等操作。
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调用的耗时。
3.5 本章小结
这一章我们聊了AIDL的原理与使用、Binder对象传递、死亡通知机制、Binder线程池管理。这些都是日常开发中高频接触的知识点。
我个人觉得,理解Binder通信的关键在于:跨进程的本质是“数据复制”和“远程调用”。AIDL帮你封装了这些细节,但底层原理你还是要懂,不然出了问题很难排查。
最后,送你一句话:Binder是Android的“血管”,理解它,你才能真正驾驭Android系统。
- AIDL是Binder的语法糖,自动生成Stub和Proxy
- 跨进程传递对象需要实现Parcelable
- 死亡通知机制用于检测服务端是否存活
- Binder线程池默认16个线程,不要在Binder方法里做耗时操作
公众号:蓝海资料掘金营,微信deep3321