19、Window 的跨进程通信:Binder 在 WindowManager 体系中的作用

说实话,WindowManager 这个体系里,最让我觉得「妙」的地方,就是它怎么把 App 进程和 SystemServer 进程串起来的。你想想看,咱们在 Activity 里调个 addView,窗口就出来了——这背后要是没有 Binder,根本玩不转。

我个人习惯把 Binder 理解为「Android 世界的筋脉」。WindowManager 的所有跨进程操作,说白了都是靠它来传递的。今天咱们就把它掰开揉碎了看看。

19.1 为什么需要跨进程通信?

先问个问题:窗口是谁在管理的?

答案是 SystemServer 里的 WindowManagerService(WMS)。你的 App 跑在自己的进程里,它不能直接操作屏幕。那 App 想加个窗口怎么办?只能通过 Binder 告诉 WMS:「老哥,帮我加个窗口。」

我刚开始做 Android 开发时,总觉得这层通信是「理所当然」的。直到有一次我调试一个 ANR 问题,发现 WMS 卡住了,所有 App 的窗口操作都排着队等——那一刻我才真正意识到,Binder 调用是同步的,而且会阻塞调用方线程。

核心结论: WindowManager 的跨进程通信,本质上是 App 进程通过 Binder 调用 WMS 提供的接口。没有 Binder,WindowManager 就是个单进程玩具。

19.2 Binder 在 WindowManager 中的角色

咱们从三个层面来看 Binder 的作用:

19.2.1 接口定义层:IWindowManager 与 IWindowSession

WMS 对外暴露了两个核心 Binder 接口:

  • IWindowManager:全局窗口管理接口,比如获取窗口列表、设置屏幕亮度等。
  • IWindowSession:每个 App 进程与 WMS 之间的会话接口,负责 addView、removeView、relayout 等操作。

我记得第一次看源码时,找了半天没找到 addView 在哪。后来才发现,WindowManagerGlobal.addView 里调的是 WindowManagerService.openSession 返回的 IWindowSession 代理对象。嗯,这里要注意:每个 App 进程只有一个 Session,所有窗口操作都走这个通道。

// WindowManagerGlobal.java (App 进程)
public void addView(View view, ViewGroup.LayoutParams params) {
    // ...
    // 获取 IWindowSession 代理
    sWindowSession = WindowManagerGlobal.getWindowSession();
    // 跨进程调用 addToDisplay
    res = sWindowSession.addToDisplay(mWindow, mSeq, mAttrs, ...);
    // ...
}

19.2.2 数据传输层:Parcelable 与 Binder 对象传递

Binder 传输数据靠的是 Parcel。WindowManager 里大量使用了 Parcelable 对象,比如 WindowManager.LayoutParams。你传进来的布局参数,会被序列化成 Parcel,跨进程飞到 WMS 那边再反序列化。

这里有个坑:Binder 传输有大小限制,默认 1MB。我曾经遇到过一个问题:某个窗口的 SurfaceControl 数据太大,导致 Binder 调用失败。后来查了半天才发现是传了一个超大 Bitmap 的缩略图。解决方案?要么压缩数据,要么改用共享内存(SharedMemory)。

避坑指南: 我曾经在传递自定义 Parcelable 时,忘了实现 describeContents() 方法,结果在跨进程传输时抛异常。记住:所有通过 Binder 传输的自定义对象,必须正确实现 Parcelable 接口。

19.2.3 回调机制:IWindow 与客户端通知

WMS 处理完窗口操作后,怎么通知 App 进程?答案是 反向 Binder 调用

App 在创建窗口时,会传一个 IWindow.Stub 对象给 WMS。这个对象是 Binder 服务端,运行在 App 进程里。WMS 通过它来通知 App:窗口位置变了、焦点变了、需要重新布局了等等。

// IWindow.aidl
interface IWindow {
    void resized(in Rect frame, ...);
    void windowFocusChanged(boolean hasFocus, ...);
    void dispatchAppVisibility(boolean visible);
    // ...
}

你想想看,这其实是个「双向 Binder」:App 调用 WMS 是正向,WMS 回调 App 是反向。两个方向都走 Binder,但方向不同,线程模型也不同。

19.3 核心流程:addView 的 Binder 之旅

咱们用一个具体的例子串起来:App 调用 WindowManager.addView,背后发生了什么?

  1. App 进程WindowManagerGlobal.addView 创建 ViewRootImpl,然后调用 ViewRootImpl.setView
  2. App 进程ViewRootImpl.setView 通过 IWindowSession 代理,调用 addToDisplay 方法。这个调用是同步的,会阻塞当前线程。
  3. Binder 驱动:将调用请求从 App 进程传输到 SystemServer 进程。数据被序列化到 Parcel 里,跨进程拷贝。
  4. SystemServer 进程:WMS 的 addWindow 方法被调用。它分配 WindowToken、创建 WindowState、分配 SurfaceControl
  5. SystemServer 进程:WMS 通过 IWindow 回调 App 进程,通知窗口创建完成。
  6. App 进程:收到回调后,ViewRootImpl 开始执行第一次布局和绘制。

整个过程,Binder 参与了两次跨进程调用:一次是 App 到 WMS,一次是 WMS 到 App。嗯,这里要注意:第二次回调是异步的,不会阻塞 WMS 的主线程。

19.4 Binder 线程池与窗口操作的性能

Binder 调用会消耗 Binder 线程池里的线程。WMS 的 Binder 线程池默认有 16 个线程。如果某个窗口操作卡住了,比如 relayout 里做了耗时操作,就会占着一个线程不放。

我遇到过一个问题:某个 App 频繁调用 addViewremoveView,导致 WMS 的 Binder 线程全部被占满,其他 App 的窗口操作全部排队等待。结果就是整个系统卡顿,甚至 ANR。

优化建议: 窗口操作尽量批量处理,不要频繁跨进程调用。比如用 View.post 把多次 addView 合并到一帧里。我自己在项目中就吃过这个亏,后来改成批量提交,性能提升很明显。

19.5 知识体系图

下面这张图展示了 Binder 在 WindowManager 体系中的完整作用:

Binder 在 WindowManager 体系中的作用 App 进程 SystemServer 进程 WindowManagerGlobal addView / removeView ViewRootImpl setView / relayout IWindow.Stub 回调接收端 IWindowSession Binder 代理 WindowManagerService addWindow / removeWindow WindowState 窗口状态管理 SurfaceControl Surface 分配 IWindow 代理 回调发送端 Binder 调用 addToDisplay Binder 回调 resized / focusChanged 关键点总结 1. IWindowSession:App 到 WMS 的 Binder 代理接口 2. IWindow:WMS 到 App 的 Binder 回调接口 3. 所有窗口操作(addView/removeView/relayout)都走 Binder 4. Binder 线程池有限,窗口操作不要过于频繁

19.6 避坑总结

最后,我把这些年踩过的坑整理一下:

问题 原因 解决方案
Binder 调用失败 传输数据超过 1MB 限制 压缩数据或用 SharedMemory
窗口操作 ANR WMS Binder 线程被占满 批量提交窗口操作
自定义 Parcelable 异常 未正确实现 describeContents 检查 Parcelable 实现
回调线程问题 IWindow 回调在 Binder 线程执行 不要直接操作 UI,post 到主线程

嗯,Binder 这个东西,说复杂也复杂,说简单也简单。你只要记住:WindowManager 的每一次窗口操作,背后都是一次 Binder 调用。理解了这一点,很多问题就能迎刃而解。


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