[{"category":"系统原理","content":" 本文源码基于 Android 10（API 29）。\n背景 Android 里几乎所有“跨进程”的事情都离不开 Binder：启动 Activity、绑定 Service、读写 ContentProvider、获取系统服务……我们每天调用的 getSystemService()，背后都是一次 Binder 通信。\n先看几个面试里常被问到的问题：\nLinux 已经有管道、Socket、共享内存，Android 为什么还要自己造一个 Binder？ 都说 Binder 只拷贝一次，这一次拷贝是怎么做到的？ AIDL 生成的那一大段代码，到底做了什么？ 为什么用 Intent 传一张大图会崩溃？ 这篇文章从整体结构讲起，再顺着一次 AIDL 调用把 Java 层、Native 层和驱动层串起来。\n整体结构 Binder 通信有四个角色：\n角色 所在位置 职责 Client 用户空间，调用方进程 通过代理对象发起调用 Server 用户空间，服务方进程 在 Binder 线程池里执行真正的方法 ServiceManager 用户空间，独立进程 服务的“通讯录”，按名字注册和查找服务 Binder 驱动 内核空间 在进程之间传递数据、管理引用和线程 Client 和 Server 不能直接访问对方的内存，所有数据都要经过内核里的 Binder 驱动中转。ServiceManager 本身也是一个 Binder 服务，它的句柄固定为 0，所以任何进程都能直接找到它。\n原理 1. 为什么是 Binder Linux 自带的 IPC 方式各有短板，放到手机系统上并不合适：\n方式 数据拷贝次数 能否可靠识别调用方 使用方式 共享内存 0 否 需要自己处理同步，容易出错 Binder 1 是，驱动填入 UID / PID C/S 架构，像调用本地方法 Socket 2 否，靠自己约定 C/S 架构，但开销大，适合网络 管道、消息队列 2 否 适合简单数据流 Binder 在性能和易用性之间取得了平衡，更关键的是安全：调用方的 UID 和 PID 由内核驱动写入，应用无法伪造。服务端调用 Binder.getCallingUid() 就能拿到真实身份，系统的权限检查正是建立在这一点上。\n2. 一次拷贝是怎么做到的 传统 IPC 需要两次拷贝：发送方把数据从用户空间拷到内核（copy_from_user），接收方再从内核拷回自己的用户空间（copy_to_user）。\nBinder 省掉了第二次。关键是 mmap：每个使用 Binder 的进程启动时，都会在 ProcessState 里打开 /dev/binder 并映射一块缓冲区：\n#define BINDER_VM_SIZE ((1 * 1024 * 1024) - sysconf(_SC_PAGE_SIZE) * 2) ProcessState::ProcessState(const char *driver) : mDriverFD(open_driver(driver)) // ...省略 { if (mDriverFD \u0026gt;= 0) { // 把内核中的一块缓冲区映射到本进程的用户空间 mVMStart = mmap(nullptr, BINDER_VM_SIZE, PROT_READ, MAP_PRIVATE | MAP_NORESERVE, mDriverFD, 0); // ...省略 } } binder_mmap 让这块缓冲区的物理内存同时映射到内核空间和接收方的用户空间。发送数据时，驱动在接收方的缓冲区里分配空间，然后只做一次 copy_from_user，把发送方的数据直接拷进去。因为这块内存接收方本来就能访问，自然不需要第二次拷贝。\n对照上图：左边的传统 IPC 中，内核缓冲区和接收方的内存是两块不同的物理内存，数据要再拷贝一次才能到达接收方；右边的 Binder 中，内核缓冲区和接收方的映射区是同一组物理页的两个“入口”，数据写进内核缓冲区的那一刻，接收方就已经能读到了。\n这里也能看到一个重要的数字：缓冲区大小是 1MB 减去两个页，大约 1016KB。后面踩坑部分会用到。\n3. AIDL：Stub 和 Proxy 以一个简单的接口为例：\ninterface IBookManager { List\u0026lt;String\u0026gt; getBooks(); void addBook(String name); } 编译后会生成 IBookManager.java，里面有两个关键的内部类：Stub 和 Proxy。客户端和服务端都会把这个文件编译进去，但运行时各用各的：\n类 运行在哪个进程 角色 Stub 服务端 服务端继承它并实现 getBooks()，它是真正的 Binder 实体 Proxy 客户端 客户端通过 asInterface() 拿到它，它是服务端 Stub 在客户端的代理 两者代表的都是服务端提供的同一个接口：Proxy 在客户端把调用“打包”发出去，Stub 在服务端“拆包”并执行。\nStub：运行在服务端进程（已简化）\npublic static abstract class Stub extends Binder implements IBookManager { private static final String DESCRIPTOR = \u0026#34;com.example.binderdemo.IBookManager\u0026#34;; static final int TRANSACTION_getBooks = IBinder.FIRST_CALL_TRANSACTION + 0; public static IBookManager asInterface(IBinder obj) { if (obj == null) return null; IInterface iin = obj.queryLocalInterface(DESCRIPTOR); if (iin instanceof IBookManager) { return (IBookManager) iin; // 同一进程：直接返回 Stub 本身 } return new Stub.Proxy(obj); // 跨进程：返回代理对象 } @Override public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException { if (code == TRANSACTION_getBooks) { // 服务端：在 Binder 线程中执行 data.enforceInterface(DESCRIPTOR); List\u0026lt;String\u0026gt; result = this.getBooks(); // 调用服务端自己实现的 getBooks() reply.writeNoException(); reply.writeStringList(result); return true; } return super.onTransact(code, data, reply, flags); } } Proxy：运行在客户端进程\nprivate static class Proxy implements IBookManager { private IBinder mRemote; Proxy(IBinder remote) { mRemote = remote; } @Override public List\u0026lt;String\u0026gt; getBooks() throws RemoteException { // 客户端：在调用方线程中执行（比如主线程） Parcel _data = Parcel.obtain(); Parcel _reply = Parcel.obtain(); try { _data.writeInterfaceToken(DESCRIPTOR); // 发起跨进程调用，当前线程会阻塞到服务端返回 mRemote.transact(Stub.TRANSACTION_getBooks, _data, _reply, 0); _reply.readException(); return _reply.createStringArrayList(); } finally { _reply.recycle(); _data.recycle(); } } } 客户端调用一次 bookManager.getBooks()，代码会在两个进程之间这样走：\n顺序 进程 线程 执行的代码 1 客户端 调用方线程 bookManager.getBooks()，实际执行的是 Proxy.getBooks() 2 客户端 调用方线程 把方法编号和参数写入 _data，调用 mRemote.transact() 后阻塞等待 3 服务端 Binder 线程 Stub.onTransact() 收到请求，按方法编号找到 getBooks 4 服务端 Binder 线程 执行服务端实现的 getBooks()，把结果写入 reply 5 客户端 调用方线程 被唤醒，从 _reply 读出结果并返回 也就是说，Proxy.getBooks() 是客户端在调用，Stub.onTransact() 是服务端在执行，中间隔着一次跨进程通信。AIDL 只是帮我们自动生成了这段打包和拆包的样板代码，手写也完全可以。\n注意 asInterface 的判断：Client 和 Server 在同一进程时，拿到的就是 Stub 对象本身，根本不会走 Binder 驱动。\n4. ServiceManager：Proxy 手里的 IBinder 从哪来 构造 Proxy 时要传入服务端的 IBinder。可客户端一开始和服务端毫无联系，这个 IBinder 是怎么拿到的？\n整个过程分两段：\n先找到 AMS（①②）。 要和别的进程通信，得先有对方的句柄，可句柄本身又要靠 Binder 传递。Android 的解法是约定 0 号句柄永远指向 ServiceManager：AMS 等系统服务开机时按名字登记在它这里，任何进程都能通过 0 号句柄查到 AMS。\n再由 AMS 转交 Stub（③～⑦）。 客户端调用 bindService()，AMS 回调服务端的 onBind() 拿到 Stub，再通过 onServiceConnected() 交给客户端。经过驱动转换，客户端拿到的已经是 Stub 的代理，用 asInterface() 包装成 Proxy 后就能直接调用了。\n一句话：ServiceManager 是 Binder 的入口，负责让大家找到 AMS 这类系统服务。我们自己的 AIDL 服务不在它那里登记，拿到 Proxy 之后的调用它也不参与。\n5. 一次调用的完整链路 mRemote.transact() 之后发生了什么？以 Android 10 为例，从 Java 层一路到驱动，再回到服务端：\n步骤 所在位置 关键调用 1 Client · Java BinderProxy.transact() → transactNative() 2 Client · Native BpBinder::transact() → IPCThreadState::transact() 3 Client · Native talkWithDriver() → ioctl(fd, BINDER_WRITE_READ, ...) 4 内核 · 驱动 binder_transaction()：在 Server 缓冲区分配空间，copy_from_user 拷贝数据，唤醒 Server 的 Binder 线程 5 Server · Native Binder 线程从 ioctl 返回，executeCommand(BR_TRANSACTION) 6 Server · Native BBinder::transact() → JavaBBinder::onTransact() 7 Server · Java Binder.execTransact() → Stub.onTransact() → getBooks() 8 返回 结果写入 reply，按原路经驱动送回 Client，Client 线程被唤醒 整个过程中，Client 的调用线程在第 3 步之后一直阻塞，直到第 8 步拿到结果，这就是普通 Binder 调用是同步的原因。如果方法声明为 oneway，驱动把数据交给 Server 后就立即返回，Client 不等待结果。\n6. Binder 线程池 第 5 步里 Server 用来执行方法的并不是主线程，而是 Binder 线程池中的线程。每个进程的 Binder 线程默认最多 15 个，再加上主动加入的主 Binder 线程，一共 16 个：\n#define DEFAULT_MAX_BINDER_THREADS 15 这带来两个直接的结论：\n服务端的方法可能被多个线程同时调用，必须考虑线程安全 如果服务端方法里做了耗时操作，又被大量并发调用，Binder 线程可能被占满，后续请求只能排队 示例：用 AIDL 实现一个跨进程服务 服务端运行在独立进程，在 Manifest 里声明：\n\u0026lt;service android:name=\u0026#34;.BookService\u0026#34; android:process=\u0026#34;:remote\u0026#34; /\u0026gt; 服务端实现 Stub：\nclass BookService : Service() { // 方法运行在 Binder 线程池，使用线程安全的集合 private val books = CopyOnWriteArrayList\u0026lt;String\u0026gt;() private val binder = object : IBookManager.Stub() { override fun getBooks(): List\u0026lt;String\u0026gt; = books override fun addBook(name: String) { books.add(name) } } override fun onBind(intent: Intent): IBinder = binder } 客户端绑定服务，并监听服务端进程死亡：\nclass MainActivity : AppCompatActivity() { private var bookManager: IBookManager? = null private val deathRecipient = object : IBinder.DeathRecipient { override fun binderDied() { // 运行在 Binder 线程，服务端进程已经不在了 bookManager?.asBinder()?.unlinkToDeath(this, 0) bookManager = null bindBookService() // 重新绑定 } } private val connection = object : ServiceConnection { override fun onServiceConnected(name: ComponentName, service: IBinder) { bookManager = IBookManager.Stub.asInterface(service) service.linkToDeath(deathRecipient, 0) } override fun onServiceDisconnected(name: ComponentName) { bookManager = null } } private fun bindBookService() { val intent = Intent(this, BookService::class.java) bindService(intent, connection, Context.BIND_AUTO_CREATE) } } asInterface 在这里返回的是 Proxy，因为 BookService 跑在 :remote 进程。把 Manifest 里的 android:process 去掉再运行，就会拿到 Stub 本身。\n踩坑记录 1. TransactionTooLargeException 前面提到，每个进程的 Binder 缓冲区只有约 1MB，而且是这个进程里所有正在进行的 Binder 调用共用的。oneway 异步调用能用的空间还要再减半。\n最常见的触发场景：\n用 Intent 的 extras 传一张大 Bitmap 或一个很大的列表 onSaveInstanceState 里保存了大量数据。从 Android 7.0 开始，targetSdk 不低于 24 时，这种情况会直接抛出异常，而不只是打一条日志 解决思路是不要通过 Binder 传大数据：\n场景 推荐做法 页面之间传图片 传文件路径或 Uri，由接收方自己加载 页面之间传大列表 传 ID，接收方从数据库或内存缓存中读取 跨进程传大块数据 使用 SharedMemory（API 27+）或 ParcelFileDescriptor 保存页面状态 onSaveInstanceState 只存关键 ID，数据放到 ViewModel 里 2. 在主线程调用远程方法导致 ANR 普通的 Binder 调用是同步的。如果服务端方法执行要 3 秒，客户端在主线程调用它，主线程也会跟着阻塞 3 秒。更麻烦的是，服务端慢可能是因为它的 Binder 线程被占满了，这种问题在客户端很难排查。\n所以对待 AIDL 方法要像对待网络请求一样：放到子线程调用，或者在不需要返回值时声明为 oneway。\n3. 服务端进程被杀后的 DeadObjectException 服务端进程崩溃或被系统回收后，客户端再调用 Proxy 的方法，会抛出 DeadObjectException。示例里的 linkToDeath 就是为此准备的：进程死亡时驱动会通知客户端，我们在 binderDied() 里清理引用并重新绑定。\n需要注意 binderDied() 运行在 Binder 线程，不是主线程，不能直接更新 UI。\n总结 问题 答案 为什么用 Binder？ 一次拷贝，性能好；驱动写入调用方 UID / PID，安全；C/S 架构，易用 一次拷贝怎么实现？ 接收方 mmap 了内核缓冲区，驱动只需 copy_from_user 一次 ServiceManager 在 AIDL 中做什么？ 0 号句柄，Binder 的入口：客户端通过它找到 AMS，再由 AMS 转交服务端的 Stub AIDL 做了什么？ 生成两个类：Proxy 在客户端打包参数发起调用，Stub 在服务端拆包并执行；同进程时 asInterface 直接返回 Stub 服务端方法在哪个线程执行？ Binder 线程池，默认最多 15 个加 1 个主 Binder 线程 为什么传大图会崩？ 进程共用约 1MB 的 Binder 缓冲区，超出就抛 TransactionTooLargeException Binder 是理解 Android Framework 的另一把钥匙。有了这篇的基础，再去看 startActivity 如何通过 Binder 调到 AMS，以及 AMS 又如何回调到应用进程，整条链路就清楚多了。\n","date":"2020-06-14","permalink":"https://liangruixi.dev/posts/binder-ipc-principle/","summary":"从 mmap 一次拷贝、ServiceManager 到 AIDL 生成的 Stub 和 Proxy，基于 Android 10 源码梳理一次 Binder 调用的完整链路，以及 TransactionTooLargeException 等几个常见的坑。","title":"Binder 跨进程通信原理"},{"category":"系统原理","content":" 本文源码基于 Android 9（API 28）。\n背景 Handler 几乎是每个 Android 开发者最早接触的“线程通信”工具：子线程拿到数据，handler.post {} 一下就能回到主线程更新 UI。\n但用得多不代表理解得深。下面这些问题，你能不能都答上来？\n一个线程可以有几个 Looper？几个 Handler？ Handler 为什么能把任务从子线程切换到主线程？ Looper.loop() 是死循环，为什么主线程没有卡死？ postDelayed 的延迟是怎么实现的？准吗？ 为什么说 Handler 容易导致内存泄漏？ 这篇文章顺着源码把消息的“发送、入队、取出、分发”走一遍，答案自然就有了。\n整体结构 Handler 机制由四个角色组成：\n角色 职责 Message 消息本身，携带 what、obj、when，以及处理它的 target（Handler） MessageQueue 按执行时间 when 排序的单链表，负责入队和取出 Looper 每个线程最多一个，不断从 MessageQueue 取消息并分发 Handler 消息的发送者，也是处理者 一句话概括：Handler 把 Message 放进 MessageQueue，Looper 从 MessageQueue 取出 Message，再交回给 Handler 处理。 发送和处理可以在不同线程，但处理一定发生在 Looper 所在的线程。\n原理 1. Looper：每个线程最多一个 Looper.prepare() 用 ThreadLocal 保证每个线程只有一个 Looper：\nprivate static void prepare(boolean quitAllowed) { if (sThreadLocal.get() != null) { throw new RuntimeException(\u0026#34;Only one Looper may be created per thread\u0026#34;); } sThreadLocal.set(new Looper(quitAllowed)); } private Looper(boolean quitAllowed) { mQueue = new MessageQueue(quitAllowed); mThread = Thread.currentThread(); } 所以第一个问题的答案是：一个线程最多一个 Looper、一个 MessageQueue，但可以有任意多个 Handler，它们共用同一个队列。\n主线程不需要我们手动 prepare，ActivityThread.main() 已经做了：\npublic static void main(String[] args) { // ...省略 Looper.prepareMainLooper(); // ...省略 Looper.loop(); throw new RuntimeException(\u0026#34;Main thread loop unexpectedly exited\u0026#34;); } loop() 后面紧跟着一个异常，说明主线程的 Looper 正常情况下永远不会退出。\n2. 发送：所有 send 和 post 最终都走到 enqueueMessage 无论是 sendMessage、sendMessageDelayed 还是 post(Runnable)，最终都会调用：\npublic boolean sendMessageAtTime(Message msg, long uptimeMillis) { MessageQueue queue = mQueue; // ...省略判空 return enqueueMessage(queue, msg, uptimeMillis); } private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) { msg.target = this; if (mAsynchronous) { msg.setAsynchronous(true); } return queue.enqueueMessage(msg, uptimeMillis); } 两个细节：\nmsg.target = this：消息记住了是谁发的，分发时就交还给它。 延迟消息的时间是 SystemClock.uptimeMillis() + delayMillis。uptimeMillis 不包含设备深度睡眠的时间，这一点后面踩坑部分会提到。 3. 入队：按时间插入有序链表 boolean enqueueMessage(Message msg, long when) { synchronized (this) { // ...省略退出检查 msg.markInUse(); msg.when = when; Message p = mMessages; boolean needWake; if (p == null || when == 0 || when \u0026lt; p.when) { // 插到队头，如果 Looper 正在阻塞就唤醒它 msg.next = p; mMessages = msg; needWake = mBlocked; } else { // 找到第一个比它晚的位置插进去 needWake = mBlocked \u0026amp;\u0026amp; p.target == null \u0026amp;\u0026amp; msg.isAsynchronous(); Message prev; for (;;) { prev = p; p = p.next; if (p == null || when \u0026lt; p.when) break; if (needWake \u0026amp;\u0026amp; p.isAsynchronous()) needWake = false; } msg.next = p; prev.next = msg; } if (needWake) { nativeWake(mPtr); } } return true; } MessageQueue 并不是真正的“队列”，而是一个按 when 从小到大排好序的单链表。新消息如果比队头还早，就插到最前面，并在 Looper 阻塞时调用 nativeWake 唤醒它。\n4. 取出：没有消息就阻塞在 epoll Looper.loop() 的核心循环很简单：\nfor (;;) { Message msg = queue.next(); // might block if (msg == null) { // 返回 null 说明队列已经退出 return; } // ...省略日志和耗时统计 msg.target.dispatchMessage(msg); // ...省略 msg.recycleUnchecked(); } 关键在 queue.next()：\nMessage next() { int nextPollTimeoutMillis = 0; for (;;) { nativePollOnce(ptr, nextPollTimeoutMillis); synchronized (this) { final long now = SystemClock.uptimeMillis(); Message prevMsg = null; Message msg = mMessages; if (msg != null \u0026amp;\u0026amp; msg.target == null) { // 遇到同步屏障，只找异步消息 do { prevMsg = msg; msg = msg.next; } while (msg != null \u0026amp;\u0026amp; !msg.isAsynchronous()); } if (msg != null) { if (now \u0026lt; msg.when) { // 还没到时间，算出需要等多久 nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE); } else { mBlocked = false; // ...从链表摘下 msg return msg; } } else { nextPollTimeoutMillis = -1; // 没有消息，无限等待 } // ...省略：执行 IdleHandler } } } nativePollOnce 最终调用到 Native 层 Looper 的 epoll_wait：\nnextPollTimeoutMillis = -1：一直阻塞，直到 nativeWake 往 eventfd 写入数据把它唤醒 nextPollTimeoutMillis \u0026gt; 0：最多阻塞这么久，到点自动醒来，这就是 postDelayed 的实现方式 这也回答了第二个问题：loop() 虽然是死循环，但没有消息时线程阻塞在 epoll 上，会让出 CPU，不会空转。 ANR 的原因从来不是 loop() 本身，而是某条消息处理得太久，导致后面的输入事件、绘制消息来不及处理。\n5. 分发：三级优先级 public void dispatchMessage(Message msg) { if (msg.callback != null) { handleCallback(msg); // 1. post(Runnable) 传入的 Runnable } else { if (mCallback != null) { if (mCallback.handleMessage(msg)) { return; // 2. 构造 Handler 时传入的 Callback } } handleMessage(msg); // 3. 子类重写的 handleMessage } } post(Runnable) 其实是把 Runnable 包装成 Message.callback，所以它的优先级最高，也不会走到 handleMessage。\n6. 为什么 Handler 能切换线程 很多人以为 handler.post {} 是把代码“搬”到了另一个线程，其实不是。线程切换靠的是两点：同一进程内的线程共享内存，以及消息由 Looper 所在的线程自己取出来执行。\n先看 Handler 的构造函数：\npublic Handler(Callback callback, boolean async) { // ...省略泄漏检查 mLooper = Looper.myLooper(); if (mLooper == null) { throw new RuntimeException( \u0026#34;Can\u0026#39;t create handler inside thread \u0026#34; + Thread.currentThread() + \u0026#34; that has not called Looper.prepare()\u0026#34;); } mQueue = mLooper.mQueue; mCallback = callback; mAsynchronous = async; } Handler 创建时就绑定了一个 Looper，并拿到了它的 MessageQueue。传入 Looper 的构造函数（如 Handler(Looper.getMainLooper())）也是同样的逻辑，只是 Looper 由我们指定。\n以子线程通过主线程 Handler 发消息为例，整个过程是这样的：\n步骤 执行线程 发生了什么 1. Handler(Looper.getMainLooper()) 任意线程 Handler 记住主线程的 Looper 和 MessageQueue 2. handler.sendMessage(msg) 子线程 把 msg 插入主线程的 MessageQueue，必要时 nativeWake 唤醒主线程 3. queue.next() 返回 msg 主线程 主线程从 epoll 中醒来，取出这条消息 4. msg.target.dispatchMessage(msg) 主线程 handleMessage 或 Runnable 在主线程执行 子线程做的事只有第 2 步：往一个共享的队列里放一条消息。enqueueMessage 内部用 synchronized 加锁，保证多个线程同时发送也是安全的。真正执行代码的是第 4 步，由主线程的 Looper.loop() 完成。\n所以结论是：消息在哪个线程处理，取决于 Handler 绑定的 Looper，与从哪个线程发送无关。 这也解释了两个现象：\nHandler(Looper.getMainLooper()) 不管在哪个线程创建，消息都在主线程处理 无参的 Handler() 绑定的是“创建它的线程”的 Looper，在没有 Looper 的子线程里创建就会抛出上面的异常 7. 两个容易被忽略的机制 同步屏障。 在 next() 里看到的 msg.target == null，就是同步屏障。它通过隐藏 API MessageQueue.postSyncBarrier() 插入，之后队列只放行异步消息，普通消息全部被挡住。ViewRootImpl.scheduleTraversals() 就是用它保证 VSYNC 到来时的绘制消息优先执行，不被业务消息插队。\nIdleHandler。 队列暂时没有要处理的消息时，会回调注册的 IdleHandler，适合做一些不着急的初始化：\nLooper.myQueue().addIdleHandler { preloadSomething() false // 返回 false 表示只执行一次，执行后移除 } 示例：在子线程里使用 Handler 子线程默认没有 Looper，直接 Handler() 会抛出 Can't create handler inside thread ... that has not called Looper.prepare()。推荐用系统提供的 HandlerThread，它已经帮我们准备好了 Looper：\nclass ImageLoader { private val workerThread = HandlerThread(\u0026#34;image-loader\u0026#34;).apply { start() } private val workerHandler = Handler(workerThread.looper) private val mainHandler = Handler(Looper.getMainLooper()) fun load(path: String, callback: (Bitmap?) -\u0026gt; Unit) { workerHandler.post { val bitmap = BitmapFactory.decodeFile(path) // 在子线程解码 mainHandler.post { callback(bitmap) } // 回到主线程回调 } } fun release() { workerHandler.removeCallbacksAndMessages(null) workerThread.quitSafely() } } 注意 release() 里用的是 quitSafely()：它会把已经到期的消息处理完再退出；quit() 则直接清空队列。\n踩坑记录 1. 页面关了，请求回来还在更新 UI 这是我早期踩过的一个坑。详情页一进入就在子线程发起网络请求，结果通过 Handler 发回主线程，再弹出一个提示对话框。测试时发现，如果请求还没返回就按返回键关掉页面，过几秒应用会直接崩溃：\nandroid.view.WindowManager$BadTokenException: Unable to add window -- token android.os.BinderProxy@xxxx is not valid; is your activity running? 当时的代码大致是这样：\nclass DetailActivity : AppCompatActivity() { private val handler = object : Handler() { override fun handleMessage(msg: Message) { AlertDialog.Builder(this@DetailActivity) .setMessage(msg.obj as String) .show() } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Thread { val result = api.loadDetail() // 耗时几秒的网络请求 handler.obtainMessage(MSG_RESULT, result).sendToTarget() }.start() } } 排查下来，是两个问题叠在了一起：\n内存泄漏：匿名内部类 Handler 持有 Activity，请求线程又持有 Handler。页面关闭后，Activity 依然被引用，无法回收。消息入队后，引用链变成 主线程 Looper → MessageQueue → Message → Handler → Activity。 对已销毁的页面更新 UI：请求回来后，Handler 照常执行 handleMessage，在一个已经 finish 的 Activity 上弹对话框。这时窗口令牌已经失效，于是抛出 BadTokenException。 泄漏本身不一定马上出问题，但它让“已经关闭的页面”继续收到回调，崩溃才是最终的表现。修复要从三个方面一起做：\nclass DetailActivity : AppCompatActivity() { private val handler = UiHandler(this) private var call: Call? = null // OkHttp 请求 // Kotlin 的嵌套类默认是静态的，不持有外部类引用 private class UiHandler(activity: DetailActivity) : Handler(Looper.getMainLooper()) { private val ref = WeakReference(activity) override fun handleMessage(msg: Message) { val activity = ref.get() ?: return // 页面正在关闭或已销毁，不再更新 UI if (activity.isFinishing || activity.isDestroyed) return activity.showResult(msg.obj as String) } } override fun onDestroy() { call?.cancel() // 1. 取消还没返回的请求 handler.removeCallbacksAndMessages(null) // 2. 清空队列里的消息 super.onDestroy() } } 措施 解决的问题 静态 Handler + WeakReference 打断引用链，Activity 可以被回收 onDestroy 里取消请求、清空消息 页面关闭后不再有回调进来 更新 UI 前检查 isFinishing / isDestroyed 万一回调还是来了，也不会操作失效的窗口 从那以后，我写任何“异步回调更新 UI”的代码，都会先问自己一句：页面关掉之后，这个回调还会不会来？\n2. 同一个 Message 不能发两次 Message 在入队时会被 markInUse()，再次发送会抛出 This message is already in use。需要重复发送时，每次都用 Message.obtain() 或 handler.obtainMessage() 拿新对象。obtain() 会从一个最多 50 个的对象池里复用，比直接 new Message() 更省内存。\n3. postDelayed 不是精确定时器 延迟基于 uptimeMillis，设备进入深度睡眠后时间会“暂停”；而且到点后还要排队等前面的消息处理完。所以 postDelayed 适合 UI 层的短延迟，不适合用来做“30 分钟后提醒”这类精确定时任务，这类需求应该交给 AlarmManager 或 JobScheduler。\n总结 问题 答案 一个线程几个 Looper？ 最多一个，由 ThreadLocal 保证；Handler 可以有多个 为什么能切换线程？ 发送线程只是往共享的 MessageQueue 里放消息，由 Looper 所在线程取出执行 loop() 死循环为什么不卡死？ 没消息时阻塞在 epoll_wait，让出 CPU postDelayed 怎么实现？ 消息按 when 排序，nativePollOnce 带超时等待 为什么会内存泄漏？ Message.target 持有 Handler，非静态 Handler 持有 Activity；页面关闭后回调更新 UI 还可能崩溃 绘制消息为什么能插队？ 同步屏障 + 异步消息 Handler 的代码量不大，但它是理解 Android 主线程模型的钥匙。下一步可以继续看 Choreographer 如何借助同步屏障调度每一帧，以及 Binder 线程收到的系统调用是怎么通过 ActivityThread.H 切回主线程的。\n","date":"2019-02-21","permalink":"https://liangruixi.dev/posts/handler-message-mechanism/","summary":"从 Looper、MessageQueue 到 dispatchMessage，基于 Android 9 源码梳理 Handler 消息机制的完整链路，以及同步屏障、IdleHandler 和几个常见的坑。","title":"Handler 消息机制源码解析"},{"category":"","content":" 梁瑞熙 Leung Android 开发工程师。在意体验，也在意代码。\ngithub.com/DuTou-x lrx15603078696@gmail.com liangruixi.dev/index.xml 写些什么 Kotlin 协程、Flow 与 K2 编译器\nJetpack 与 Compose Lifecycle、ViewModel、DataStore 与 Compose\n架构与测试 MVI、多模块、依赖注入与测试\n系统原理 绘制、事件分发、Handler、Binder、AMS、WMS\n工程化与性能 启动、卡顿、内存与构建优化\n系统适配 Android 10 到 16 的版本适配\n跨平台与混合开发 Flutter、H5、KMP 与 Compose Multiplatform\n通信与 IoT 网络、WebSocket、WebRTC 与设备通信\nNative 与端侧 AI JNI、NDK 与端侧模型\nAbout Android engineer. I care about the experience, and about the code.\n","date":"0001-01-01","permalink":"https://liangruixi.dev/about/","summary":"梁瑞熙，高级 Android 开发工程师","title":"关于我"}]