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 拿到后,会做以下几件事:

  1. 校验参数合法性——比如 type 是否在允许范围内,flags 是否有冲突。
  2. 更新 WindowState 的 mAttrs——把新的 LayoutParams 存进去。
  3. 计算窗口的显示区域——结合屏幕大小、状态栏、导航栏等,算出窗口实际能占多大地方。

这里有个坑,我曾经踩过:App 端传过来的宽高可能是 MATCH_PARENTWRAP_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 端收到结果后,会做两件事:

  1. 把新的 Surface 对象交给 ThreadedRendererCanvas
  2. 调用 scheduleTraversals() 触发一次完整的绘制流程(measure → layout → draw)。

说白了,WMS 只负责「分配画布」,真正往画布上画东西的,还是 App 自己。

16.5 核心流程图:relayoutWindow() 的完整执行路径

下面这张图,是我自己画的一个简化版流程。你可以把它当作 debug 时的路线图:

App 调用 relayout() Binder → WMS.relayoutWindow() 校验 LayoutParams,更新 WindowState.mAttrs Surface 需要重建? 创建新 SurfaceControl 复用已有 Surface performLayoutAndPlaceSurfacesLocked() 返回 relayoutResult 给 App

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