AI 技术博客
Android全栈15 分钟阅读9466

【全栈第1课】Binder — Android 跨进程通信的命脉

Android 全栈工程师进阶教程 第01课。Binder — Android 跨进程通信的命脉。基于公开 AOSP 知识整理,含原理、可编译示例、常见问题定位。

> 本文是《Android 全栈工程师进阶教程》系列第 01 课。完整 26 课见 GitHub 仓库

【全栈第1课】Binder — Android 跨进程通信的命脉


0. 这节课你将亲手做出什么

一个真实的 AOSP Binder 服务:写一个 IHelloService 接口(有 sayHelloadd 两个方法),编译出 helloserver(服务端)和 helloclient(客户端)两个可执行文件。 跑起来后,helloclient 进程能跨进程调到 helloserver 进程里的方法,并且服务端能拿到调用方的 uid(内核盖的戳)。

学完你能回答:你天天写的 context.getSystemService(...)Binder.getCallingUid(),底层到底发生了什么?


1. 背景:为什么 Android 不用传统 IPC,要自己造个 Binder

Android 是多进程系统:你的 App 一个进程,系统服务(AMS/WMS/IMS)在 system_server 进程。它们要通信,就需要 IPC(进程间通信)。Linux 本来就有一堆 IPC,为什么 Google 还要造 Binder?

IPC 方式数据拷贝次数致命问题
管道 / 消息队列2 次(发送方→内核缓冲→接收方)
Socket2 次慢,主要为网络设计
共享内存0 次快,但谁都能读写,没有身份认证
Binder1 次(靠 mmap)快 + 内核给每次调用盖上 UID/PID 戳

Binder 的两个杀手锏:

  1. 一次拷贝:发送方的数据 copy_from_user 到内核空间,而接收进程通过 mmap 把这块内核缓冲区直接映射到自己的地址空间——接收方读的时候不用再拷一次。所以是 1 次,比管道少一半。
  2. 内核盖身份戳:每次跨进程调用,Binder 驱动会从内核的进程信息里取出真实的调用方 uid/pid,填进这次调用。调用方伪造不了——因为这是内核填的,不是应用层传的参数。

> 💡 这第 2 点就是你在 示例项目 里写 Binder.getCallingUid() 做权限校验为什么安全的根本原因:应用层骗不过内核。


2. 一次跨进程调用的全链路(先建立全局图)

以你最熟的 inputManager.getInputDevice() 为例:

【你的 App 进程】                       【system_server 进程】
inputManager.getInputDevice(id)
   │
   ▼ IInputManager.Stub.Proxy          ← ① AIDL 自动生成的"客户端代理"
   .getInputDevice(id)
   │  把参数 id 打包进 Parcel
   ▼  mRemote.transact(code, data, reply)
   │
   ▼ BinderProxy.transactNative() (JNI)
   │
   ╞═══════ Binder 驱动(内核)═══════▶  唤醒 system_server 的 binder 线程
   │         (盖上调用方 uid/pid)         │
   │                                     ▼ IInputManager.Stub.onTransact(code)  ← ② "服务端骨架"
   │                                     │  按 code 解包 Parcel,分发
   │                                     ▼ InputManagerService.getInputDevice() ← ③ 真正的实现
   │                                     │  结果打包进 reply
   ◀═══════ Binder 驱动 ═══════════════════
   ▼
拿到 reply,解包,返回 InputDevice

记住三个角色(整个 Binder 体系就这三个):

角色在哪一端干什么谁写的
Proxy(代理)客户端打包参数 + 发起 transactAIDL 工具自动生成
Stub(骨架)服务端onTransact 收包 + 解包 + 分发AIDL 工具自动生成
Service(实现)服务端真正的业务逻辑你写

这就是为什么用 AIDL:你只写接口和实现,打包/解包/transact 这些脏活全自动生成。


3. 在 某真实机型 真实源码里印证(不是教科书,是量产代码)

3.1 身份戳从哪来 —— getCallingUid 的真相

你调 Binder.getCallingUid() 时:

Java: Binder.getCallingUid()
  │ JNI
  ▼ android_os_Binder_getCallingUid()        frameworks/base/core/jni/android_util_Binder.cpp:1234
  │
  ▼ IPCThreadState::self()->getCallingUid()   返回 mCallingUid
                                              frameworks/native/libs/binder/IPCThreadState.cpp:445

