返回技术文章
更新:2026/09/01约 14 分钟

重新理解 Binder:从一次 AIDL 调用说起

重读 2018 年写下的 Binder 文章,从一次 AIDL 调用重新梳理 Proxy、BinderProxy、驱动、一次拷贝和线程模型,也修正几个过去没有讲准确的问题。

AndroidBinderAIDL
重新理解 Binder:从一次 AIDL 调用说起 文章封面
文章目录 · 8 章

一. 前言

2018 年,我写过一篇《写给 Android 应用工程师的 Binder 原理剖析》。

那篇文章我准备了很久。看了不少资料,也翻了不少源码,真正开始写的时候还是有些忐忑。Binder 牵扯的层次太多了,从 Java 到 native,再到内核驱动,任何一层没想明白,都可能把一个似是而非的结论传递给读者。

几年后再读,我依然觉得那篇文章完成了它当时的任务。它把进程隔离、内存映射、ServiceManager、Binder 驱动和 AIDL 串到了一起,让一个应用工程师可以先看到 Binder 的全貌。

但也有一些地方,现在的我不会再那样写了。

比如,我曾经把 Binder 描述成“驱动把服务端对象转换成客户端代理对象”。这句话很容易理解,却把 AIDL 生成代码和 Binder 驱动的职责混到了一起。再比如,我们经常说 Binder 只有一次拷贝,但这“一次”究竟指什么,mmap() 又映射了什么,并没有真正说清楚。

这也是我重新写这篇文章的原因。

我不打算重新铺一遍 Linux IPC 的全部背景,也不想把 Binder 的每个知识点都塞进来。这次只从应用开发中最熟悉的一次 AIDL 调用开始,顺着这次调用往下走。走到哪里,就解决那一层最容易混淆的问题。

二. 先从一次 addBook() 调用开始

还是用一个很简单的例子。假设远程服务提供了一个添加图书的方法:

// IBookManager.aidl
package com.byron.binderdemo;

import com.byron.binderdemo.Book;

interface IBookManager {
    void addBook(in Book book);
}

服务端实现 AIDL 生成的 Stub:

private val binder = object : IBookManager.Stub() {
    override fun addBook(book: Book) {
        repository.add(book)
    }
}

override fun onBind(intent: Intent): IBinder = binder

客户端绑定服务以后,在 onServiceConnected() 中拿到一个 IBinder:

override fun onServiceConnected(name: ComponentName, service: IBinder) {
    bookManager = IBookManager.Stub.asInterface(service)
}

然后就可以像调用普通对象一样调用远程服务:

bookManager?.addBook(Book("深入理解 Binder"))

如果 Client 和 Server 位于不同进程,这一行代码背后大致会经过下面这条路径:

一次 AIDL 调用从客户端穿过 Binder 驱动并进入服务端的完整路径

先记住图里的主线就够了:

业务代码 → AIDL Proxy → BinderProxy → Binder 驱动 → Server Binder 线程 → Stub → 服务实现

这里面有两个名字很像的东西:AIDL 生成的 Proxy 和系统里的 BinderProxy。我以前理解 Binder 时,曾经把它们模糊地看成同一层代理。事实上,这恰恰是理解整条调用链的第一个关键点。

三. asInterface() 到底返回了什么

客户端拿到 IBinder 后,第一步通常是调用:

IBookManager.Stub.asInterface(service)

把生成代码简化以后,asInterface() 的逻辑并不复杂:

public static IBookManager asInterface(IBinder binder) {
    if (binder == null) {
        return null;
    }

    IInterface local = binder.queryLocalInterface(DESCRIPTOR);
    if (local instanceof IBookManager) {
        return (IBookManager) local;
    }

    return new Proxy(binder);
}

它先通过 queryLocalInterface() 查找本地实现。

如果 Client 和 Server 在同一进程,查找能够成功,asInterface() 返回的就是服务端真正的对象。后面的 addBook() 只是一笔普通的方法调用,不需要进入 Binder 驱动,也不会切换到 Binder 线程。

如果双方不在同一进程,Client 当然不可能拿到另一个进程中的 Java 对象。此时 queryLocalInterface() 返回空,asInterface() 创建 AIDL 生成的 Proxy。

