一、从WindowManager.addView说起
大家应该都用过WindowManager,但你真的理解它的addView流程吗?
我记得刚做Android开发那会儿,以为addView就是把一个View塞到屏幕上。后来踩了坑才发现,这背后是一整套跨进程通信机制。说白了,你的App和系统服务之间,隔着一层Binder。
1.1 addView的调用链
我们先看一个最简单的例子:
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
wm.addView(myView, layoutParams);
这行代码背后发生了什么?我画了一张图,帮你理清整个流程:
看到这张图,你应该能感受到:addView不是简单的本地调用。它从App进程出发,经过Binder到达WMS,再通知SurfaceFlinger创建Surface。每一步都是跨进程的。
二、WindowToken与Session:两个关键角色
2.1 WindowToken是什么?
WindowToken,说白了就是一个令牌。系统用它来标识窗口的类型和归属。
我举个例子:你在Activity里addView,系统会检查这个Activity对应的WindowToken是否合法。如果token不对,直接拒绝添加。
核心要点:WindowToken是WMS管理窗口的凭证。没有token,窗口就创建不了。
WindowToken有三种常见类型:
| 类型 | 用途 | 创建时机 |
|---|---|---|
| Activity类型的Token | 标识Activity窗口 | Activity启动时 |
| 子窗口类型的Token | 标识PopupWindow等 | 父窗口创建时 |
| 系统窗口类型的Token | 标识Toast、系统弹窗等 | 系统服务创建时 |
2.2 Session的作用
Session是App与WMS之间的通信通道。每个App进程在WMS中都有一个对应的Session对象。
我个人习惯把Session理解成「专属客服」。你的App有什么窗口操作需求,都通过这个Session告诉WMS。WMS处理完,结果也通过Session返回。
// 简化后的Session工作流程
// App端
IWindowSession session = WindowManagerGlobal.getWindowSession();
session.addToDisplay(window, seq, attrs, viewVisibility, displayId, outFrame, outContentInsets);
// WMS端
class Session extends IWindowSession.Stub {
@Override
public int addToDisplay(IWindow window, ...) {
// 验证WindowToken
// 创建WindowState
// 分配Surface
return res;
}
}
避坑指南:我曾经遇到过一个问题:在Service中直接addView,结果崩溃了。原因就是Service没有有效的WindowToken。后来我改用系统窗口类型(TYPE_SYSTEM_ALERT),才解决。
三、Binder在绘制中的角色
你想想看,App进程和系统服务不在同一个进程里。View要绘制到屏幕上,数据怎么传过去?
答案就是Binder。Android的跨进程通信全靠它。
3.1 Binder传递了什么?
在绘制流程中,Binder主要传递这几类数据:
- 窗口参数:LayoutParams、窗口位置、大小等
- Surface引用:通过Binder传递Surface的句柄
- 绘制指令:DisplayList等渲染数据
- 输入事件:触摸事件的分发
3.2 一次完整的Binder调用
我们以addView为例,看看Binder调用链:
// 1. App端调用
WindowManagerGlobal.addView()
→ 获取IWindowSession (Binder代理)
→ session.addToDisplay() // Binder调用
// 2. WMS端接收 (Binder Stub)
Session.addToDisplay()
→ 验证WindowToken
→ 创建WindowState
→ 分配Surface
// 3. 结果返回
→ 通过Binder传回SurfaceControl
→ App端拿到Surface
注意:Binder调用是同步的。如果WMS处理时间过长,App主线程会卡住。所以窗口操作要轻量,别在addView里做耗时计算。
四、从源码角度看addView
我们直接看关键源码,这样更直观:
// WindowManagerGlobal.java
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
// 1. 检查参数
if (view == null) throw new IllegalArgumentException("view must not be null");
// 2. 创建ViewRootImpl
ViewRootImpl root = new ViewRootImpl(view.getContext(), display);
// 3. 设置参数
view.setLayoutParams(wparams);
// 4. 添加到列表
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
// 5. 核心:通过Binder添加窗口
root.setView(view, wparams, panelParentView);
}
// ViewRootImpl.java
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
// 请求WMS添加窗口
res = mWindowSession.addToDisplay(
mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mTmpFrame, mAttachInfo.mContentInsets,
mAttachInfo.mStableInsets, mAttachInfo.mOutsets,
mAttachInfo.mDisplayCutout, mInputChannel,
mTempInsets);
}
}
这段代码我看了不下十遍。每次看都有新体会。核心就一句话:addView的本质,是通过Binder让WMS帮你创建一个窗口。
五、绘制流程中的跨进程协作
绘制不是App自己画完就完事了。它需要多方协作:
- App进程:测量、布局、绘制View树,生成DisplayList
- WMS:管理窗口层级、分配Surface、处理输入事件
- SurfaceFlinger:合成所有Surface,输出到屏幕
这三者之间,全靠Binder通信。我画个简化的时序图:
看到没?每一次交互都是Binder在背后默默工作。没有Binder,这些进程就是孤岛。
六、总结与避坑
好了,我们来捋一下今天的内容:
- addView流程:App → Binder → WMS → SurfaceFlinger
- WindowToken:窗口的身份证,没有它寸步难行
- Session:App与WMS的专属通信通道
- Binder:贯穿整个绘制流程的跨进程桥梁
最后说一句:理解这些底层机制,不是为了背源码。而是当你遇到窗口显示异常、崩溃问题时,能快速定位是哪个环节出了问题。我这些年排查的诡异bug,十有八九都跟WindowToken或Binder调用有关。
嗯,这一章就到这里。内容不少,但都是干货。你消化一下,有问题随时交流。