4、Binder通信机制(下):AIDL使用、Binder对象传递、死亡通知、Binder池优化
好,咱们接着聊Binder。上一章我把Binder的驱动层原理讲了个七七八八,这一章咱们落地到实际开发中。说白了,你平时写Android应用,真正跟Binder打交道最多的场景就是AIDL。我见过不少开发同学,AIDL写得飞起,但一问到Binder对象怎么传、死亡通知怎么收,就有点含糊了。嗯,今天咱们把这些坑都填上。
4.1 AIDL的正确打开方式
AIDL这东西,全称是Android Interface Definition Language。它的作用就是帮你自动生成Binder接口的桩代码。我个人习惯是,只要涉及跨进程通信,优先考虑AIDL,而不是用Messenger或者广播。为什么?因为AIDL生成的代码结构清晰,性能也好,而且支持自定义对象传递。
写AIDL文件时,有几个要点你得记住:
- 接口和方法名要语义化,别用拼音
- 参数和返回值类型必须是AIDL支持的类型
- 自定义对象必须实现Parcelable接口
- in、out、inout的方向标记别搞混
来看一个典型的AIDL定义:
// IBookManager.aidl
package com.example.binder;
import com.example.binder.Book;
interface IBookManager {
List<Book> getBookList();
void addBook(in Book book);
void registerListener(IOnNewBookArrivedListener listener);
void unregisterListener(IOnNewBookArrivedListener listener);
}
这里有个细节——in关键字。它告诉Binder驱动,这个参数是从客户端流向服务端的。如果你不写,默认就是in。但如果你需要服务端修改这个对象并返回给客户端,就得用inout。我在项目中遇到过一个问题:有个同事把inout写成了in,结果服务端改了对象,客户端拿到的还是原来的值。排查了半天,最后发现是方向标记搞错了。
4.2 Binder对象的跨进程传递
你可能会问:Binder对象本身能不能跨进程传递?答案是肯定的。而且这是Binder机制里非常强大的一个特性。
Binder对象在驱动层有一个唯一的标识——handle。当你在进程A中拿到一个Binder引用,把它传给进程B时,驱动会自动在进程B中创建一个新的Binder代理对象。这个代理对象指向的还是进程A中的那个真实Binder对象。说白了,就是同一个Binder对象,在不同进程中有不同的“化身”。
具体怎么传?通过Bundle.putBinder()或者AIDL接口参数。我举个例子:
// 服务端
public class MyService extends Service {
private final IBinder mBinder = new MyBinder();
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
private class MyBinder extends IMyInterface.Stub {
@Override
public void passBinder(IBinder anotherBinder) {
// 这里拿到的anotherBinder是客户端传过来的Binder代理
// 可以调用它的方法,或者注册死亡通知
}
}
}
我记得有一次做插件化框架,需要把宿主进程的Binder传给插件进程。当时就是通过AIDL接口参数传递的。这里有个坑:Binder对象传过去之后,如果宿主进程挂了,插件进程里的Binder代理就变成“死亡”状态了。所以一定要配合死亡通知来用。
4.3 死亡通知:别让你的Binder变成“僵尸”
Binder死亡通知,说白了就是当服务端进程挂了,客户端能收到一个回调。这个机制非常重要。你想想看,如果你的App持有一个远程Binder引用,服务端突然挂了,你还傻乎乎地继续调用,那就会抛出DeadObjectException。
注册死亡通知的方式有两种:
- linkToDeath:给Binder代理注册一个DeathRecipient
- unlinkToDeath:取消注册
代码长这样:
IBinder binder = service.asBinder();
binder.linkToDeath(new IBinder.DeathRecipient() {
@Override
public void binderDied() {
// 服务端挂了,在这里做清理和重连
Log.e("Binder", "服务端挂了,准备重连...");
reconnectService();
}
}, 0);
我曾经在一个即时通讯项目里踩过这个坑。当时没有注册死亡通知,结果服务端进程被系统杀掉了,客户端还在不断地发消息。每次发消息都抛DeadObjectException,然后客户端就卡死了。后来加上死亡通知,在binderDied()里重新绑定服务,问题就解决了。
这里有个细节:linkToDeath必须在主线程调用吗?其实不是。它可以在任何线程调用,但回调binderDied()是在Binder线程池中执行的。所以如果你要在回调里更新UI,记得切到主线程。
4.4 Binder池优化:别让服务端成为瓶颈
好了,咱们聊点进阶的。Binder池是什么?说白了,就是用一个Service管理多个Binder接口。你想想看,如果你的App有10个跨进程接口,难道要开10个Service吗?那也太浪费了。Binder池的思路是:只开一个Service,通过一个唯一的Binder对象来分发不同的接口。
实现方式很简单:
public class BinderPoolService extends Service {
private static final int BINDER_TYPE_BOOK = 0;
private static final int BINDER_TYPE_USER = 1;
private final IBinder mBinderPool = new BinderPool.Stub() {
@Override
public IBinder queryBinder(int binderCode) {
switch (binderCode) {
case BINDER_TYPE_BOOK:
return new BookManagerImpl();
case BINDER_TYPE_USER:
return new UserManagerImpl();
default:
return null;
}
}
};
@Override
public IBinder onBind(Intent intent) {
return mBinderPool;
}
}
客户端调用时,先拿到BinderPool的代理,然后通过queryBinder()获取具体的接口。这样做的好处很明显:
- 减少Service数量,节省系统资源
- 统一管理Binder连接,方便做重连和缓存
- 接口扩展方便,加一个新接口只需要加一个binderCode
我在做大型App架构时,Binder池几乎是标配。但有个问题要注意:Binder池本身也是一个Binder对象,它也会死亡。所以你需要给Binder池也注册死亡通知。一旦Binder池挂了,所有通过它获取的子Binder都会失效。这时候需要重新绑定Service,重新查询所有子Binder。
4.5 知识体系总览
说了这么多,咱们用一张图来总结一下Binder通信机制的核心脉络:
这张图把咱们今天讲的内容串起来了。从AIDL接口定义开始,到Binder对象跨进程传递,再到死亡通知保活,最后用Binder池做统一管理。每一步都有坑,每一步也都有优化空间。
好了,这一章的内容就到这里。Binder这块东西,说深也深,说浅也浅。关键是你得在实际项目中多踩坑、多总结。下次遇到跨进程通信的问题,希望你能想起今天聊的这些内容。