AccessibilityService 的滥用检测,现在的限制有多严

AccessibilityService 的滥用检测,现在的限制有多严

AccessibilityService 的滥用检测,现在的限制有多严


AccessibilityService 的滥用检测,现在的限制有多严


从一次上架被拒说起


去年帮一个做效率工具的朋友处理 Google Play 上架问题,应用核心功能是自动填充表单,底层依赖 AccessibilityService 监听窗口变化。代码在 Android 10 上跑得稳稳当当,结果 targetSdk 升到 33 之后,Google Play 的审核直接给了一记 rejection,理由写得明明白白:"The app uses AccessibilityService in a way that is not intended for users with disabilities."


这已经不是第一次听到这种拒审理由,但让我意外的是,这次连申诉通道都变了。以前还能在 Play Console 里扯几句"我们的功能确实帮助了视障用户",现在系统直接弹出一个表单,要求你上传无障碍功能演示视频,还要证明你的目标用户群体包含残障人士。更狠的是,Google 开始用机器学习扫描 AccessibilityService 的调用模式,不是人工审,是机器先过一遍。


我那个朋友的应用,AccessibilityService 里注册了 TYPE_WINDOW_CONTENT_CHANGEDTYPE_VIEW_CLICKED 两种事件,代码大概长这样:


override fun onAccessibilityEvent(event: AccessibilityEvent) {
    when (event.eventType) {
        AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED,
        AccessibilityEvent.TYPE_VIEW_CLICKED -> {
            // 遍历节点找 EditText
            event.source?.let { root ->
                findEditableNodes(root)
            }
        }
    }
}

机器审核的判定逻辑后来从一位 Googler 的公开回复里拼凑出来:如果你的 service 大量调用 getSource() 并且伴随 performAction(),尤其是 ACTION_SET_TEXT 这类写操作,系统会标记为"高风险模式"。我们的应用恰好两条都占,因为自动填充必然要读节点再写内容。


Android 12 的权限分水岭


真正让事情变复杂的不是 Google Play 的政策,是 Android 12(API 31)引入的 android:canRequestTouchExplorationModeandroid:canRetrieveWindowContent 这两个属性的强制声明要求。以前写 AccessibilityService,manifest 里声明个 service 再贴个 intent-filter 就完事,现在系统会检查你的 accessibilityServiceFlags 是否和实际行为匹配。


我在 Pixel 6 上测过,Android 12 之前,用户开启服务时系统只弹一次确认框。Android 12 之后,如果你的 service 声明了 FLAG_RETRIEVE_INTERACTIVE_WINDOWS,系统会额外弹一层警告,告诉用户这个应用能读取屏幕上的所有内容。更麻烦的是,这个警告在 Android 13 上变成了"非可关闭"的,用户必须等 10 秒才能点确认,而且每次更新 APK 签名变化后,权限会被重置。


有个细节很多人没注意到:Android 12 的 AccessibilityServiceInfo 里加了 FLAG_INCLUDE_NOT_IMPORTANT_VIEWS 的隐式限制。以前这个 flag 可以拿到所有节点,包括 isImportantForAccessibility=false 的,现在系统会过滤掉一部分,而且过滤规则没有文档,只能通过源码反推。我在 AOSP 的 AccessibilityInteractionController.java 里找到相关逻辑,发现系统会根据窗口的 accessibilityImportance 属性做分层,某些 SurfaceView 和 TextureView 的内容直接被屏蔽。


这直接导致一个后果:如果你的应用依赖 WebView 里的内容,Android 12 之后 AccessibilityNodeInfo 的遍历深度被限制在 50 层,超过直接返回 null。这个 50 层的限制写在 ViewRootImpl.javaMAX_RECURSION_DEPTH 常量里,但不同厂商的 ROM 会改这个值。我在三星 Galaxy S22 上测出来是 30 层,小米 13 上是 40 层,华为 Mate 50 上干脆把这个限制和"纯净模式"绑定,开启后 AccessibilityService 对第三方应用完全失效。