mCallingUid 这个值,是 Binder 驱动在每次 transact 时从内核填进来的。还有个配套的 clearCallingIdentity()(IPCThreadState.cpp:547)—— 系统服务里"临时变回自己身份去做特权操作"就用它,做完再 restoreCallingIdentity()。这个模式你在系统服务源码里会反复见到。

3.2 服务怎么"上架" —— IMS 的注册

InputManagerService.Lifecycle.onStart()        InputManagerService.java:4312
  └─ publishBinderService(Context.INPUT_SERVICE, mService)
       └─ ServiceManager.addService("input", mService, ...)   SystemService.java:663

而 App 端 getSystemService(INPUT_SERVICE) 最终走到 ServiceManager.getService("input"),拿到的就是 IMS 的 BinderProxyaddService 上架 ↔ getService 取货——ServiceManager 就是系统服务的"黄页/114"。它自己也是个 Binder 服务,handle 固定为 0。


4. 动手:一步步做出来

步骤 1:定义接口 aidl/com/example/hello/IHelloService.aidl

package com.example.hello;

interface IHelloService {
    // @utf8InCpp 让生成的 C++ 用 std::string(否则是 String16,麻烦)
    @utf8InCpp String sayHello(@utf8InCpp String name);
    int add(int a, int b);
}

你只写这一个文件,aidl 工具会自动生成 4 个产物:

  • IHelloService.h — 接口
  • BnHelloService.h服务端骨架(Stub),自带 onTransact
  • BpHelloService.h客户端代理(Proxy),自带打包 transact
  • IHelloService.cpp — 上面的实现

步骤 2:写服务端 helloserver.cpp(逐段讲)

头文件和命名(注意第一行 include 的是生成的 Bn 头):

#include <binder/IPCThreadState.h>     // 取调用方 uid/pid
#include <binder/IServiceManager.h>    // defaultServiceManager / addService
#include <binder/ProcessState.h>       // binder 线程池
#include "com/example/hello/BnHelloService.h"   // ← aidl 自动生成的骨架
using com::example::hello::BnHelloService;

实现类:继承生成的 Bn(骨架),只写业务方法:

class HelloService : public BnHelloService {   // 继承 Stub
public:
    Status sayHello(const std::string& name, std::string* _aidl_return) override {
        // ★ 关键:这里取到的 uid 是内核盖的,客户端伪造不了
        const int uid = IPCThreadState::self()->getCallingUid();
        const int pid = IPCThreadState::self()->getCallingPid();
        ALOGI("sayHello called by uid=%d pid=%d, name=%s", uid, pid, name.c_str());
        *_aidl_return = "Hello, " + name + " (from uid=" + std::to_string(uid) + ")";
        return Status::ok();   // AIDL 方法用 Status 返回成功/失败
    }
    Status add(int32_t a, int32_t b, int32_t* _aidl_return) override {
        *_aidl_return = a + b;        // 返回值通过出参指针 _aidl_return 带回
        return Status::ok();
    }
};

> 注意:AIDL 生成的 C++ 方法,返回值不是函数返回值,而是最后一个出参 _aidl_return,函数本身返回 Status(表示这次调用成功还是失败)。这是 AIDL C++ 后端的固定约定。 > onTransact(解包+按 code 分发)由 BnHelloService 生成代码处理,你完全不用碰

main:创建实例 → 上架 → 开线程池阻塞等调用:

int main() {
    sp<HelloService> service = sp<HelloService>::make();   // sp<> 是 Android 的强引用智能指针

    // 上架到 servicemanager,名字 "hello"
    status_t ret = defaultServiceManager()->addService(String16("hello"), service);
    if (ret != OK) { LOG(ERROR) << "addService failed!"; return 1; }

    ProcessState::self()->startThreadPool();        // 开启 binder 线程池
    IPCThreadState::self()->joinThreadPool();        // 阻塞,把当前线程也加入,等 transact 进来
    return 0;
}

> 为什么要 joinThreadPool 阻塞? 因为服务端是被动的——没人调用时它就睡着(epoll 阻塞在 binder fd 上,0 CPU),有 transact 进来才被驱动唤醒。不阻塞的话 main 一返回进程就退了,服务就没了。

步骤 3:客户端 helloclient.cpp 关键逻辑