这个 Proxy 懂的是业务接口。它知道 addBook() 对应哪个 transaction code,也知道 Book 应该按照什么顺序写入 Parcel。

但 Proxy 自己并没有跨进程通信的能力,它内部还持有一个 IBinder:

IBookManager.Stub.Proxy
        ↓ 持有
android.os.BinderProxy
        ↓ 对应
当前进程中的 Binder handle

BinderProxy 不知道什么是图书,也不知道 addBook() 是做什么的。它代表的是另一个进程中的某个 Binder node,负责把已经准备好的 transaction 送到更底层。

所以这两层代理的分工其实很清楚:

  • AIDL Proxy 负责把“方法调用”翻译成 transaction;
  • BinderProxy 负责把 transaction 发给“那个远端 Binder”。

旧文中我写过,Binder 驱动会把服务端对象转换成客户端的代理对象。作为一种帮助入门的比喻,它并非完全不能用,但不够准确。

驱动真正处理的是 Binder 引用在不同进程中的表示。服务端的 Binder 对象在驱动中对应一个 node,Client 进程得到的是指向这个 node 的 handle。至于那个拥有 addBook() 方法的 AIDL Proxy,是编译器生成的代码,不是驱动凭空创建出来的业务对象。

把这件事分清楚以后,后面的调用过程就顺了。

四. 一次 transaction 是怎样走到服务端的

AIDL Proxy 中的 addBook(),简化以后大概是下面这样:

public void addBook(Book book) throws RemoteException {
    Parcel data = Parcel.obtain();
    Parcel reply = Parcel.obtain();

    try {
        data.writeInterfaceToken(DESCRIPTOR);
        data.writeTypedObject(book, 0);

        remote.transact(
            Stub.TRANSACTION_addBook,
            data,
            reply,
            0
        );

        reply.readException();
    } finally {
        reply.recycle();
        data.recycle();
    }
}

这里没有传递 Java 方法,也没有把 Book 对象直接交给另一个进程。Proxy 做的事情只有两类:

  1. 用 transaction code 表明要调用 addBook();
  2. 把调用参数按照约定的格式写进 Parcel。

remote.transact() 接着进入 BinderProxy,再通过 JNI 进入 native Binder。当前线程最终使用 ioctl(BINDER_WRITE_READ) 和 Binder 驱动交换命令。

驱动收到 transaction 后,会根据 Client 传入的 handle 找到对应的 Binder node,再找到真正持有这个 node 的 Server 进程。

这里的 handle 不是服务端对象地址。它只在当前进程的 Binder 引用表中有意义。同一个 Binder node,在不同进程里很可能对应不同的 handle;反过来,同一个数字在两个进程里,也可能指向完全不同的 node。

我更愿意把 handle 理解成进程手里的一个号码牌。Client 只认识号码,号码背后究竟是哪一个 node,由驱动维护。

Server 进程中的 Binder 线程收到事务后,Java Binder 层会把调用送到 Stub.onTransact():

protected boolean onTransact(
    int code,
    Parcel data,
    Parcel reply,
    int flags
) throws RemoteException {
    switch (code) {
        case TRANSACTION_addBook: {
            data.enforceInterface(DESCRIPTOR);
            Book book = data.readTypedObject(Book.CREATOR);
            data.enforceNoDataAvail();

            addBook(book);

            reply.writeNoException();
            return true;
        }
    }

    return super.onTransact(code, data, reply, flags);
}

onTransact() 根据 code 找到对应的方法,按照和 Client 相同的顺序读取参数,然后调用服务端实现的 addBook()。

如果这是一个普通的同步调用,Server 执行完成后还要把 reply 返回给 Client。刚才等待结果的 Client 线程被唤醒,这次方法调用才真正结束。

把中间细节暂时拿掉,一次同步 Binder 调用其实就是:

Client 线程发出 transaction 并等待
                ↓
Server Binder 线程执行服务方法
                ↓
Server 返回 reply,Client 线程继续运行

它看起来像本地方法调用,只是为了让上层代码更容易使用。底下仍然是一笔需要跨进程、跨线程完成的 RPC。

五. 再谈 Binder 的“一次拷贝”

