1. 先搞清楚Winscope里的“Invisible due to”是给谁看的1.1 “Invisible due to”不是Winscope自己猜的是系统算好的如果你经常在Winscope里翻窗口状态应该对这样一个细节很熟悉点开某个窗口右侧状态面板里会冒出一行“Invisible due to: ...”后面跟着一段英文短语。我第一次看到的时候还以为是Winscope在后台帮我做了分析把不可见原因推断出来了。后来追了一圈源码才发现Winscope压根没做这种推理它只是把系统窗口管理模块里已经算好的可见性结论原样展示出来。也就是说这个标记的本质是“系统窗口状态快照”里自带的一个结果字段。窗口为什么不可见、什么时候被判定为不可见都是系统侧在更新窗口状态时写入的。Winscope拿到这份trace后只是把这个字段翻译成可读文案。当你看到“Invisible due to”时看到的是系统在某一瞬间对这个窗口做的整体判断而不是Winscope对你的善意提醒。这带来一个很实际的启示要理解这个标记是怎么来的不能只停留在Winscope界面上得往系统窗口管理逻辑里钻。窗口可见性不是单个条件决定的而是好几个维度同时检查任何一个不满足都会被记录成不可见原因。1.2 为什么窗口不可见这件事需要一个明确的“原因”你可能会想窗口不可见就是不可见给个布尔值不就行了为什么要单独弄一个“Invisible due to”?早期窗口调试确实苦在这个地方。窗口不可见只是一个结果导致不可见的原因能差很远可能是应用自己把视图藏了可能是Activity退到后台了可能是窗口没有surface可绘制也可能是窗口被上层盖住了。如果系统只给一个invisible开发者拿着日志根本不知道从哪里下手只能一层层打log去猜。把原因字段写进窗口状态以后调试路径就变成点开窗口看到不可见原因直接定位到对应模块。“Invisible due to”等于系统在窗口旁边留了一张便签写着“我为什么不画它”。哪怕便签写得不够完整至少给了排查一个方向比盲人摸象强得多。1.3 从窗口状态到Winscope面板数据和谁有关Winscope展示的窗口状态基本是系统侧窗口状态对象里那些公开状态位的搬运。和你直接相关的有这么几个关键点窗口对应的token状态比如token是不是被标成隐藏token关联的ActivityRecord是否为空窗口本身的视图可见性值也就是平时说的View visibility是多少窗口surface是否存在mHasSurface是true还是false窗口挂在哪个display上display当前是否处于正常可用状态窗口在层级树上是否被其他窗口遮挡。这些字段在Winscope窗口详情里基本都能看到。它们组合出来的结果就是“Invisible due to”后面跟的那些原因。所以想准确读懂这个标记需要先知道系统在算可见性时到底按什么顺序查了哪些东西。2. 常见“Invisible due to”来源逐个拆2.1 我见过最多的几类原因不同系统版本里具体文案可能不太一样但判断逻辑大差不差。我把反复见过的几类整理成一张表每一类都补充了触发场景和排查方向方便对照。原因文本含义最常出现的场景排查重点Activity is not resumed窗口对应的Activity不在resumed状态应用退到后台、页面没走到前台状态、被其他Activity盖住Activity生命周期、Task状态、onStart/onStop时序WindowsToken is hidden窗口token被标记为隐藏窗口动画进行中、token被系统隐藏、应用被系统回收前token的hidden状态、窗口动画控制器View is GONE / INVISIBLE窗口自身视图被人为设为不可见应用主动调用setVisibility、布局尚未完成、列表回收应用侧视图逻辑、XML里initial visibilityOn appropriate display窗口当前所在display不被判定为合适分屏、画中画、多屏扩展、折叠态切换display id、display状态、窗口支持flagWindow is obscured窗口被其他窗口遮挡上层全屏遮罩、弹层盖住底层页面层级树z-order、遮挡窗口属性No surface / surface destroyed窗口没有可用surfacesurface尚未创建、surface被销毁、绘制流程中断mHasSurface、relayout调用时机2.2 一次可见性判定到底串起了哪些条件系统在决定一个窗口是否可见时不会只看某一个字段而是差不多按照下面这个顺序逐个检查先看窗口本身是否还挂在一个合法的token上。token如果已经是空、已经被标记为移除或者hidden那这个窗口连“存在”都谈不上后面的检查根本不用继续直接进入不可见分支。再看视图层的可见性。窗口一般关联着一个视图View视图有个visibility属性。如果应用把人家的visibility设置成了GONE或者INVISIBLE窗口管理模块也不会强行让它显示。这层检查拦截的是应用主动隐藏的情况。继续往下看Activity状态。如果窗口属于某个Activity而这个Activity没有处在resumed状态说明这个页面目前不应该是前台页面窗口自然也不能显示。这里会把“Activity is not resumed”记进原因。接着看display。窗口必须依附在一个合适的display上如果窗口当前被放到了一个系统认为不适合展示的display上或者display本身状态不正常也会被判定为不可见。这种情况在分屏、副屏场景最容易冒出来。再看surface。窗口最终能不能把内容显示到屏幕上靠的是surface。如果surface还没有创建、已经被销毁或者当前正在销毁过程中窗口就没有内容来源自然不可见。最后还要处理遮挡关系。有些窗口本身能显示但被其他窗口盖住了底下的窗口会被标成obscured。特别是在全屏弹层出现的时候底层窗口往往会带这条原因。上面任何一条不满足窗口就会被归到invisible。这也是你会看到“Invisible due to”后面跟着好几条原因的原因。2.3 判定顺序带来的两个影响第一原因是有先后关系的。系统是按顺序检查的所以第一条件不满足时后面的检查甚至不会再执行。这意味着“Invisible due to”里列出的第一条往往是最早被触发的原因也更接近根因。第二多条原因同时出现时要怀疑是不是有连锁反应。比如Activity退到后台它既不在resumed状态同时surface也可能在后台回收流程里被销毁于是两条原因同时出现。可真正的主因其实是“应用离开了前台”而不是surface本身有问题。3. 高级疑问为什么“Invisible due to”经常是一长串原因3.1 多个原因越是叠加越要往前找主因很多人在Winscope里看到一长串“Invisible due to”就懵了觉得窗口同时出了好几个问题。但实测下来大多数情况下只有一个根因其他都是伴随状态。举一个我实际追过的例子。某次排查一个应用从后台切回前台时偶发黑屏的问题我抓了一份trace。窗口状态面板里写着“Invisible due to: Activity is not resumed, WindowsToken is hidden”。当时我盯着这两条原因看了半天怀疑是不是Activity启动逻辑有问题。后来把时间线拉长才发现应用切到后台后系统先把它所在Task的token标记成隐藏随后Activity进入非resumed状态。也就是说token被隐藏才是根因Activity状态变化是结果。如果你只盯着“Activity is not resumed”去查应用生命周期方向就彻底偏了。所以当原因列表很长的时候我的习惯是倒着看先从时间线上最早出现的那个状态变化入手不要在一串结果里挑一个自认为合理的当根因。3.2 用“三段式对比法”还原状态变化Winscope的trace本质上是快照它只能告诉你“这一刻窗口是什么状态”不能直接告诉你“它是怎么变成这样的”。针对这个问题我在实战里反复用一套土办法我管它叫“三段式对比法”异常发生前抓一段trace异常发生时抓一段trace异常恢复后再抓一段trace。三份快照放在一起对比窗口状态的变化过程自然会浮出来。打个比方这就像你看一张照片只知道人摔倒了但你看连续三帧照片才能判断他是被绊倒的还是自己滑倒的。窗口状态也一样“Invisible due to”只是摔倒的那一帧真正有价值的是前后两帧里发生了什么。实际操作中不需要真的抓三份trace。Winscope本身支持在trace时间轴上拖动你可以选中异常前后几个关键时间点分别查看窗口状态。重点看这几个时间点里窗口的token状态、ActivityRecord状态、mHasSurface状态和display状态谁先变了谁后变了逻辑链就清楚了。4. 实战黑屏、分屏、遮挡三类问题的定位记录4.1 后台恢复黑屏先于“Activity is not resumed”出现的断点某次我处理一个“应用退到后台再切回来偶尔黑屏一下”的问题现场日志显示应用进程活着主线程没卡布局也执行了但就是有几帧显示不出来。打开Winscope的窗口快照以后异常窗口的状态里清清楚楚写着“Invisible due to: Activity is not resumed, No surface for window”。初看像是Activity生命周期和surface创建一起出问题但三段式对比把问题暴露了问题发生前窗口是可见的mHasSurface为true切后台的瞬间Activity状态先掉出resumed随后surface进入销毁流程切回前台时Activity resume状态先恢复但surface重建成了一张新surface中间出现了几百毫秒的空档。这个空档里窗口虽然label为可见但因为没有surface实际画不出来。定位到这里就知道这不是生命周期逻辑的错误而是surface重建时序和应用侧界面恢复节奏没有对齐。后来在应用侧调整了界面恢复的触发时机黑屏现象明显减少。这个案例最有价值的点在于如果只看“Invisible due to”里的“Activity is not resumed”很容易误判成生命周期问题真正的断点其实是surface重建窗口期。4.2 分屏窗口不显示窗口挂在了不合适的display上另一种让我印象深刻的场景是分屏模式下某一侧窗口显示不出来。界面看不到但应用进程活着页面逻辑也在跑。Winscope里的原因很简短“Invisible due to: On appropriate display”。原因越短问题往往越小但这不代表好查。我顺着display状态去翻发现这个窗口挂在了一个非默认display上而那个display的分屏状态和窗口预期的显示策略不一致。多数情况下这类问题的根源在应用自己身上有些应用没有声明对分屏的支持系统为了兼容性把它限制在主显示区域里也有的应用在多屏场景下没有正确处理display归属变化窗口被放到了错误的display上。无论哪种窗口管理模块都不会强行让窗口显示而是根据窗口的自身属性决定它在哪个display上可见。排查分屏相关问题时我建议先记住一个原则系统给的“Invisible due to”不一定代表有bug有些不可见是设计内的行为。区分“设计内”和“异常”的方法很简单——在单屏模式下复现同样的操作看窗口能不能正常显示。如果单屏正常而分屏不正常问题大概率出在窗口对display状态的处理上。4.3 窗口被遮挡却显示“可点”不可见与不可交互要分清还有一种情况和“Invisible due to”直接相关但很多人会看漏窗口本身并没有变成不可见仍然挂在层级树上正常显示但因为它被另一个全屏窗口盖住了用户点不到下面那层的东西。我遇到过一个问题一个半透明弹层弹出来之后底层页面的按钮点上去完全没有反应。Winscope打开一看底层窗口的状态竟然是可见的也没有标“Invisible due to”。但它上方叠了一个全屏窗口那个全屏窗口设置了可点击属性但没有把底层判成obscured。实际上的效果就是用户看着底层页面还在可是怎么点都没反应。这类问题如果只盯“Invisible due to”会漏掉因为底层窗口压根没被标记成不可见。正确的排查习惯应该是出现点击无响应时重点关注层级树里这个窗口上面压着什么。系统对“不可见”和“不可交互”是两套判断Winscope的“Invisible due to”只能帮你定位“显示”层面的问题交互层面要结合层级关系和窗口的touchable属性来分析。5. 看懂Winscope窗口状态字段从原因反查根因5.1 四个比“Invisible due to”本身更重要的字段原因文本是结论结论背后的字段才是证据。我每次看Winscope窗口详情都会先把下面这四个字段扫一眼。字段含义与不可见原因的关系mHasSurface窗口是否拥有可用surfacefalse时通常会伴随No surface类原因mViewVisibility视图层的可见性值值为8对应GONE值为4对应INVISIBLEmActivityRecord窗口绑定的Activity记录为null或状态异常会影响resumed判断display id和状态窗口挂载的屏幕变化时容易触发On appropriate display这四个字段单独看都只是状态位合在一起就能还原系统的判断思路。比如mHasSurface为false“Invisible due to”里出现No surface那就说明窗口确实没有可绘制内容如果mHasSurface为true但原因里还是写了No surface那就说明原因文本可能是快照时刻的临时状态与真正的现场有偏差需要再看时间线。5.2 从原因反查根因的四步流程我不建议拿到“Invisible due to”直接改代码原因是它只是入口。我习惯走这样四步第一步先确认你在Winscope里点开的这个窗口是不是用户看到的那个问题窗口。多窗口场景下窗口树很长点错层级很常见。第二步看窗口状态面板里的mHasSurface、mViewVisibility、mActivityRecord、display相关字段从这些原始状态里找出第一个不正常的字段。第三步把“状态变化发生的时间点”和logcat里的关键日志对齐。这一步可以把窗口状态和业务逻辑联系起来比如某个业务操作之后窗口才进入invisible那大概率就是这次操作引起的。第四步把异常时刻前后的窗口树变化整体过一遍看看有没有其他窗口在同一时刻发生可见性变化。比如一个窗口从invisible变成visible同时另一个窗口变成invisible这可能就是一次焦点或显示区域的切换。四步走完基本能把“Invisible due to”背后的故事讲圆。如果讲不圆说明线索还不够回去再抓一份trace。5.3 抓取与分析trace的正确姿势Winscope的分析能力再强也怕trace质量差。抓trace有几个讲究踩过几次坑以后我会特别注意抓取的时机比时长重要。问题能稳定复现就掐着问题出现前后那几十秒抓不要提前几分钟就开始录否则导入Winscope后拖动时间轴找异常点会很痛苦。抓trace时最好同步在logcat里打一条marker比如“start reproduce”和“bug appeared”。两个时间点一对齐窗口状态和业务日志就能互为印证。保存trace时多存几份。只有一份trace的情况下遇到状态瞬时变化你连对比的素材都没有。存档好习惯是异常前一份、异常时一份、异常后一份哪怕后两份不分析万一需要追溯也有原始数据。6. 排障经验与避坑清单6.1 最容易让人走弯路的三个习惯第一个是把“Invisible due to”当成根因。它只是结果窗口为什么走到这个结果要回到状态字段和时间线里找答案。我早期就犯过这毛病看到原因文本后直接去改窗口属性问题复现依旧因为真正的原因在另一个模块里。第二个是只盯窗口本身不看它在窗口树里的位置。窗口是否被遮挡、是否被父窗口隐藏、是否和兄弟窗口相互影响这些信息在窗口详情页里看不全需要跳回层级树里看整体结构。第三个是忽略“中间态”。窗口从一个状态切到另一个状态时会经历一些不稳定阶段比如surface正在销毁、可见性正在切换。这些中间态快照可能显示“Invisible due to”但它只存在几十毫秒。如果你拿中间态去定位线上问题会被带偏。想排除中间态干扰就用三段式对比法看它后续恢复到什么状态。6.2 一张可以照做的“Invisible due to”排查清单结合这些年排查窗口相关问题的经验我整理了一份固定的排查清单每次都是按这个顺序走。你可以直接拿过去用。确认问题窗口在窗口树里的位置以及它属于哪个应用还是系统进程看mHasSurface确认窗口当前有没有可绘制内容看mViewVisibility确认是不是应用自己把自己藏了看mActivityRecord和token状态确认Activity生命周期是否正常看display相关字段确认窗口挂载的显示区域是否合适看层级树里窗口上方是否有其他窗口排除遮挡和交互拦截把异常时刻和logcat里marker时间对齐锁定业务触发点。这套流程不一定每次都能一步到位但至少能保证你不会在“Invisible due to”这个结果字段上原地打转。最后分享一个我自己的体会每次拿到窗口相关的疑难问题我都先快速抓一份trace看看“Invisible due to”出现在什么时间点、什么背景下然后再决定要不要改代码。窗口的表现千奇百怪但状态数据不会说谎。真正值钱的不是那行原因文本而是它前后几步的状态变化。把时间线拉出来很多看起来复杂的问题其实就是一个状态切换没接上。