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 的核心。每个窗口都有一个 mBaseLayer 和 mSubLayer,决定了它的 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 的场景。
我排查泄漏的套路是这样的:
- 先跑
dumpsys window tokens,看所有存活的 WindowToken。 - 找到
mActivityRecord为 null 但mWindows不为空的 token。 - 再跑
dumpsys window windows | grep "mToken=",找到对应的窗口。
举个例子,输出里如果看到:
WindowToken{...}
mActivityRecord=null
mWindows=[Window{...}]
这就说明有一个窗口的宿主 Activity 已经没了,但窗口还在。这就是泄漏。
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 一张图看懂调试流程
下面这张图是我自己总结的调试流程。你按这个顺序走,基本不会漏掉关键信息。
你看,整个流程其实不复杂。关键是你要知道每一步看什么、怎么看。我刚开始接触 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 看看。