Binder 经常被概括成“一次拷贝”。我在旧文里也花了很大篇幅解释这件事,当时的示意图画了多块内核缓冲区和多组映射关系。现在回头看,那种画法虽然试图解释 mmap(),反而容易让人误以为 Binder 在内核里还要来回搬运几次数据。

我们先回到传统的内核中转模型。

发送进程准备好数据后,内核先通过 copy_from_user() 把数据复制到内核缓冲区。接收进程读取时,内核再通过 copy_to_user() 把数据复制到接收方的用户缓冲区。

发送进程用户缓冲区
        ↓ copy_from_user
内核中转缓冲区
        ↓ copy_to_user
接收进程用户缓冲区

这就是常说的两次拷贝。

Binder 的做法不同。目标进程初始化 Binder 时,会通过 mmap() 建立 Binder 事务缓冲区的用户地址映射。驱动处理 transaction 时,把发送方的数据直接复制到为目标进程分配的事务缓冲页。目标 Binder 线程之后可以通过自己的映射地址读取这些页面,不需要再做一次从内核中转区到接收方缓冲区的复制。

Binder 驱动把发送方 Parcel 数据直接复制到目标进程事务缓冲页

所以 Binder 语境中的“一次拷贝”,更准确地说是 transaction 数据从发送方用户地址进入目标进程 Binder 事务缓冲页的这一次复制。

这里还有两个容易产生误解的地方。

第一,mmap() 并没有让 Client 和 Server 共享同一个 Java 对象。Client 中的 Book 仍然要被序列化进 Parcel,Server 也要从 Parcel 中读出属于自己进程的对象。

第二,“一次拷贝”不等于整个调用过程中只发生了一次内存复制。对象序列化、反序列化、reply 返回、字符串和数组处理,都可能产生额外开销。它描述的是驱动转移 transaction 数据时采用的路径,不应该被扩大成对整次调用性能的绝对结论。

因此我现在不会再简单地说“Binder 因为一次拷贝,所以一定比其他 IPC 快”。Binder 更重要的价值,是在 Android 大量小型 RPC 场景中,把数据传输、远程引用、线程调度和调用方身份放进了一套统一机制里。

六. ServiceManager 没有参与每一次 Binder 调用

旧文借用了 DNS 来解释 ServiceManager,这个类比到今天依然很好用:Server 用名称注册服务,Client 再通过名称找到对应的 Binder 引用。

但这个类比也很容易被用过头,仿佛所有 Binder 调用都要先经过 ServiceManager。

事实上,ServiceManager 解决的是 Framework 系统服务的注册和发现。以 ServiceManager.getService() 为例,Client 可以通过当前 Binder context 中约定的 0 号 handle 查询某个系统服务。查询成功后,驱动会在 Client 进程中建立指向目标 node 的 handle。

ServiceManager 负责系统服务的注册和查询,后续业务调用由 Binder 驱动直接路由

一旦 Client 拿到这个 Binder 引用,后面的业务 transaction 会由驱动直接发送给 Server,不会每次都绕道 ServiceManager。这一点和 DNS 的类比倒是非常贴切:DNS 帮我们找到目标,但后续数据不会一直经过 DNS。

本文开头使用的又是另一条路径:应用调用 bindService() 绑定普通 Service。

这种情况下,应用不是自己拿着服务名称去查询 ServiceManager。系统进程中的 ActivityManagerService 负责解析组件、管理 Service 和连接关系,再通过回调把服务端返回的 IBinder 交给 Client。

两条路径的入口不同,但底层有一件事是相通的:Binder 引用本身可以通过 transaction 传给另一个进程。 驱动会识别其中的 Binder 对象,为接收进程建立对应的 handle,而不是把服务端虚拟地址原样交出去。

七. 真正容易踩坑的地方,其实在线程上

研究 Binder 时,我们很容易把注意力都放在驱动、mmap() 和 node/handle 上。但对于应用开发来说,最容易真正引发线上问题的,往往还是线程。

7.1 跨进程不等于异步

没有标记 oneway 的 AIDL 方法,默认是同步调用。

如果在主线程里调用一个耗时远程方法,主线程会一直等待 Server 返回 reply。业务代码虽然运行在另一个进程,等待结果的却仍然是当前这条主线程。远端做数据库查询、文件读写或者复杂计算,一样可能把调用方拖进 ANR。

