Android 悬浮窗藏不住自己:一个标志位与 140 毫秒

Android 悬浮窗藏不住自己:一个标志位与 140 毫秒

结论先给。 如果你的 Android 应用既要开 SYSTEM_ALERT_WINDOW、又要是 AccessibilityService(也就是用一个悬浮层当界面、靠无障碍读屏点屏),那么这层悬浮窗会同时污染四条彼此独立的通道:读树、注入手势、截屏、以及折叠之后的时序。而常见的那个修法------IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS------在你自己的服务 上恰好无效,因为无障碍配置里的 flagIncludeNotImportantViews 会把它抵消掉。真正能把覆盖层从自己的树里摘出去的,是 API 34 才有的 setAccessibilityDataSensitive。

这篇把四条通道逐个拆开,给症状、给代码、给代价。

四条通道,四种不同的坏法

通道 症状 关键判据
读(节点树) 大脑拿到的元素清单是我们自己的按钮 只在活动窗口给出 0 个有用节点时才发生
点(注入) dispatchGesture 报 completed: true,屏幕没反应 不可触摸 ≠ 不遮挡
截(显示捕获) 截图里有我们 覆盖层天然进入 display capture
折(时序) 折叠完成了,树还是 0 个节点 遮挡由系统在一两帧后重算

这四条没有共同解。第 1、3 条靠"隐藏",第 2 条靠"移出屏幕",第 4 条只能靠"等"。

通道一:覆盖层被读回自己

触发条件很窄,但撞上就很难查:只有 活动窗口的 nodes_useful 为 0 时,代码才会去扫全屏其它窗口兜底。而"活动窗口一个节点都没有"的界面,恰好就是小程序、WebView、游戏画布------也就是最需要兜底的那一类。

在这些界面上,全屏唯一「有用节点数 > 0」的树,经常是我们自己的工具栏。兜底逻辑是对的,数据源是脏的:它老老实实挑了内容最多的那个窗口,而那个窗口是我们。

同一根因的第二个症状更尴尬:设置面板必须是可聚焦的(否则里面的 EditText 收不到输入法),一旦它浮在前面,rootInActiveWindow 就是我们。此时 typeText 走 findFocus(FOCUS_INPUT),拿到的是设置面板自己的输入框------大脑的下一句话被打进了「语音服务地址」。

值得强调的一点:触摸链路本身是干净的。 派发手势前所有覆盖层都被降成不可触摸,注入的点击一定穿到下面那个真实 App。所以问题不在"点错了地方",而在"瞄准了一个属于我们的控件,然后点到了它下面的内容"。这个区别决定了修法:不能写成「球那个矩形内拒绝点击」------球下面压着的 App 内容本来就应该能点,且确实点得通,那样的规则会把正确点击一起拒掉。修在元素交给大脑的那一刻。

通道二:为什么 NO_HIDE_DESCENDANTS 不起作用

第一版就是这么写的,NO_HIDE_DESCENDANTS(连子树一起摘,而不是只摘根)。它不生效的原因不在这个 flag,在我们的服务配置:

xml 复制代码
android:accessibilityFlags="flagDefault|flagIncludeNotImportantViews|..."

flagIncludeNotImportantViews 会让 View.includeForAccessibility() 无条件返回 true,无视 importantForAccessibility。结果就是「对所有服务隐藏,除了最需要这个标志起作用的那一个」------正好反了。

实测过一次,不抽象:保险箱表单打开时取树,返回的是表单本身、全部标签、以及用户的手机号。

API 34 起有了正确的工具:

kotlin 复制代码
fun View.hideFromAccessibility() {
    importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
        setAccessibilityDataSensitive(View.ACCESSIBILITY_DATA_SENSITIVE_YES)
    }
}

accessibilityDataSensitive 只对没有 声明 android:isAccessibilityTool 的服务隐藏该子树。我们没声明,TalkBack 声明了。所以它比上面那个钝器更精确:屏幕阅读器仍然摸得到球,只有我们自己看不见。它沿树继承,根视图调一次即可。

通道三:不可触摸,仍然遮挡

折叠态下球只占左边一小块,为了不挡注入手势,代码把它移到屏幕外,等 200 毫秒让位移落到合成器。既然已经是 FLAG_NOT_TOUCHABLE,直觉上就安全了。