sp<IServiceManager> sm = defaultServiceManager();
sp<IBinder> binder = sm->getService(String16("hello"));   // 取货:拿到 BinderProxy
sp<IHelloService> svc = interface_cast<IHelloService>(binder);  // 转成接口代理(Bp)
std::string reply;
svc->sayHello("zhoubenliang", &reply);   // 看着像本地调用,实际跨进程了
ALOGI("got: %s", reply.c_str());

步骤 4:Android.bp 编译脚本

aidl_interface {
    name: "com.example.hello",
    unstable: true,                 // 学习用,不冻结版本
    srcs: ["aidl/com/example/hello/IHelloService.aidl"],
    backend: { cpp: { enabled: true }, java: { enabled: true } },  // 生成 C++ + Java 后端
}
cc_binary {
    name: "helloserver",
    srcs: ["helloserver.cpp"],
    shared_libs: ["libbinder", "libutils", "liblog", "libbase"],
    static_libs: ["com.example.hello-cpp"],   // 链接 aidl 生成的 C++ 库
}
// helloclient 同理

5. 看一眼"自动生成"到底生成了什么(理解 transact 的关键)

用 aidl 工具生成后,BpHelloService::sayHello(客户端代理)真实长这样:

::android::binder::Status BpHelloService::sayHello(const std::string& name, std::string* _aidl_return) {
  ::android::Parcel _aidl_data, _aidl_reply;
  _aidl_data.writeInterfaceToken(getInterfaceDescriptor());   // 写接口标识,防串台
  _aidl_data.writeUtf8AsUtf16(name);                          // ① 打包参数 name
  remote()->transact(BnHelloService::TRANSACTION_sayHello,    // ② 发起 transact,过 binder 驱动!
                     _aidl_data, &_aidl_reply, 0);
  _aidl_status.readFromParcel(_aidl_reply);                   // ③ 解包返回值
  ...
}

这就是第 2 节链路图里"Proxy 打包→transact"的真实代码。 你写一行 .aidl,这一坨样板全自动生成。这就是 AIDL 的价值。


6. 编译 & 运行(看到效果)

# AOSP 根目录 source/lunch 之后,进 lesson 目录:
mm                                   # 编译,产出 helloserver / helloclient
adb push out/.../helloserver  /data/local/tmp/
adb push out/.../helloclient  /data/local/tmp/
adb shell /data/local/tmp/helloserver &       # 后台起服务端
adb shell /data/local/tmp/helloclient zhoubenliang   # 客户端调用
adb logcat | grep -i hello           # 看完整链路日志

预期 logcat(亲眼看到 Binder 生效 + 身份戳):

helloclient: >>> 发起 sayHello("zhoubenliang") 跨进程调用
helloserver: sayHello called by uid=2000 pid=12345    ← 服务端收到,uid 是客户端的(shell=2000)
helloclient: <<< 收到返回: Hello, zhoubenliang (from uid=2000)

那个 uid=2000 就是内核盖的戳——服务端没法被客户端骗,这一刻你就理解了权限校验的根基。


7. 踩坑提醒

  1. AIDL C++ 返回值是出参,不是函数返回值,函数返回 Status。新手常写成 return result; → 编不过。
  2. 服务端必须 joinThreadPool 阻塞,否则 main 返回进程退出,服务消失。
  3. @VintfStability 不要乱加(那是 HAL 用的,见 L9),这里 unstable: true 即可。
  4. 这一课的 helloserver 是独立 cc_binary —— 能编进镜像,但不会自动跑,得手动 adb shell helloserver &。这叫"游离模块"。真正集成进系统、开机自动跑的做法见 L2(那才是 IMS/AMS 的路子)。

9. 常见问题分析与定位(Binder 实战)

Binder 是 Android 万物之基,线上问题极多。下面是真实高频的几类,以及怎么查。

9.1 常见问题清单

  1. getService("xxx") 返回 null —— 拿不到服务
  2. 调用抛 DeadObjectException / transact failed —— 对端进程挂了
  3. TransactionTooLargeException —— 一次传太多数据
  4. App 卡死 / ANR,trace 显示卡在 BinderProxy.transact —— 同步 binder 调用对端处理慢
  5. 权限校验失效 / getCallingUid 拿到的 uid 不对

9.2 每个问题的分析与定位

