7、Binder通信机制:Binder驱动原理、ServiceManager角色、AIDL实战

Binder,可以说是Android系统里最核心的通信机制了。我刚开始做嵌入式Android开发时,总觉得这玩意儿很玄乎——不就是个跨进程通信吗?后来踩了不少坑才明白,Binder的设计其实非常巧妙。今天咱们就把它彻底讲透。

7.1 为什么需要Binder?

先问个问题:Android为啥不用Linux自带的管道、共享内存或者Socket?

嗯,原因其实很简单。Android系统里,每个应用都在自己的进程里跑。进程之间是隔离的,你不能直接访问别人的内存。但系统服务(比如ActivityManager、WindowManager)又需要跟所有应用打交道。这时候就需要一个高效、安全、且支持双向通信的机制。

Binder就是为此而生的。它比Socket快(一次拷贝就够了),比共享内存安全(有权限校验),而且支持同步调用。我在项目中遇到过用Socket做跨进程通信的情况,延迟高不说,还得自己处理粘包拆包,后来全改成Binder了。

7.2 Binder驱动原理

Binder的核心,其实是内核里的一个虚拟字符设备驱动——/dev/binder。它负责把用户空间的请求,传递到另一个进程。

说白了,Binder驱动干的就是三件事:

  • 内存映射(mmap):每个Binder进程在内核空间里有一块缓冲区。发送数据时,数据从发送进程的用户空间拷贝到内核空间,然后直接映射到接收进程的用户空间。整个过程只有一次拷贝,效率很高。
  • 线程管理:Binder驱动维护了一个线程池。当请求过来时,驱动会从目标进程的线程池里挑一个空闲线程来处理。
  • 身份校验:每个Binder调用都携带了调用者的UID/PID。驱动会检查权限,防止恶意进程冒充别人。

关键点:Binder通信是同步的。发送方调用transact()后会阻塞,直到接收方返回结果。这在设计接口时一定要注意——不要在Binder回调里做耗时操作,否则会卡住调用方。

我曾经在项目里遇到过一个诡异的问题:某个系统服务偶尔会超时。查了半天才发现,是服务端在Binder回调里做了数据库写入,导致调用方线程被阻塞了十几毫秒。嗯,后来改成异步处理就解决了。

7.3 ServiceManager的角色

ServiceManager,你可以把它理解成一个“电话本”。系统里所有的Binder服务,都要在ServiceManager里注册。客户端想用某个服务,得先找ServiceManager拿到这个服务的Binder引用。

流程是这样的:

  1. 服务端注册:服务进程调用ServiceManager.addService(),把自己的Binder对象注册进去。
  2. 客户端获取:客户端调用ServiceManager.getService(),传入服务名称,拿到Binder引用。
  3. 直接通信:拿到引用后,客户端和服务端直接通过Binder驱动通信,不再经过ServiceManager。

ServiceManager本身也是一个Binder服务,它的句柄是0。所有进程在启动时,都会通过这个句柄跟ServiceManager建立连接。

小技巧:在嵌入式设备上,如果系统服务启动顺序不对,可能导致ServiceManager里查不到服务。我建议在客户端加一个重试机制,比如每隔100ms查一次,最多查10次。这样能避免服务还没注册好就调用的问题。

7.4 AIDL实战

AIDL(Android Interface Definition Language)是Android提供的一种接口定义语言。你只需要写一个.aidl文件,编译工具就会自动生成Binder通信的Java代码。

咱们直接上手写一个例子。假设我们要做一个简单的计算器服务,支持加法和减法。

7.4.1 定义AIDL接口

先创建一个ICalculator.aidl文件:

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

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

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

7.4.2 实现服务端

服务端是一个Service,在onBind()里返回Binder对象:

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 int subtract(int a, int b) throws RemoteException {
            return a - b;
        }
    };

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

然后在AndroidManifest.xml里注册这个Service,最好加上android:process=":remote",让它跑在独立进程里,这样才是真正的跨进程通信。

7.4.3 客户端调用

客户端通过bindService()绑定服务,拿到Binder后转成AIDL接口:

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("BinderDemo", "3 + 5 = " + result);
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

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

// 绑定服务
Intent intent = new Intent(this, CalculatorService.class);
bindService(intent, mConnection, Context.BIND_AUTO_CREATE);

注意:Binder调用可能抛出RemoteException,比如服务端进程挂了。一定要加try-catch,否则你的应用会崩溃。我曾经在线上环境遇到过服务端被系统杀掉的情况,客户端没处理异常,直接闪退。嗯,从那以后我再也不敢漏掉RemoteException了。

7.5 Binder通信流程图

下面这张图展示了Binder通信的完整流程,从客户端调用到服务端响应:

Binder通信流程图 客户端进程 调用 transact() 发送请求数据 等待返回结果 系统调用 Binder驱动(内核) 拷贝数据到内核空间 查找目标进程 唤醒目标线程 数据映射 服务端进程 接收请求 执行 onTransact() 返回结果 返回结果(同步) ServiceManager 服务注册与查询 getService() addService() 1. 服务端先向ServiceManager注册 2. 客户端通过ServiceManager获取服务引用 3. 客户端与服务端直接通过Binder驱动通信

7.6 避坑指南

最后,分享几个我在实际项目中踩过的坑:

  • Binder线程池耗尽:Binder默认的线程池大小是16个。如果你的服务端同时处理大量请求,线程池可能被占满,导致新的请求排队。我建议在服务端用异步方式处理耗时操作,不要占用Binder线程。
  • Binder对象泄漏:客户端拿到Binder引用后,如果不再使用,记得释放。否则会导致服务端进程的Binder引用计数一直增加,最终可能触发异常。
  • 跨进程传递大数据:Binder对数据大小有限制,默认是1MB。超过这个大小会抛出TransactionTooLargeException。如果你要传大文件,建议用文件描述符(FileDescriptor)或者ContentProvider。

个人习惯:我在写AIDL接口时,会尽量把参数和返回值设计成基本类型或String。这样既简单又高效。如果非要传复杂对象,一定要实现Parcelable,并且注意对象的序列化大小。

好了,Binder通信机制就讲到这里。记住一句话:Binder是Android的血管,ServiceManager是心脏,AIDL是血管里的血液。理解了这三者的关系,你就能在嵌入式Android开发中游刃有余了。


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