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,结果服务端改了对象,客户端拿到的还是原来的值。排查了半天,最后发现是方向标记搞错了。

小提示:能用in就别用inout。inout会多一次序列化/反序列化操作,性能有损耗。除非你真的需要双向修改,否则别偷懒。

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代理就变成“死亡”状态了。所以一定要配合死亡通知来用。

注意:Binder对象跨进程传递时,不要传太大的对象。Binder事务缓冲区默认只有1MB,超过这个大小会抛出TransactionTooLargeException。我见过有人把Bitmap序列化后通过Binder传,结果直接崩了。

4.3 死亡通知:别让你的Binder变成“僵尸”

Binder死亡通知,说白了就是当服务端进程挂了,客户端能收到一个回调。这个机制非常重要。你想想看,如果你的App持有一个远程Binder引用,服务端突然挂了,你还傻乎乎地继续调用,那就会抛出DeadObjectException

注册死亡通知的方式有两种:

  1. linkToDeath:给Binder代理注册一个DeathRecipient
  2. 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,记得切到主线程。

核心要点:死亡通知不是可选项,而是必选项。只要你的App持有远程Binder引用,就必须注册死亡通知。否则,服务端一挂,你的App就等着崩溃吧。

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。

优化建议:Binder池中的子Binder可以缓存起来,避免每次都重新查询。但缓存要配合死亡通知一起用——一旦Binder池死亡,清空所有缓存。

4.5 知识体系总览

说了这么多,咱们用一张图来总结一下Binder通信机制的核心脉络:

Binder通信机制核心知识体系 AIDL接口定义 in/out/inout方向标记 Binder对象传递 跨进程引用传递 死亡通知 linkToDeath回调 Binder池优化 单一Service管理多个Binder接口 AIDL定义接口 → Binder对象跨进程传递 → 死亡通知保活 → Binder池统一管理 关键点: • AIDL方向标记影响序列化性能,能用in就别用inout • Binder对象传递时注意1MB缓冲区限制 • 死亡通知是必选项,不是可选项

这张图把咱们今天讲的内容串起来了。从AIDL接口定义开始,到Binder对象跨进程传递,再到死亡通知保活,最后用Binder池做统一管理。每一步都有坑,每一步也都有优化空间。

好了,这一章的内容就到这里。Binder这块东西,说深也深,说浅也浅。关键是你得在实际项目中多踩坑、多总结。下次遇到跨进程通信的问题,希望你能想起今天聊的这些内容。


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