1. 先搞清楚:Winscope里的“Invisible due to”到底是谁写的
前阵子连续帮人排查了两个窗口不显示的问题,现象几乎一样:在Winscope窗口详情面板里,目标窗口状态写着“invisible due to: app not visible”。不少人对这个字段的理解只停在“窗口不可见”,但真拿到一个窗口莫名消失的问题时,这个“due to”后面的原因才是真正的线索。这篇文章就围绕这个高级疑问展开,聊聊“Invisible due to”是怎么来的,底层在算哪些条件,以及拿到这个字段之后怎么一步步往下追。
1.1 Winscope的窗口数据从哪来
Winscope不是自己凭空造状态的,它吃的是系统录制下来的Window Trace。开发者一般是在开发者选项里开启“系统跟踪”,或者用命令行录制trace,把WindowManagerService(WMS)和SurfaceFlinger的关键事件变成一份带时间线的记录。Winscope页面里的窗口树,就是这份trace在不同时刻的窗口状态快照。
那“Invisible due to”为什么会出现?因为它本身就是WMS在计算某个窗口是否可见时,顺手记录下来的“原因字段”。WMS在每次窗口状态变化时,比如窗口被添加到系统窗口集合、窗口执行relayout、Activity启动或停止,都会重新计算相关窗口的可见性,并把结果保存下来。Winscope读trace文件后,把WMS保存的原因翻译成人能理解的文本。
换句话说,你看到的“Invisible due to”不是Winscope前端自己parse出来的,根源在WMS的WindowState对象里。搞懂WMS里这个判断是怎么算的,就搞懂了这个高级疑问的一半。
另外,这里要区分两个概念:Winscope里有Window Trace和Layer Trace两类数据。Window Trace管的是“窗口管理”,决定窗口允不允许显示;Layer Trace管的是“图层合成”,决定最终有没有上屏。“Invisible due to”出现在Window Trace里,说明WMS层面已经把这个窗口判为不可见。如果这个字段正常,但屏幕里还是没有内容,那就得切到Layer Trace查SurfaceFlinger为什么没合成这个Layer。
1.2 “Invisible due to”字段的定位和含义
打开一份Window Trace,点击窗口列表里的某个窗口节点,右侧详情面板里通常能看到类似这样的一行:
Visibility state: invisible due to app not visible在Android 12之后的版本里,字段可能叫isVisible或visibility,值为false时会附带原因描述。这里说的“可见性”,不是指窗口里某个按钮或某张图片的可见性,而是指整个窗口在屏幕层面是否处于“用户能看到”的状态。窗口系统判断这个值时不看窗口内容,只看窗口自身的状态标志,以及它所属的Activity和Task当前处于什么状态。
这个字段最大的价值是给了一个明确的排查方向。如果原因写的是“view not visible”,问题大概率在窗口自身的View可见性上;如果原因写的是“app not visible”,问题大概率在Activity生命周期或Activity状态管理上。很多人遇到窗口不显示,第一反应是翻UI布局代码,这个方向大概率会走偏。先读这个字段,能省下不少时间。
1.3 可见性不是单一判断:三层判断体系
窗口可见性在系统里不是“非黑即白”的单一标志,而是由多个条件共同决定的。我习惯把它拆成三层来看。
第一层是对窗口自身的检查。窗口的顶层View可见性、窗口是否处于销毁中、是否已经被移除,这些都算在这一层。第二层是策略检查,系统UI策略会给特定类型的窗口加限制,比如锁屏时某些窗口不允许显示、沉浸模式下某些系统栏被隐藏。第三层是宿主检查,也就是窗口所属的Activity和Task是否可见,这一层由ActivityTaskManagerService(ATMS)管理。
三层全部通过,窗口才会显示;任何一层失败,窗口就被判为不可见,并且把最先拦截的那条原因记下来。这也是为什么“Invisible due to”后面永远跟一个具体原因,而不是笼统地写“不可见”。
理解这个三层结构,排查起来就能顺着“源头”往上追。之前看到一个窗口状态是“invisible due to parent not visible”,我顺着窗口树往上找,结果发现是父窗口所在的Task不可见。如果只看目标窗口,会以为是自身代码出问题,实际上问题出在最外层Task状态上。
2. 逐个拆解最常见的“Invisible due to”原因
窗口不可见的原因在AOSP源码里不是一组固定不变的标识,而是随着Android版本一直在演进。但从实际排查经验看,高频出现的就那么几个。下面把每个原因的含义、典型场景、排查方向分开讲清楚。
2.1 app not visible:最常见,也最容易误判
这个原因的字面意思是:窗口所属的App,准确说是这个窗口关联的ActivityRecord,在WMS眼中不可见。
出现这个状态最常见的场景是:Activity A启动了Activity B,而B是全屏且不透明的Activity。A进入onStop,ActivityRecord的visible标志变成false。此时A上挂着的所有窗口,包括A的主窗口、Dialog、PopupWindow,以及通过A的token添加的自定义窗口,全都会被标为“invisible due to app not visible”。
这个判断里,WMS不是自己拍板说“我看不见App”,而是会去依赖ATMS里保存的ActivityRecord visible状态。这个状态跟Activity生命周期强相关:处于RESUMED状态的Activity通常visible为true;一旦进入STOPPED或PAUSED状态,visible就变成false。
所以排查“app not visible”时,第一优先级是查Activity状态,而不是查布局代码。去看目标窗口是用哪个Context创建的、创建时对应的Activity还活着没有、生命周期停在什么阶段。不少问题最后都指向同一个根因:窗口创建时宿主Activity已经不visible了,代码却在以这个Activity的Context为宿主挂窗口。
2.2 view not visible:最直观,但容易查错对象
窗口创建时,顶层View的getVisibility()结果会被记录到WindowManager.LayoutParams里。如果这个顶层View是View.GONE(值为8)或View.INVISIBLE(值为4),WMS就会认为这个窗口自身不具备显示条件,于是标记为“view not visible”。
这里有个容易踩的坑:窗口可见性检查的是顶层View(一般是DecorView)的状态,不是某个子View。项目里经常有人告诉我“把界面里一块内容设置成GONE之后窗口就没了”,实际查下来,代码设置的是某个子View的GONE,窗口顶层View还是VISIBLE。这说明窗口本身没问题,只是内容区域被隐藏了,两者不能混在一起。
遇到“view not visible”时,建议先看Winscope里窗口详情中的mViewVisibility字段。它区分了0、4、8三种值:0是VISIBLE,4是INVISIBLE,8是GONE。确认之后再回代码里查这个顶层View是谁、在哪个生命周期节点被设置。有时候是主题里配置了隐藏窗口,有时候是某段回调代码调了setVisibility,定位起来并不难。
2.3 task not visible:整个任务一起不可见
窗口所属的Task不可见,会直接导致该Task下所有Activity的窗口全部不可见。这个原因常见于多任务、分屏、画中画场景。比如用户把App滑入最近任务列表但没恢复,该App对应Task在WMS里的可见状态就是false,Task下的所有窗口状态就会显示成“task not visible”。
与“app not visible”相比,“task not visible”的层级更高。app not visible可能是某个Activity被遮挡或停止,task not visible则是整个任务栈脱离了前台。在Winscope窗口树上,Task本身也是一个节点,点击Task节点能看到它的visible属性。如果一批窗口同时变成这个原因,先看Task节点,很多场景下不用再逐个窗口查了。
排查方向上,要让这个Task可见,通常需要把Task带回前台,或者调整Task的组织方式。多窗口场景里,如果一个Task被另一个Task完全覆盖,也会出现这个状态。检查时留意窗口树的层级顺序,不可见的Task节点在Winscope里通常会被灰显或折叠。
2.4 wm dieing / removed:窗口正在走销毁流程
当应用调用removeView,或者应用进程被系统清理,窗口会被WMS标记为dieing。通俗说,窗口已经踏上“被销毁”的路,只是还没走完。整个过程像一个人已经递交了离职申请,但还没办完手续,工牌暂时还在。这个期间Winscope看到的窗口状态就是“wm dieing”。
与dieing相近的还有“removed”,含义是窗口已经从WMS的活跃窗口集合里移除,Winscope保留它只是因为trace录制的时刻在移除之前或移除瞬间。遇到这两个原因时,注意力要放在窗口生命周期管理上:什么时候调的remove,进程有没有被杀,有没有窗口泄漏导致一直清理不掉。
代码侧排查时,通常是日志里先看到removeView调用,但窗口还在,于是怀疑泄漏。再看Winscope里状态是dieing,基本就能确认是“销毁流程还没走完”。离开页面时窗口消失异常,多检查onDetachedFromWindow之后是否还持有旧窗口引用。
2.5 parent not visible:子窗口跟着父窗口遭殃
Dialog、PopupWindow、输入法窗口这类窗口属于子窗口,它们挂在某个父窗口下。父窗口不可见,子窗口必然不可见,所以Winscope会标成“parent not visible”。
我遇到过不少这样的案例:开发者把一个PopupWindow当成“独立的悬浮窗”来用,却不知道它实质上是主窗口的附属子窗口。一旦主窗口因为Activity切换而不可见,PopupWindow也就跟着隐身了。
排查技巧是:在窗口树里点击目标窗口,看它的parent节点是谁,一句话就能定位问题。父窗口如果状态是“app not visible”,问题又回到了Activity生命周期;父窗口如果是“view not visible”,要查父窗口顶层View的Visibility。总之,从目标窗口向根节点方向逐层看,不要只停留在这个窗口的状态行上。
另外,子窗口会带一些隐含限制,比如子窗口不能决定父窗口的可见性,但父窗口可以随时让子窗口失效。写这类UI时,最好在脑子里把父子关系先理清,再决定用PopupWindow还是独立窗口。
2.6 policy类原因:not visible by policy与op not visible
这类原因来自系统策略和权限管理。比如后台弹窗权限(SYSTEM_ALERT_WINDOW)被拒绝,窗口会显示“op not visible”;有些系统窗口在特定显示模式下被策略拦住,会显示“not visible by policy”。
这类问题的排查方向要跳出应用代码,去看系统状态。一般要检查三件事:应用的SYSTEM_ALERT_WINDOW授权是否正常、系统当前是否处于会拦截窗口的特殊模式、以及窗口类型是否符合系统策略要求。用命令直接查授权状态和窗口策略比在Winscope里反复翻更快,后面第4章会专门整理。
3. 用Winscope定位一个“看不见的悬浮面板”
3.1 现场与trace采集
某开发者的应用里实现了一个悬浮面板,设计逻辑是:用户把App切到后台后,面板依然悬浮在桌面上,方便快速操作。测试时发现,App在前台时面板正常,一旦按Home键切到后台,面板就消失了。
复现步骤很简单:打开App让面板出现,按Home键回到桌面,面板没有显示。为了抓原因,开启系统跟踪,勾选window manager和surfaceflinger相关类别,再复现一次,保存trace并导入Winscope。
这里给一个建议:复现前先把Winscope页面准备好,trace生成后马上导入。如果拖太久,容易忘了同时间段的动画状态。录制时记下按Home键的大致时间点,方便在Winscope里快速切到关键帧。
3.2 在Winscope里定位不可见窗口
导入trace后,在窗口列表右上角搜索包名,找到悬浮面板对应的窗口。点击窗口卡片,右侧详情里能看到状态行:
invisible due to: app not visible再看窗口树的挂载关系,会发现它是挂在某个Activity节点下面,而不是一个独立的application token节点。也就是说,这个“悬浮面板”在WMS眼里并不是独立的悬浮窗,而是那个Activity的一个附属窗口。
把时间线拖动到按Home键前后,能看到Activity状态从RESUMED变成STOPPED的那一帧,面板窗口的可性状态也同步从true变成false。这就基本确认了问题方向:悬浮面板的可见性,完全跟随宿主Activity的生命周期在走。
3.3 结合判断链条找root cause
为什么窗口跟着Activity“消失”?因为三层判断里,“app not visible”这一层拦截了。窗口自身没问题,策略层也放行,但宿主这一层不合格。
继续查代码,发现这个面板是用Activity的Context调用WindowManager.addView添加的,窗口类型虽然写的是TYPE_APPLICATION_OVERLAY,但token来源是Activity。WMS在绑定可见性时,把窗口的可见性绑定到了这个Activity的ActivityRecord上。所以Activity一进后台,窗口就被连坐了。
这里有个很多人会忽略的点:窗口类型不等于窗口的宿主绑定关系。同一套代码里,即使用了TYPE_APPLICATION_OVERLAY类型,如果context来自Activity且addView时没有显式指定独立的application token,系统处理时仍会把窗口归属到那个Activity名下。Winscope窗口树上的挂载关系,直接反映的就是这个归属。分析时要先看挂载关系,再看窗口类型,顺序不能反。
3.4 修改方案与验证
修复思路是让悬浮面板不再依赖Activity生命周期,改成通过前台Service来承载悬浮窗。
具体改法可以这样做的点:
- 新建一个前台Service,保证进程状态有效。
- 悬浮面板的addView操作改用Service的Context,并显式使用TYPE_APPLICATION_OVERLAY类型。
- Activity切到后台时Service继续运行,不销毁面板。
- 同时检查并持有SYSTEM_ALERT_WINDOW权限,防止被系统策略拦在“op not visible”上。
改完再抓一次trace,这次面板窗口挂在application token下,而不是Activity节点下。点击Home键后,Activity状态变化不再影响面板窗口,状态行变成isVisible=true,桌面上的悬浮面板也正常出现了。
这个案例值钱的不是那几行代码,而是排查思路:用Winscope定位到“什么在变化”,再顺着窗口挂载关系找到“谁决定了变化”,最后用代码结构解除绑定关系。
4. 排查“Invisible due to”的常用套路与速查
4.1 从trace文本到代码的翻译表
为了快速对应,我把常见原因整理成一张对照表。这张表是排查起点,不是终点的结论。比如看到“app not visible”,只把方向定在Activity状态还不够,Activity为什么不可见,还得继续追一级:是被不透明Activity遮挡,还是被系统强制stop,又或者是进后台了。方向定了,后面才谈得上排查效率。
| Winscope显示的原因 | 系统里谁在判断 | 优先排查方向 |
|---|---|---|
| app not visible | ATMS中ActivityRecord.visible | Activity生命周期、当前可见Activity栈 |
| view not visible | WindowState中mViewVisibility | 顶层View的VISIBLE/INVISIBLE/GONE |
| task not visible | Task节点的visible状态 | Task是否在前台、分屏或多窗口覆盖 |
| wm dieing | WindowState的销毁状态 | removeView时机、窗口泄漏、进程被杀 |
| removed | WindowState被移除 | 窗口是否已清理、trace回放时间点 |
| parent not visible | 父WindowState可见性 | 父窗口状态、Activity状态、视图层级 |
| not visible by policy | 系统窗口策略 | 系统UI模式、锁屏、分屏、特殊显示模式 |
| op not visible | AppOpsManager授权 | SYSTEM_ALERT_WINDOW等权限授权 |
4.2 我常用的排查命令和过滤方式
Winscope本身已经很直观,但命令行快速过滤也有妙用。抓不到trace或trace损坏时,可以直接读当前系统窗口状态:
adb shell dumpsys window windows | grep -i "invisible due to"不同Android版本的dumpsys输出格式差别很大,有的版本写“invisible due to”,有的版本只输出“visible=false”加reason编号。这个命令的意义不是给出标准答案,而是快速找到当前所有不可见窗口里有没有目标窗口。
想看Activity状态时候可以用:
adb shell dumpsys activity activities | grep -E "mVisible|state=|ResumedActivity"查目标App处于哪个生命周期状态。如果App已经在后台,输出里通常能看到stopped状态。还有一步很有用:把窗口树和Activity状态放在一起对比。Winscope的日志导出功能可以导出某个时间点的窗口树详情,把它和同一时刻的Activity状态对应起来,就形成了一张完整的现场图。
4.3 一个容易被忽略的坑:多层嵌套窗口可见性
窗口可见性会往上传导。父窗口不可见,子窗口必不可见;Task不可见,所有Activity窗口不可见;Activity的visible为false,挂在该Activity上的普通窗口全部不可见。这个传导关系看似简单,但在真实场景里很容易“误伤”。
遇到过一个例子:App里的输入法窗口无论在哪个页面都弹不出来,排查半天以为是输入法设置问题,最后在Winscope里发现输入法窗口挂在一个不可见的Activity下,导致整个子窗口的可见性被压制。这个Activity其实已经被另一个不透明Activity盖住,在ATMS里的visible已经是false,输入法窗口就被判了不可见。
所以我的习惯是:不论目标窗口的“Invisible due to”写的是什么,都先把窗口树往根节点方向完整走一遍。也就是说,先看窗口树顶层所有显式Task和Activity节点的visible状态,再自上而下逐层核对。很多问题根本不在目标窗口这一层。
5. 一些更深入的个人经验
5.1 版本差异带来的“同词不同义”
Android 10、11、12、13、14的WMS代码在窗口可见性判断上有不少变化。“Invisible due to”后面的原因,在不同版本里的取值集合和文本描述不完全一致。有的版本把“App未可见”写成“app not visible”,有的版本则用数字枚举表示原因,再由Winscope前端转译一次。
所以在论坛或源码里搜到一个原因,不要默认它跟你当前系统版本完全等价。最好在自己使用的SDK版本里,打开WindowState.java或对应的协议定义文件,核对legacy字符串和枚举值的对应关系。系统性地认识一遍之后,排查时才不会“看原因对不上号”。
5.2 trace与dumpsys的交叉验证
Winscope的好处是能看到状态变化的时间线,坏处是它依赖trace录制的精度。如果某些事件没被记录下来,你可能只看到结果状态,看不到变化过程。这时用dumpsys即时抓一份当前系统窗口状态,能补上Winscope缺失的“现场”。反过来,dumpsys只能看当前瞬间,状态一闪而过就来不及了,而Winscope的时间线可以反复回放。
我自己常用的组合拳是:先看Winscope里状态变化的起始点,再用dumpsys确认当前实时的可见性原因,两者对上之后,再去代码里找根因。如果两个工具显示的原因不一致,通常说明系统处于动态变化中,或者有窗口在快速切换可见性。这种场景优先相信Winscope的时间线,因为它能逐帧解释变化过程。
5.3 给窗口相关开发调试留好“后手”
给做窗口类功能开发的朋友一个建议:在代码里把窗口的添加时机、类型、所属Context来源、顶层View可见性这几个关键字段打点到日志里。出问题时,先看自己的日志确认这几个字段,再对比Winscope里的窗口属性,很快就能区分“代码层设置问题”和“系统层状态问题”。打点时不涉及具体业务内容,只记录技术字段,排查用起来非常顺手。
另外,测试窗口类功能时,建议手动开关一次屏幕旋转,再切换一次前后台。这两个操作会把窗口可见性计算路径里的大部分分支触发一遍,很多偶现的不可见问题,都能通过这些触发点稳定复现。窗口可见性逻辑的边界条件,比普通UI生命周期容易踩得多。
在Winscope里看窗口状态这么久,越看越觉得“Invisible due to”这类字段,实际上是系统给开发者留的线索。它不只是一个状态,更是一个指向问题源头的箭头。我自己排查窗口问题时,习惯先把窗口树切到时间线模式,找到可见性翻转的那一帧,再看同一时间点系统发生了什么变化——是Activity退到后台了,还是某个窗口被remove了。把“状态”和“事件”对上号,问题基本就浮出水面了。如果你手头正好有一个闷闷不乐的窗口,打开trace找一找它身后那个“due to”,读懂了,大概率离修复就不远了。