Android 13 的"非残疾用户"判定


Android 13(API 33)的 AccessibilityManager.isEnabled() 行为没变,但 Google Play Protect 在云端加了一层校验。2023 年 3 月的某次 Play Services 更新后,我注意到 logcat 里多了这样的输出:


W GooglePlayServices: AccessibilityService com.example.app/.AutoFillService blocked by policy check

这个 policy check 不是本地逻辑,是 Play Services 定期从服务器拉的黑名单规则。规则文件路径在 /data/data/com.google.android.gms/shared_prefs/ 下面,加密存储,但可以通过抓包看到更新频率——大约每 48 小时一次。规则里匹配的是包名 + service 类名的组合,也有按行为模式匹配的,比如"在 1 分钟内触发超过 100 次 performAction"。


我做过一个实验:写一个纯本地测试的 AccessibilityService,不联网、不上架,就在自己手机上跑。包名是随机生成的,没有历史记录。结果运行 3 天后,logcat 里出现了同样的 blocked by policy check 警告,service 被强制停止,而且系统设置里的开关被置灰,显示"此服务已被系统管理员禁用"。


这说明 Google 的判定已经不依赖应用是否来自 Play Store,而是基于设备端的机器学习模型。模型的输入特征可能包括:service 的存活时长、事件触发频率、节点遍历的广度、是否调用 takeScreenshot()(Android 13 新 API,但仅限声明了 CAPABILITY_TAKE_SCREENSHOT 的 service)、是否调用 gestureSwipe() 等。


Android 13 还引入了一个很隐蔽的变更:AccessibilityService.onGesture() 的回调频率被限制。以前做手势模拟可以连续调用,现在系统会丢事件,logcat 里报 Dropping gesture event due to rate limit。这个限制没有文档,我通过 systrace 抓到的间隔大约是 200ms 一次,但不同设备差异很大。Pixel 上严格执行,三星上似乎宽松一些,OV 系的部分机型干脆没做这个限制——这也导致依赖手势模拟的自动化脚本在跨设备测试时完全不可控。


国内厂商的二次加码


Google 的限制已经够头疼,国内厂商的定制 ROM 又加了一层。小米的 MIUI 从 12.5 开始,AccessibilityService 的开启流程里插了一个"风险检测",如果应用不是来自小米应用商店,会弹全屏警告"该应用可能窃取您的隐私信息",用户需要点三次"仍要开启"才能进入系统设置界面。这个警告的 Activity 是 com.miui.securitycenter.accessibility.AccessibilityRiskAlertActivity,可以通过反编译看到它的判定逻辑:检查应用商店的签名、检查是否有 xiaomi.market 的证书链、检查应用是否在 MIUI 的"白名单数据库"里。


华为的 HarmonyOS 2.0 之后更直接,AccessibilityService 对"非系统级应用"默认只开放只读权限。也就是说,你的 service 能收到事件,但 performAction() 返回 false,ACTION_CLICKACTION_SET_TEXT 全部失效。这个限制在 AccessibilityInteractionClient.java 里实现,华为把 AOSP 的代码改了,加了一个 isSystemApp(uid) 的判断。 workaround 是有的,但涉及到签名级别的权限,普通应用根本拿不到。


OPPO 和 vivo 的限制集中在后台存活上。ColorOS 的"电池优化"会把 AccessibilityService 的进程优先级强行降到 CACHED_EMPTY,导致 service 在锁屏 5 分钟后被杀死。这个行为和 AOSP 的 Doze 模式不同,Doze 是延迟网络请求,ColorOS 是直接杀进程。我抓过 logcat,看到 ActivityManager: Killing ... accessibility service due to inactivity 这种日志,但"inactivity"的定义是厂商私有的,AOSP 源码里找不到对应逻辑。


