28、WindowManager 的调试与优化:使用 dumpsys window 命令、查看窗口层级、分析窗口泄漏、性能优化建议

说实话,WindowManager 这层东西,平时写业务代码很少直接碰。但一旦出问题,基本都是疑难杂症——界面卡住、窗口叠错、内存泄漏……这时候你光靠 Log 是搞不定的。我个人的习惯是,先上 dumpsys window,把现场“拍个照”,再慢慢分析。

这一章,我就带你走一遍我平时排查问题的完整流程。从命令怎么用,到怎么看懂输出,再到怎么定位泄漏和优化性能,咱们一步到位。

28.1 dumpsys window:你的第一把手术刀

dumpsys window 是系统自带的调试工具。它能把 WindowManagerService 内部的状态全部 dump 出来。说白了,就是给你一张当前所有窗口的“全家福”。

我最常用的几个子命令:

命令 作用
dumpsys window 输出所有窗口信息,内容最全
dumpsys window windows 只看窗口列表,精简很多
dumpsys window displays 查看显示区域和屏幕信息
dumpsys window policy 查看窗口策略,比如焦点、按键行为
dumpsys window tokens 查看所有 WindowToken,分析泄漏常用

举个例子,我排查一个“弹窗不消失”的问题时,会先跑:

adb shell dumpsys window windows | grep -E "Window|mOwner"

这样能快速过滤出所有窗口和它们的宿主。嗯,这里要注意,输出里有个 mSurface 字段,如果看到 Surface(name=null),说明这个窗口已经销毁了但资源没释放——典型的泄漏信号。

28.2 看懂窗口层级:谁在上面?谁在下面?

窗口层级是 WindowManager 的核心。每个窗口都有一个 mBaseLayermSubLayer,决定了它的 Z 轴顺序。

我一般用这个命令看层级:

adb shell dumpsys window windows | grep -E "Window #|mBaseLayer|mSubLayer|mAttachedWindow"

输出大概长这样:

Window #0: Window{...}
  mBaseLayer=21000
  mSubLayer=0
  mAttachedWindow=null

Window #1: Window{...}
  mBaseLayer=21000
  mSubLayer=1
  mAttachedWindow=Window{...}

你看,mBaseLayer 相同的窗口属于同一层级组,mSubLayer 决定组内顺序。如果两个窗口的 mBaseLayer 一样,但 mSubLayer 乱掉了,就会出现“该弹窗跑到后面去了”这种诡异现象。

我曾经遇到过一个 case:某个 PopupWindow 总是被 Dialog 挡住。查了半天,发现是 PopupWindow 的 mSubLayer 被设成了负数。嗯,这个坑我后面会细说。

核心要点: 窗口层级由 mBaseLayer + mSubLayer 共同决定。调试时重点关注这两个值。

28.3 分析窗口泄漏:谁还活着?

窗口泄漏,说白了就是 Activity 已经销毁了,但它的 Window 还挂在 WMS 里。这种情况最容易出现在 Dialog 或 PopupWindow 忘记 dismiss 的场景。

我排查泄漏的套路是这样的:

  1. 先跑 dumpsys window tokens,看所有存活的 WindowToken。
  2. 找到 mActivityRecord 为 null 但 mWindows 不为空的 token。
  3. 再跑 dumpsys window windows | grep "mToken=",找到对应的窗口。

举个例子,输出里如果看到:

WindowToken{...}
  mActivityRecord=null
  mWindows=[Window{...}]

这就说明有一个窗口的宿主 Activity 已经没了,但窗口还在。这就是泄漏。

警告: 窗口泄漏不仅浪费内存,还会导致触摸事件分发异常。我曾经在一个项目中,因为一个泄漏的 Dialog 窗口,导致整个页面点击无响应。排查了整整两天才找到原因。

28.4 性能优化建议:别让 WMS 成为瓶颈

WindowManager 的性能问题,通常不是它本身慢,而是它被频繁调用了。我见过最夸张的 case,一个页面在 1 秒内调了 200 次 addView——直接导致 UI 线程卡死。

下面是我总结的几个优化点:

问题 建议
频繁 addView/removeView 用 ViewStub 或 ViewPool 复用,减少 WMS 调用
窗口层级过深 控制子窗口数量,尽量用 Dialog 替代 PopupWindow
Surface 创建过多 每个窗口对应一个 Surface,减少窗口数量就是减少 Surface
动画期间频繁更新布局 动画期间避免调用 requestLayout,用 invalidate 代替

我个人习惯在性能敏感的场景下,用 dumpsys window 配合 dumpsys gfxinfo 一起看。前者看窗口数量,后者看渲染帧率。两个数据一对比,瓶颈在哪一目了然。

小技巧: 如果你怀疑某个窗口导致卡顿,可以在 dumpsys window windows 的输出里找到它的 mSurface 地址,然后去 dumpsys SurfaceFlinger 里查它的渲染状态。这是最直接的定位方式。

28.5 一张图看懂调试流程

下面这张图是我自己总结的调试流程。你按这个顺序走,基本不会漏掉关键信息。

WindowManager 调试与优化流程 1. 收集现场 2. 查看窗口层级(dumpsys window windows) 层级异常? 检查 mBaseLayer/mSubLayer 3. 分析泄漏(dumpsys window tokens) 4. 性能优化(减少窗口/复用Surface) 常用命令速查 dumpsys window windows dumpsys window tokens dumpsys window policy dumpsys gfxinfo

你看,整个流程其实不复杂。关键是你要知道每一步看什么、怎么看。我刚开始接触 WMS 调试时,也对着满屏的 dump 输出发懵。后来慢慢总结出这套流程,效率高了很多。

28.6 避坑指南

最后,分享几个我踩过的坑:

  • 不要忽略 mActivityRecord 为 null 的 token——这往往是泄漏的源头。我曾经在一个项目里,因为一个 Dialog 的 dismiss 没调用,导致整个 Activity 无法被 GC,内存暴涨。
  • 注意 mSubLayer 为负数的窗口——它会被压到父窗口下面。如果你发现某个弹窗显示不出来,先查这个值。
  • 频繁调用 addView 时,记得用 View.post 延迟一下——否则 WMS 会频繁刷新,导致卡顿。我见过一个极端 case,1 秒内调了 200 次 addView,直接 ANR。

嗯,调试 WindowManager 其实没那么玄乎。掌握这几个命令和思路,大部分问题都能快速定位。下次遇到窗口相关的 bug,别慌,先跑个 dumpsys window 看看。


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