问题1:getService 返回 null

  • 现象:interface_cast 后调用崩溃,或 getService 返回空。
  • 可能原因(高→低):① 服务还没起(addService 没执行) ② 服务名拼错 ③ SELinux 没放行(见 L6) ④ 服务进程崩了
  • 定位步骤:
    adb shell service list | grep hello     # 服务在不在黄页里?不在=没 addService 成功
    adb shell dumpsys -l | grep hello        # 同上,另一种列法
    adb logcat | grep -iE "avc.*hello|SELinux"   # 有没有 avc denied(SELinux 拦了)
    adb shell ps -A | grep helloserver       # 服务进程还活着吗
    
  • 根因→修复:不在 service list → 查 addService 返回值/服务进程是否起;avc denied → 补 sepolicy(L6/L11)。

问题2:DeadObjectException

  • 现象:之前能调,某次调用突然抛 DeadObjectException
  • 原因:服务端进程死了(crash/被 lmkd 杀)。你手里的 BinderProxy 成了"野指针"。
  • 定位:
    adb logcat | grep -iE "died|tombstone|lmkd.*kill"   # 服务进程怎么没的
    adb shell ls /data/tombstones/                       # native crash 现场
    
  • 修复:客户端要注册死亡通知(linkToDeath),对端死了能感知并重连;别缓存 BinderProxy 当永久有效。

问题3:TransactionTooLargeException

  • 现象:传大 Bundle/大 List/大 bitmap 时抛异常。
  • 原因:Binder 一次事务的内核缓冲区有上限(每进程约 1MB,且多个并发调用共享)。
  • 定位:看抛异常的调用传了多大数据;dumpsys meminfo 看 binder 缓冲。
  • 修复:大数据别走 binder 参数,改用共享内存(ashmem/Bitmap.parcel via ashmem)、文件描述符、分页传输

问题4:卡在 transact 的 ANR

  • 现象:ANR trace 主线程栈顶是 android.os.BinderProxy.transactNative
  • 原因:同步 binder 调用,对端服务处理太慢(或对端也在等锁),把你主线程卡住。
  • 定位:
    adb shell dumpsys activity        # 看 ANR
    # 抓 trace,看主线程卡在哪个 transact,再看对端服务那个方法为啥慢
    adb pull /data/anr/traces.txt
    
  • 修复:① 别在主线程做同步 binder 调用 ② 用 oneway(异步,不等返回) ③ 优化对端实现。

问题5:getCallingUid 拿到的 uid 不对

  • 现象:权限校验时 uid 不是预期的调用方,而是自己(system)。
  • 原因:在调用 getCallingUid 之前,代码里调过 clearCallingIdentity()(把身份临时清成自己了)却没及时取回,或不在 binder 调用线程上取。
  • 定位:检查调用链里有没有 clearCallingIdentity;getCallingUid 必须在 binder 线程、transact 上下文里取才准。
  • 修复:clearCallingIdentity 后务必配对 restoreCallingIdentity(token);权限判断在进入服务方法的第一时间取 uid。

9.3 定位决策树

Binder 调用出问题
├─ 拿不到服务(null) → service list 有没有 → 没有=addService/进程问题;有=SELinux(L6)
├─ DeadObjectException → logcat died/tombstone/lmkd → 对端进程死了 → linkToDeath 重连
├─ TooLarge → 传了多大 → >~1MB → 改共享内存/fd
├─ ANR 卡 transact → traces.txt 主线程 → 对端慢 → oneway/异步/优化对端
└─ uid 不对 → 查 clearCallingIdentity 配对 → 第一时间取 uid

10. 小结(一句话记住)

  1. Binder = 一次拷贝 + 内核盖 UID/PID 戳 —— 快,且权限校验有根。
  2. 三角色:Proxy(客户端打包)→ Binder 驱动(内核转发+盖戳)→ Stub(服务端解包分发)→ Service(你的实现)。
  3. ServiceManager 是黄页:addService 上架,getService 取货。
  4. AIDL 把跨进程调用自动化:写一行接口,Parcel 打包/transact 全自动生成。
  5. 排查口诀:拿不到查 service list+SELinux;挂了 linkToDeath;太大走共享内存;卡了用 oneway。

下节预告

L2:getSystemService 全链路 + 写一个真集成进 system_server 的 Java 系统服务 —— 把这一课的 native 思维升级到 framework Java 层,让服务开机自动起、App 能 getSystemService 拿到

评论