最麻烦的是这些限制没有统一文档,每个厂商的客服给出的答复都是"为了用户安全",具体什么规则闭口不谈。我做了一个兼容性测试矩阵,覆盖 12 个机型,同样的 AccessibilityService 代码,在不同设备上的可用功能差异巨大:


  • 节点遍历深度:30-50 层不等
  • 手势模拟频率:无限制到 500ms 间隔不等
  • 后台存活时间:锁屏即杀到 30 分钟不等
  • 截图能力:完全开放到完全禁止不等
  • 跨应用操作:允许到仅允许系统应用不等

  • 这个矩阵我维护了一年,最后放弃更新了,因为厂商的更新策略是"云端动态调整",同一个 ROM 版本,今天和明天的行为可能不一样。


    代码层面的具体踩坑


    说几个写代码时遇到的实际问题。


    第一个是 AccessibilityNodeInfo 的回收机制。Android 的 Accessibility API 要求手动调用 recycle(),但很多人忽略的是,getSource() 返回的节点和 findAccessibilityNodeInfosByViewId() 返回的列表里的节点,生命周期管理规则不同。前者绑定到事件对象,事件处理完就失效;后者需要显式 recycle。我在 Android 11 上踩过一个坑:批量处理节点时用了 use 扩展函数,结果在 Android 13 上因为节点池大小变化,出现 IllegalStateException: Already in the pool! 的崩溃。查源码发现 AccessibilityNodeInfo 的 mPoolSize 在 Android 13 从 50 改到了 20,高频回收复用触发了竞态条件。


    第二个是 TYPE_WINDOW_CONTENT_CHANGED 的节流。Android 12 之后,同一个窗口的连续内容变化事件会被系统合并,你收到的 AccessibilityEvent 可能包含多个变化的聚合结果。这个合并逻辑在 AccessibilityManagerService.java 里,默认窗口是 100ms,但某些厂商改成 500ms。如果你的应用依赖实时响应,比如抢单、抢票这类场景,这个延迟直接决定功能是否可用。我测过,Pixel 上 100ms 的窗口,实际端到端延迟(事件产生到 onAccessibilityEvent 回调)大约是 150-200ms;小米上 500ms 窗口,端到端延迟 600-800ms;华为上这个事件类型在部分应用里直接被屏蔽,回调永远收不到。


    第三个是 performAction() 的返回值。文档说返回 true 表示成功,但实际很多情况下返回 true 却什么都没发生。比如对 WebView 里的节点调用 ACTION_CLICK,Android 10 之前能点到,Android 12 之后 WebView 的渲染进程隔离加强,点击事件被路由到渲染进程后丢失,主进程的 AccessibilityService 收不到完成通知。这个 bug 在 Chromium 的 issue tracker 里有记录(crbug.com/1234567,具体编号我记不清了),但 Google 的回复是"working as intended",因为 WebView 团队认为 AccessibilityService 的模拟点击不应该绕过用户的直接交互。


    替代方案的可行性


    既然 AccessibilityService 限制越来越严,有没有替代方案?我试过几条路。


    MediaProjection + ImageReader 做截图 OCR,然后模拟点击。这条路在 Android 10 上因为 FLAG_SECURE 的广泛应用已经半残,银行类、支付类应用基本都开了这个 flag,截图黑屏。Android 12 之后 MediaProjection 的启动流程加了用户确认,每次重启后要重新授权,而且确认框不能自动点击——因为点击它的正是被限制的 AccessibilityService。


    UiAutomator 框架,Google 官方测试工具。这个框架底层也是 AccessibilityService,但运行在 shell 权限下,需要 adb shell 或者系统签名。普通应用用不了,自动化测试场景下可以,但要求设备连接电脑或者预装测试工具,完全不适合面向终端用户的应用。


    WindowManager.addView() 做悬浮窗,然后自己解析布局。这条路能拿到屏幕坐标,但拿不到 View 的语义信息。你知道有个按钮在 (100, 200),但不知道它是"提交"还是"取消"。而且 Android 12 之后悬浮窗的 TYPE_APPLICATION_OVERLAY 权限申请流程也加了限制,和 AccessibilityService 的开启确认框类似,需要用户进设置手动开。


    NotificationListenerService,监听通知变化。这个服务限制相对松,但只能拿到通知内容,拿不到应用内部界面。而且 Android 13 的通知权限 POST_NOTIFICATION 变成 runtime permission,用户可以直接拒绝应用发通知,连带影响你的监听。


    我最后采用的方案是混合策略:AccessibilityService 做轻量级事件触发,OCR 做内容识别,本地 ML 模型做语义理解,把重度的节点遍历和模拟操作全部砍掉。这样虽然功能弱了很多,但至少能过审,能在主流设备上跑起来。代价是代码复杂度翻倍,维护成本很高,而且某些场景下用户体验断崖式下降。


    一个未公开 API 的副作用


    Android 14 Beta 期间,我在源码里注意到一个未公开的方法:AccessibilityService.setServiceInfo() 的调用频率被限制。文档没提,但实际测试发现,连续调用两次会抛出 IllegalStateException: setServiceInfo called too frequently。这个限制是通过 mSetServiceInfoInterval 控制的,默认值 1000ms。


    这个限制影响的是动态调整 service 配置的场景。比如根据当前应用包名,动态开关某些事件监听,减少系统负担。以前的做法是:


    override fun onAccessibilityEvent(event: AccessibilityEvent) {
        val packageName = event.packageName?.toString()
        if (packageName == "com.target.app") {
            serviceInfo = serviceInfo.apply {
                eventTypes = AccessibilityEvent.TYPE_VIEW_CLICKED
            }
        }
    }

    现在这种动态调整如果频繁触发,会直接崩溃。workaround 是预定义几套配置,用 setServiceInfo 批量切换,而不是每次事件都改。但这个 1000ms 的间隔在 Android 14 正式版里又被取消了,只在某些 Beta 版本出现,导致测试和生产的兼容性矩阵更加混乱。


    最后说点实际的


    如果你现在还在维护依赖 AccessibilityService 的应用,我的建议是:把核心功能从"自动操作"转向"辅助提示",降低对 performAction() 的依赖。Google 的审核机器模型对"读"的容忍度明显高于"写",对"用户主动触发"的容忍度高于"后台自动执行"。


    具体操作上,把 accessibilityServiceFlags 里去掉 FLAG_REPORT_VIEW_IDSFLAG_REQUEST_ENHANCED_WEB_ACCESSIBILITY,这两个 flag 在审核标记里权重很高。事件类型尽量只注册 TYPE_WINDOW_STATE_CHANGED,避免 TYPE_WINDOW_CONTENT_CHANGED 的高频触发。节点遍历用 findAccessibilityNodeInfosByText() 代替全量递归,减少系统记录的"遍历深度"指标。


    如果业务上确实需要自动操作,考虑分拆成两个应用:一个纯提示的 AccessibilityService 过审上架,一个插件包通过侧载或者企业分发走。这个方案脏,但确实是现在部分工具类应用的生存现状。


    Android 15 的开发者预览里,我看到 AccessibilityService 的文档加了一段新说明:"Services that automate actions on behalf of the user may be subject to additional scrutiny." 措辞比 Android 14 的"should not be used for automation"又进了一步。scrutiny 的具体形式还没看到,但方向很明确。


    这条路只会越来越窄。不是技术问题,是平台治理问题。技术层面总有 workaround,但平台要封你的时候,workaround 本身也会变成封禁理由。去年有个案例,某知名自动化工具用了反射调用 IAccessibilityServiceConnection 的隐藏接口,结果被 Google Play 的签名扫描检测到,直接下架,连带着开发者账号进入观察期。


    能说的就这些。具体代码怎么写,每个应用的场景不同,但大方向是收敛,不是扩张。把功能做轻,把证据做足,把用户手动操作的痕迹留够。这是现在 AccessibilityService 能在主流渠道存活的唯一姿势。

    Scrcpy 的投屏控制,开发者调试神器 2026-06-11
    Fuchsia 系统的进度,Android 会被替代吗 2026-06-11

    评论区