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 调用是同步的,而且会阻塞调用方线程。
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)。
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,背后发生了什么?
- App 进程:
WindowManagerGlobal.addView创建ViewRootImpl,然后调用ViewRootImpl.setView。 - App 进程:
ViewRootImpl.setView通过IWindowSession代理,调用addToDisplay方法。这个调用是同步的,会阻塞当前线程。 - Binder 驱动:将调用请求从 App 进程传输到 SystemServer 进程。数据被序列化到 Parcel 里,跨进程拷贝。
- SystemServer 进程:WMS 的
addWindow方法被调用。它分配WindowToken、创建WindowState、分配SurfaceControl。 - SystemServer 进程:WMS 通过
IWindow回调 App 进程,通知窗口创建完成。 - App 进程:收到回调后,
ViewRootImpl开始执行第一次布局和绘制。
整个过程,Binder 参与了两次跨进程调用:一次是 App 到 WMS,一次是 WMS 到 App。嗯,这里要注意:第二次回调是异步的,不会阻塞 WMS 的主线程。
19.4 Binder 线程池与窗口操作的性能
Binder 调用会消耗 Binder 线程池里的线程。WMS 的 Binder 线程池默认有 16 个线程。如果某个窗口操作卡住了,比如 relayout 里做了耗时操作,就会占着一个线程不放。
我遇到过一个问题:某个 App 频繁调用 addView 和 removeView,导致 WMS 的 Binder 线程全部被占满,其他 App 的窗口操作全部排队等待。结果就是整个系统卡顿,甚至 ANR。
View.post 把多次 addView 合并到一帧里。我自己在项目中就吃过这个亏,后来改成批量提交,性能提升很明显。
19.5 知识体系图
下面这张图展示了 Binder 在 WindowManager 体系中的完整作用:
19.6 避坑总结
最后,我把这些年踩过的坑整理一下:
| 问题 | 原因 | 解决方案 |
|---|---|---|
| Binder 调用失败 | 传输数据超过 1MB 限制 | 压缩数据或用 SharedMemory |
| 窗口操作 ANR | WMS Binder 线程被占满 | 批量提交窗口操作 |
| 自定义 Parcelable 异常 | 未正确实现 describeContents | 检查 Parcelable 实现 |
| 回调线程问题 | IWindow 回调在 Binder 线程执行 | 不要直接操作 UI,post 到主线程 |
嗯,Binder 这个东西,说复杂也复杂,说简单也简单。你只要记住:WindowManager 的每一次窗口操作,背后都是一次 Binder 调用。理解了这一点,很多问题就能迎刃而解。