16、WMS 更新窗口流程:relayoutWindow() 的核心逻辑、布局参数更新、Surface 重新分配、窗口重绘
这一章,我们来啃一块硬骨头——relayoutWindow()。说实话,这个函数是我在 WMS 里最常打交道的地方之一。你想想看,应用端每次调用 ViewRootImpl.relayout(),最终都会通过 Binder 跨进程调用到 WMS 这边的 relayoutWindow()。它就像是一个窗口的「刷新按钮」,按下去之后,布局、Surface、绘制,全得重新来一遍。
我个人习惯把 relayoutWindow() 的执行过程拆成四个阶段:参数校验与更新、Surface 重新分配、窗口重绘触发、结果回传。下面我们逐个来看。
16.1 核心入口:relayoutWindow() 的调用链
先看一眼调用链,心里有个底:
App 端:ViewRootImpl.relayout()
→ Session.relayout() // Binder 调用
→ WMS.relayoutWindow() // 核心逻辑
→ WindowState.relayout() // 窗口状态更新
→ SurfaceControl.openTransaction() // 事务提交
嗯,这里要注意,relayoutWindow() 是一个同步调用。什么意思?就是 App 端会阻塞等待 WMS 处理完,拿到新的 Surface 和布局参数后才继续执行。我在项目中遇到过因为 relayoutWindow() 耗时过长导致应用 ANR 的情况,后面会讲怎么排查。
16.2 布局参数更新:从 WindowManager.LayoutParams 到 WindowState
App 端传过来的 WindowManager.LayoutParams,包含了窗口的 x、y、宽、高、flags、type 等信息。WMS 拿到后,会做以下几件事:
- 校验参数合法性——比如 type 是否在允许范围内,flags 是否有冲突。
- 更新 WindowState 的 mAttrs——把新的 LayoutParams 存进去。
- 计算窗口的显示区域——结合屏幕大小、状态栏、导航栏等,算出窗口实际能占多大地方。
这里有个坑,我曾经踩过:App 端传过来的宽高可能是 MATCH_PARENT 或 WRAP_CONTENT,但 WMS 这边需要把它们解析成具体的像素值。如果解析逻辑有 bug,窗口就可能显示不全或者超出屏幕。
关键代码片段:
// WMS.relayoutWindow() 中更新布局参数
WindowState win = windowForClientLocked(session, client, false);
if (win != null) {
win.mAttrs.copyFrom(attrs); // 复制新的 LayoutParams
win.mRequestedWidth = attrs.width;
win.mRequestedHeight = attrs.height;
// 后续会调用 performLayoutAndPlaceSurfacesLocked() 重新布局
}
16.3 Surface 重新分配:从 SurfaceControl 到 Surface
这是 relayoutWindow() 最核心的部分。说白了,就是给窗口重新分配一块「画布」。这块画布在 SurfaceFlinger 那边对应一个 Layer,在 App 这边对应一个 Surface 对象。
流程大致如下:
- 如果窗口是第一次 relayout,WMS 会创建一个新的
SurfaceControl。 - 如果窗口已经有过 Surface,WMS 会根据参数变化决定是复用还是重建。
- 如果窗口需要隐藏或销毁,WMS 会释放 Surface 并通知 SurfaceFlinger 移除 Layer。
你想想看,每次 relayoutWindow() 都涉及一次 Binder 调用到 SurfaceFlinger,开销不小。所以 Android 做了优化:只有当窗口的尺寸、格式、flags 等真正变化时,才会重新分配 Surface。如果只是位置移动,Surface 可以复用。
避坑指南:我曾经遇到过一个 bug,App 端频繁调用 relayoutWindow() 且每次都改变窗口尺寸,导致 Surface 不断重建。结果就是界面卡顿,掉帧严重。后来加了一层判断:如果尺寸变化小于 1px,就不触发 Surface 重建。问题就解决了。
16.4 窗口重绘:从 WMS 到 App 的绘制循环
Surface 分配好之后,WMS 会通过 performLayoutAndPlaceSurfacesLocked() 触发一次全局布局。然后,它会向 App 端返回一个 relayoutResult,告诉 App:「你的窗口已经准备好了,可以开始画了。」
App 端收到结果后,会做两件事:
- 把新的
Surface对象交给ThreadedRenderer或Canvas。 - 调用
scheduleTraversals()触发一次完整的绘制流程(measure → layout → draw)。
说白了,WMS 只负责「分配画布」,真正往画布上画东西的,还是 App 自己。
16.5 核心流程图:relayoutWindow() 的完整执行路径
下面这张图,是我自己画的一个简化版流程。你可以把它当作 debug 时的路线图:
16.6 关键数据结构与返回值
relayoutWindow() 的返回值是一个 RelayoutResult 对象,里面包含了 App 端需要的一切:
| 字段 | 类型 | 说明 |
|---|---|---|
| outFrame | Rect | 窗口在屏幕上的最终位置(像素) |
| outInsetsState | InsetsState | 系统栏(状态栏、导航栏)的 insets 信息 |
| outSurface | Surface | App 端用来绘制的 Surface 对象 |
| outMergedConfiguration | MergedConfiguration | 合并后的配置(包含资源更新) |
| result | int | 标志位:是否成功、是否重绘、是否尺寸变化等 |
注意:outSurface 是一个 Binder 对象,App 端拿到后需要通过 SurfaceControl.createFromSurface() 转成本地可用的 Surface。如果转换失败,App 端会收到一个空的 Surface,绘制就会黑屏。我在项目中遇到过两次这种情况,都是因为 SurfaceFlinger 那边的 Layer 创建失败导致的。
16.7 性能优化建议
最后,分享几个我在实际项目中用过的优化手段:
- 减少 relayout 频率:App 端不要每帧都调用
relayout(),尽量合并多次请求。 - 避免不必要的 Surface 重建:如果只是改变窗口透明度或 z-order,不需要重建 Surface。
- 使用异步 relayout:Android 10 之后支持
relayoutAsync(),可以避免阻塞 UI 线程。 - 监控 relayout 耗时:在 WMS 侧打日志,如果单次 relayout 超过 10ms,就要警惕了。
嗯,这一章的内容就到这里。记住一句话:relayoutWindow() 是 WMS 和 App 之间最频繁的交互点,搞懂它,你就掌握了窗口更新的命脉。
公众号:蓝海资料掘金营,微信deep3321