把代码放进另一个进程,并不会自动得到异步能力。

7.2 同一个接口,可能有两种线程语义

前面提到,Stub.asInterface() 有可能返回本地实现,也可能返回 AIDL Proxy。

本地实现在哪条线程调用,就在哪条线程执行;远程调用则通常由 Server 的 Binder 线程处理。也就是说,同一个接口在本地测试和跨进程运行时,可能呈现完全不同的并发行为。

服务端实现不能假设自己一定运行在主线程,也不能因为本地调用没有并发问题,就认为远程调用也一定安全。

7.3 oneway 也不是一剂万能药

oneway 表示 Client 发出远程 transaction 后不再等待 reply,适合不需要返回结果的通知类接口。

但它并不等于“为每次调用创建一条新线程”。针对同一个 Binder node 的异步事务仍然需要保持顺序;如果 Server 消费不及时,transaction 也会积压并占用 Binder 缓冲区。

同进程调用时,oneway 更不会把普通方法突然变成异步方法。

还有一个不那么常见、但出了问题很难查的情况:Binder 允许嵌套调用。进程 A 同步调用进程 B,B 在处理过程中又同步回调 A,这条调用链就可能发生重入。

如果 A 持有一把锁等待 B,而 B 回调 A 时又需要这把锁,死锁就出现了。因此我一直觉得有一条经验很值得记住:

不要在持有锁时调用一段无法完全控制的 Binder 代码。

除了线程,应用层还应该记住两个现实限制。

一个是 Binder 的事务缓冲区。我们常说它大约是 1MB,但这是进程级共享缓冲区,并不是每次调用都独享 1MB。多个并发 transaction 会共同占用它,所以单次数据没有达到 1MB,也可能抛出 TransactionTooLargeException。大 Bitmap、大数组和大段二进制数据,本来就不应该直接塞进 AIDL 或 Bundle。

另一个是远端进程随时可能死亡。linkToDeath() 能帮助 Client 尽快收到死亡通知,但不能保证下一次调用一定成功。RemoteException 和 DeadObjectException 不是极端异常,而是远程调用模型的一部分。

这些问题比背下一串 Binder 驱动命令更接近应用工程师的日常工作。

八. 我现在怎样理解 Binder

重新走完这次 addBook() 调用以后,我现在更愿意从下面几个角度理解 Binder:

  • 对应用代码来说,它提供了一种接近本地接口调用形式的 RPC;
  • 对 Client 来说,它拿到的是由 handle 表示的远端 Binder 引用;
  • 对 Server 来说,它导出的是由驱动记录和管理的 Binder node;
  • 对驱动来说,它要完成 transaction 路由、数据复制、引用传递、线程唤醒和死亡通知;
  • 对 Android 系统来说,它又连接着服务发现、权限校验和组件协作。

Binder 的复杂,不在于某一个类特别难,而在于同一个“Binder”在不同语境里指向了不同层次的东西。只要没有先分清当前讨论的是业务接口、AIDL Proxy、BinderProxy、handle 还是 node,很多解释听起来都对,放到一起却会互相矛盾。

这也是为什么我这次没有再从进程空间开始,而是选择跟着一次调用往下走。每到一层,只问三个问题:

  1. 这段代码运行在哪个进程、哪条线程?
  2. 当前拿到的是本地对象、远端引用,还是已经序列化的数据?
  3. 下一层依靠什么信息找到真正的目标?

这三个问题如果始终是清楚的,Binder 的调用链再长,也不至于彻底失去方向。

旧文章我会继续保留。它记录了我当时理解 Binder 的过程,也仍然适合作为一篇完整的入门材料。这篇文章则更像是多年后的一次补充:有些结论没有变,有些说法需要变得更准确,而有些当年没有意识到的问题,现在值得单独拿出来再讲一次。

如果想动手走一遍这套流程,也可以继续参考我以前写的 HelloBinder。脱离 AIDL 生成代码,自己实现一遍 Stub 和 Proxy,依然是理解 Binder 最直接的方法之一。

参考资料:

感谢你读到这里。

这里记录产品创造、工程实践,以及把想法一步步做成作品的过程。欢迎继续关注,也欢迎交流。

ByronNote 微信公众号二维码

原创内容,转载请注明出处。