2026-10-09,一台华为手机上,支付宝把这个假设打穿:dispatchGesture 对底部每个 tab 都返回 completed: true;球窗口 frame 范围内的 tab 一个都没切,范围外的切了。包括「消息」------它落在窗口的透明区域里,离画出来的球很远,照样没切。

也就是说:平台会把手势从不可信覆盖层下方穿过标记成遮挡,支付类应用有权静默丢弃,不报错。返回值和事实在这条通道上是分叉的,这是四条里唯一一条你无法靠读返回值发现的。

通道四:折叠只是请求

updateViewLayout 只是提交请求,遮挡由系统稍后重算。在这之前,卡片后面 App 的每个节点都报 isVisibleToUser == false,而 buildNode 在计数前就会丢掉这些节点------于是从一个完全健康的屏幕上读出 nodes_total = 0。

实测数字:卡片展开时,Chrome 的窗口存在、是活动窗口、有焦点、是根节点,节点数 0;折回去,同一屏 75 个。折叠本身几毫秒返回,系统认账要 4 次轮询。等待实现是轮询而不是加长 sleep,理由和输入回读一样:主线程空闲时几十毫秒,忙的时候长得多,而每条工具调用都付最坏情况是一笔对所有命令征的税。预算 600 毫秒、间隔 30 毫秒,超时也不抛------dump 自己会把原因说出来。

截屏是同一件事的镜像:覆盖层本来就在 display capture 里,所以截图前先把窗口与光晕设成不可见,等 80 毫秒让参数变更和背景模糊一起生效。有一个例外刻意保留:点击标记不隐藏。它是大脑唯一应该看到的自己------一个猜出来的坐标需要它变回屏幕上的位置。

判定:0 个节点有四种原因

这是这套东西真正的复杂度来源。四种原因,四种处置,而用户看到的只有一句提示:

  1. 自己的全屏卡片盖住了 ------ 折起来;
  2. 卡片正在折叠、系统还没认 ------ 再等;
  3. 无障碍服务真的僵了 ------ 让用户重开;
  4. 这个 App 自己在拒绝所有客户端 ------ 什么也别做。

判错的代价很具体:我们曾经把第 4 种判成第 3 种,用户被支去拨了一个本来完全正常的权限。所以现在第 1、2 种各带自己的标记,第 2 种用计数器而不是布尔------保险箱会开在设置面板上面,一层不够,得数层数。

代价

API 34 以下 ,accessibilityDataSensitive 不存在,只能退回旧的两道判据(扫窗口 + 空树),注释里的原话是「仅仅不完美,而不是坏的」。

同一段代码在低版本上还有一个副作用 :NO_HIDE_DESCENDANTS 会把覆盖层对所有无障碍服务藏起来,屏幕阅读器也够不到球和设置面板了。这是实打实的无障碍回退,接受它的前提是这是个只有一颗球和六个按钮的单用途应用。

要记得关的地方是 22 处。 现在 14 个界面类各自调 hideFromAccessibility(),共 22 个调用点;配对记账 11 处、10 个类。只要有一处忘了调,那个界面就会把手机号暴露给我们自己。这种"每个新界面都要记得做一次"的约定,本质上是靠人,不是靠编译器。

版本 0.12.1 / versionCode 25。覆盖层 3938 行、无障碍 3742 行、派发 2257 行;专门处理这件事的那个文件 136 行------它不加任何能力,只是让这个应用承认一件事:界面盖在屏幕上,就一定会被自己读到。

相关推荐
传奇开心果18 分钟前
【Flutter踩坑排雷】第5课:扩展方法冲突坑|同名扩展引发编译歧义,故障规避手段
android·flutter
.痞老板.3 小时前
mysql初阶--数据类型
android·数据库·mysql
StarsGreen3 小时前
Android 启动流程详解
android·linux·单片机·嵌入式硬件
booboosui21 小时前
Android 上来电怎么判:7 个条件加 5 个中文词
android
MardaWang1 天前
App 内嵌 H5 的信任边界设计(ArkWeb/HarmonyOS 实战)
android
石头慢慢说1 天前
不写 XPath,让 AI 在真机上跑测试:Appium MCP 全流程实战
android
90后的晨仔1 天前
解决 Android 构建报错:Unable to delete file .../file-map.txt
android·前端
mg6681 天前
安卓玩机工具----安卓手机变成桌面电脑!多窗口镜像+音频投射+游戏操控,无需 Root
android·智能手机·电脑
peakmain91 天前
异常处理 - Native 层的崩溃捕获机制及实现 (一)
android·性能优化