前一阵调试自己写的一个剪贴板监听工具,在 MIUI 上折腾得够呛。App 切到后台回一条微信消息,再切回来,Logcat 里只剩一行 Force stopping,进程直接没了。一开始我以为是自己 Service 的生命周期管理写得不对,翻来覆去找不到 bug。后来换了个思路,把核心逻辑挂到无障碍服务上,同一台手机、同一个包,后台挂一整个晚上都没有被杀。这篇文章就是这次实践的完整复盘,从系统杀进程的底层逻辑讲起,把无障碍服务用于保活的原理、工程配置、完整代码和厂商 ROM 适配细节全部拆开。如果你也在做自动化工具、输入法、剪贴板增强这类需要后台常驻的 App,这篇文章应该能帮你少走不少弯路。
1. 后台服务为什么活不过一顿午饭时间,问题未必出在你的代码上
1.1 系统判定进程生死的真正标准:不是“前台/后台”,而是进程优先级
很多刚接触 Android 的同学会有个直觉:只要我在代码里 startService,App 就能一直在后台跑。这个直觉放在十年前勉强成立,放在今天基本是错的。
Android 系统对进程的回收从来不看“这个 App 是不是正在前台”,它看的是一个内部的 oom_adj 分数。这个分数大致对应进程的重要程度:前台 Activity 所属的进程分数最低、最不容易被杀;被用户主动打开过的空进程分数最高、内存一紧张就被清掉。普通的后台 Service 进程,优先级排在“可见进程”之后、“空进程”之前,内存不够时依然处于比较靠前的淘汰序列。
用大白话说:你以为 Service 是“保命符”,其实系统只把它当成一个“稍后一点再杀”的缓冲。
这还没完。从 Android 6.0 开始引入 Doze 和 App Standby,到 Android 9 的 App Compatibility Changes,再到 Android 12 的 ForegroundServiceStartNotAllowedException,系统对后台的收紧是一步一步递进的。一个后台 App 在 Doze 模式下连网络访问都可能被限制,更不用说让你堂而皇之驻留在内存里。写代码的人如果不理解这一整套生命周期约束,写出来的保活方案自然一被系统遇到就崩盘。
1.2 厂商 ROM 的补刀逻辑:省电策略比原生系统更激进
原生 Android 的杀进程逻辑已经够复杂了,国内厂商还在它的基础上加了一层自己的“后台清理”策略。
MIUI、EMUI/HarmonyOS、ColorOS、OriginOS、Flyme 这些定制系统都有自己的异常耗电检测和一键加速模块。它们判断一个 App 该不该清掉,依据的是用户使用习惯、锁屏后的耗电曲线、自启动关联启动次数这些“社会性”指标,而不是单纯的系统内存压力。所以经常出现这样的情况:手机内存还剩好几个 GB,后台 App 照样被清理掉,因为厂商认为“它耗电了”“它弹通知了”“它在后台干了不该干的事”。
这一层杀逻辑对普通开发者的影响是致命的。你按照官方文档老老实实写了 START_STICKY、加了前台服务、申请了通知权限,结果用户锁屏半小时后,应用依然消失得无影无踪。我调试过一台 ColorOS 机器,即使给 App 加了电池白名单,系统加速球一键清理之后,服务和它所在的应用进程照样被整组清掉。原因很简单:厂商的清理器会跳过用户手动加保护的应用,但它判断的标准往往是“用户是否在最近任务列表里给这个 App 上了锁”,跟代码里的 Service 标志关系不大。
1.3 无障碍服务为什么能挤进“存活名单”
在这么多层限制下,为什么无障碍服务还能成为保活的突破口?
关键在于它的运行机制和普通 Service 完全不同。无障碍服务不是由应用自身启动的,它由系统进程在启动完成后主动绑定,并且全局的 AccessibilityManagerService 会持有这个绑定的状态。用户一旦在系统设置里打开了某个无障碍服务的开关,系统就会认为“这是一个用户有意开启的、在全局范围内提供能力的系统级组件”,所以它会被当成系统辅助功能的一部分来对待。
这个身份带来的直接好处有两个:
- 进程优先级显著高于普通后台进程,在系统内存压力下被杀顺序非常靠后;
- 厂商的一键清理、自动清理策略通常不会动无障碍服务,因为一旦误杀,系统辅助功能(比如 TalkBack、第三方自动化工具)会崩溃,用户第一反应是“系统坏了”,而不是“App 该被清理”。
我在多台不同厂商的机器上观察过,只要界面刷新到无障碍列表里能看到这个服务开着,即使 App 在最近任务列表里被滑掉,服务所在进程大概率也能继续留在后台。而一个常规的 Service,在最激进的清理策略下连 5 分钟都撑不过去。
注意:无障碍服务更准确的说法是“大幅提升存活概率”,不是“绝对不死”。我后面会专门讲它照样会挂掉的场景。
2. 接入前必须先搭好的三件套:manifest声明、XML配置、权限闭环
2.1 Manifest 中声明服务,这几个属性少写一个都起不来
把无障碍服务接入工程,第一步是在 AndroidManifest.xml 里注册服务。这一步看起来简单,但有三个容易踩坑的地方。
<application> <!-- 其他配置 --> <service android:name=".core.KeepAliveAccessibilityService" android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE" android:exported="false" android:label="@string/accessibility_service_label" android:process=":accessibility"> <intent-filter> <action android:name="android.accessibilityservice.AccessibilityService" /> </intent-filter> <meta-data android:name="android.accessibilityservice" android:resource="@xml/accessibility_service_config" /> </service> </application>第一个是android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"。这个权限是系统进程用来绑定服务的,必须声明,漏掉的话系统会直接拒绝绑定,服务永远进不了 onServiceConnected。
第二个是 intent-filter 里的 action。很多从普通 Service 转过来的同学会想当然写成别的值,实际上必须精确写android.accessibilityservice.AccessibilityService,这是系统注册无障碍服务的唯一入口。
第三个是android:exported。从 Android 12(API 31)开始,四大组件如果定义了 intent-filter 却不显式声明 exported,会导致应用无法安装或崩溃。为了安全,这里统一写false就可以了,因为系统进程通过 BIND_ACCESSIBILITY_SERVICE 权限就能完成绑定,不需要被外部应用显式唤起。
2.2 accessibility_service_config.xml 逐项解读:每个字段背后的系统逻辑
注册服务时必须通过 meta-data 指向一份 XML 配置文件,这份文件决定了服务的行为边界。我贴一份实际工程里一直在用的完整配置:
<?xml version="1.0" encoding="utf-8"?> <accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged" android:accessibilityFeedbackType="feedbackGeneric" android:accessibilityFlags="flagDefault|flagReportViewIds" android:canPerformGestures="true" android:canRetrieveWindowContent="true" android:description="@string/accessibility_service_description" android:notificationTimeout="100" android:settingsActivity="com.example.app.ui.AccessibilitySettingsActivity" />下面逐项解释,这些经验基本是文档里没有的:
accessibilityEventTypes:服务需要监听哪些无障碍事件。常用的有typeWindowStateChanged(窗口状态变化)和typeWindowContentChanged(窗口内容变化)。很多自动化脚本只需要这两个事件就够了。如果你在代码里通过 serviceInfo 动态修改事件类型,也记得用AccessibilityEvent.TYPES_ALL_MASK去替代“全部监听”,但这会带来额外功耗,建议按需配置。
accessibilityFeedbackType:反馈类型。一般写feedbackGeneric或feedbackVisual。这不影响后台保活,但系统会据此判断服务的辅助能力。注意,在部分 ROM 上,如果这个值为 0,系统可能把服务判定为“无有效反馈”,导致在无障碍列表里显示异常。
canRetrieveWindowContent:是否允许读取界面节点树。如果你只是保活用,这个可以不写;但如果你后面要做自动点击、读取界面字段,必须写true。我建议一开始就打开,避免后续功能扩展时又去改配置。
canPerformGestures:是否允许模拟手势(全局点击、滑动、长按)。从 Android 7.0 开始支持,Android 10 之后还可以通过 dispatchGesture 直接下发手势,是自动化操作的基础能力。
notificationTimeout:事件通知的时间窗。单位是毫秒,表示系统在多少毫秒内批量发送一次事件。如果业务需要快速响应,设置 50~100 比较合理;如果只是保活兜底,设 200 甚至更高也行。注意,值设太小会频繁唤醒进程,增加耗电,反而容易被厂商盯上。
accessibilityFlags:一些进阶能力开关。flagReportViewIds可以拿到每个节点的 viewIdResourceName,对 UI 自动化定位非常有用。flagRetrieveInteractiveWindows能获取当前窗口层级信息,Android 10 之后要读取窗口内容时经常需要;flagIncludeNotImportantViews则会让系统把一些原本被标记为不重要视图的节点也返回出来。这些 flag 不要一次性全开,按实际需求配置就好,否则事件洪峰能把你的回调函数冲垮。
settingsActivity:可选字段。如果想让用户在无障碍设置列表里点进 App 的详情页,配置一个 Activity 全类名即可。用户看到的不再是“无法打开详情”,体验会好很多。
2.3 不同 Android 版本的适配差异,直接决定服务能不能正常绑定
无障碍服务是个老接口,但从 Android 10 到 Android 14,行为变化并不少:
Android 10(API 29):新增了手势派发的dispatchGesture能力,对canPerformGestures的要求更严,如果你的 targetSdk 是 29 及以上,模拟手势时必须检查GestureDescription的合法性。
Android 11(API 30):系统开始只允许包可见性范围内的应用读取部分无障碍信息,服务声明里最好加上<queries>或在canRetrieveWindowContent下保证只读自己需要的内容。
Android 12(API 31):组件必须显式声明exported,否则无法安装。同时,AccessibilityService连接后的首次绑定位置会有变化,务必在onServiceConnected里做一次日志输出确认。
Android 13(API 33)及以后:部分厂商对“无障碍服务用于自动化”有明显收紧趋势,应用市场审核时也会对无障碍权限的使用目的做更严格审查。工程上必须写好description,明确告诉用户服务用途,否则上架审核会卡。
3. 完整服务代码与存活检测实现:绑定、事件回调、状态检查
3.1 服务主体代码:从绑定成功到事件回调的完整实现
我平时用 Kotlin 开发,这里也给出 Kotlin 版本的实现。这个类本身并不复杂,真正容易出问题的是绑定成功之后你做了什么。
package com.example.app.core import android.accessibilityservice.AccessibilityService import android.accessibilityservice.AccessibilityServiceInfo import android.accessibilityservice.GestureDescription import android.content.Context import android.content.Intent import android.graphics.Path import android.os.Build import android.provider.Settings import android.util.Log import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityNodeInfo class KeepAliveAccessibilityService : AccessibilityService() { companion object { private const val TAG = "KeepAliveA11y" @Volatile var isRunning = false private set } override fun onServiceConnected() { super.onServiceConnected() isRunning = true Log.d(TAG, "onServiceConnected: 无障碍服务已绑定成功") // 动态调整配置:这里把事件类型设为全量,并按需修改通知超时 serviceInfo = serviceInfo.apply { eventTypes = AccessibilityEvent.TYPES_ALL_MASK feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC notificationTimeout = 100 } } override fun onAccessibilityEvent(event: AccessibilityEvent?) { if (event == null) return when (event.eventType) { AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED -> { val className = event.className?.toString() ?: "unknown" val packageName = event.packageName?.toString() ?: "unknown" Log.d(TAG, "窗口变化: $packageName / $className") // 在这里识别当前窗口,按需触发自己的业务逻辑 } AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED -> { // 内容变化事件会比较频繁,这里注意做节流 // 不要每次都去拉取节点树,否则性能会很难看 } } } override fun onInterrupt() { Log.d(TAG, "onInterrupt: 服务被系统中断") isRunning = false } override fun onDestroy() { super.onDestroy() isRunning = false Log.d(TAG, "onDestroy: 服务销毁") } /** * 示例:查找当前窗口里包含指定文本的节点并执行点击。 * 返回 true 表示成功找到了节点并触发了点击。 */ fun clickNodeByText(text: String): Boolean { val root = rootInActiveWindow ?: return false val nodes = root.findAccessibilityNodeInfosByText(text) if (nodes.isNotEmpty()) { val target = nodes[0] if (target.isVisibleToUser && target.isEnabled) { val bounds = android.graphics.Rect() target.getBoundsInScreen(bounds) return performGestureAt(bounds.centerX(), bounds.centerY()) } } return false } /** * 示例:在指定坐标触发一次点击手势。 */ fun performGestureAt(x: Int, y: Int): Boolean { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return false val path = Path().apply { moveTo(x.toFloat(), y.toFloat()) } val gesture = GestureDescription.Builder() .addStroke(GestureDescription.StrokeDescription(path, 0, 50)) .build() return dispatchGesture(gesture, null, null) } }这里有几个我踩过之后才明白的点:
serviceInfo = serviceInfo.apply { ... }这种写法是允许的。无障碍服务的配置既可以在 XML 里静态写,也可以在运行期动态覆盖。但要注意,动态设置必须发生在 onServiceConnected 之后,否则不生效。
rootInActiveWindow是读取当前活动窗口根节点的主要入口。在 Android 11 之后,如果目标 App 的包名不在你的 queries 声明里,root 可能返回 null 或者节点信息不完整。所以,如果要做跨 App 自动化,建议在 manifest 里补充相关的<queries>声明。
事件回调里不要做耗时操作。onAccessibilityEvent运行在服务的回调线程,如果你在里面解析大节点树或者做网络请求,非常容易引起 ANR。正确的做法是只做轻量判断,把重逻辑丢到 Handler 子线程或协程里。
3.2 判断服务是否还活着的工具函数:用 AccessibilityManager 查询
接入无障碍服务之后,最常遇到的一个需求是:App 主界面怎么判断这个服务到底开没开?用户以为开了,实际没开;或者服务被系统重启,状态还没刷新。
我提供一个通用的工具函数,放在任意类里都可以直接用:
object AccessibilityUtils { /** * 判断指定的无障碍服务当前是否已启用。 */ fun isAccessibilityServiceEnabled( context: Context, serviceClass: Class<*> ): Boolean { val am = context.getSystemService(Context.ACCESSIBILITY_SERVICE) as? AccessibilityManager ?: return false val expected = ComponentName(context, serviceClass) val enabledServices = am.getEnabledAccessibilityServiceList( AccessibilityServiceInfo.FEEDBACK_ALL_MASK ) return enabledServices.any { info -> info.resolveInfo.serviceInfo.packageName == expected.packageName && info.resolveInfo.serviceInfo.name == expected.className } } }这段逻辑的核心是利用AccessibilityManager.getEnabledAccessibilityServiceList()拿到当前所有已启用的无障碍服务列表,再去匹配目标服务的 componentName。注意要使用FEEDBACK_ALL_MASK作为过滤参数,这样才不会漏掉只声明了部分反馈类型的服务。
另一个细节:resolveInfo.serviceInfo.name拿到的类名可能是简写,也可能是全限定名,实际对比时建议统一用Class.getName()的结果,也就是带包名的完整类名。否则会出现“明明服务开着,检测结果却是 false”的诡异 bug。
3.3 一键跳转无障碍设置页,降低用户操作成本的正确姿势
用户开启无障碍服务需要经历“设置 -> 无障碍 -> 已安装的服务 -> 打开开关”这个长流程。体验做得好的 App,都会提供一键跳转按钮。
fun gotoAccessibilitySettings(context: Context) { val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) try { context.startActivity(intent) } catch (e: Exception) { // 极少部分 ROM 可能屏蔽了这个 action,退而求其次跳到系统设置页 val fallback = Intent(Settings.ACTION_SETTINGS) fallback.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) context.startActivity(fallback) } }有些 ROM 对ACTION_ACCESSIBILITY_SETTINGS的处理并不一致,尤其是小众定制系统,可能出现跳转后页面空白的情况。所以上面这段代码里我保留了 Fallback 到ACTION_SETTINGS的逻辑,配合桌面端引导提示,基本能覆盖绝大多数机器。
还有一个为自己留后路的细节:如果 App 自己不希望被用户在无障碍设置列表里看到入口以外的东西,可以给settingsActivity配置一个专门的说明页面,在页面里放上“为什么需要无障碍服务”“使用场景是什么”的说明。这既是为了符合厂商审核规范,也是减少用户困惑、提高开启率的重要手段。
4. 换到小米、华为、OPPO、vivo 上实测,活下来的概率大不一样
4.1 不同 ROM 的后台清理策略对比:一张表格说清楚
我把这段时间在各品牌机器上的实测情况整理成表格,注意这里说的都是“默认设置,未手动加锁”的前提。
| ROM | 普通 Service 表现 | 无障碍服务表现 | 需要注意的额外设置 |
|---|---|---|---|
| MIUI/HyperOS | 锁屏后 10~30 分钟内被清理 | 基本稳定,可跨夜存活 | 需要在最近任务列表给 App 上锁,否则用户手动清理时可能连坐 |
| EMUI/HarmonyOS | 几分钟到几小时不等,波动大 | 相对稳定,但系统更新后偶发失效 | 需要在“应用启动管理”中设为手动管理,关掉“自动管理” |
| ColorOS / OriginOS | 频繁被清,清理器策略激进 | 比较稳定,偶发被杀 | 需要在最近任务下拉菜单里点击锁定图标 |
| OriginOS | 系统省电策略较猛 | 稳定度不错 | 建议加入“电池优化”白名单 |
| Flyme | 后台限制严格 | 稳定度尚可 | 需要在设置里打开“允许后台运行” |
上面这张表反映的是我个人的实测经验,不代表每个包名、每个系统版本都完全一样。比如 MIUI 在某些版本上,用户如果主动点击“一键清理”,即使开了无障碍的服务也可能被整组清掉,但正常锁屏挂机不会死。这个“手动清掉”的行为,单靠代码层面很难完全规避。
4.2 要一起设置的系统选项:电池白名单、自启动、最近任务加锁
如果你追求的是“尽可能常驻”,光开无障碍服务是不够的,还需要引导用户做以下三项设置。这三个设置对最终效果的影响甚至可能超过无障碍服务本身。
第一,电池优化白名单。在 Android 原生系统里,开发者可以通过ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS引导用户把 App 加入忽略电池优化的名单。不同厂商对这个权限的处理不一样,但大部分都能接受一次弹窗引导。代码是这样写的:
fun requestIgnoreBatteryOptimizations(context: Context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { val pm = context.getPackageManager() val hasPermission = pm.checkPermission( Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, context.packageName ) == PackageManager.PERMISSION_GRANTED if (hasPermission) { val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) intent.data = Uri.parse("package:${context.packageName}") context.startActivity(intent) } } }第二,自启动权限。国内多数 ROM 默认禁止第三方应用自启动,这会导致手机重启后无障碍服务虽然还处于“已开启”状态,但应用进程没法自动拉起来。不同 ROM 的自启动设置入口五花八门,代码层面只能通过跳转到对应的设置页引导用户手动允许。这个环节逃不掉,必须做。
第三,最近任务锁。MIUI 和 ColorOS 都支持在最近任务页下滑或点击锁定图标,锁定后的应用不会被一键清理和滑动清理杀掉。这个操作逻辑上只影响“用户主动清理”的场景,但对保活稳定性非常关键。
4.3 force-stop 重启悖论:被手动停止后,任何代码都救不回来
这是我踩过最大的坑,也是很多人对无障碍保活产生误解的地方。
用户如果通过“设置 -> 应用 -> 强行停止”来杀掉 App,系统会给这个应用打上FLAG_STOPPED状态。处于这个状态的应用,既不会接收广播,也不会被任何定时任务拉起,连无障碍服务都会被系统自动关闭。只有用户下一次手动点击打开 App 图标,系统才会解除这个状态,并在用户重新开启无障碍服务后才能恢复。
换句话说,代码层面的保活手段,对“强行停止”这个操作是无解的。这也是 Android 系统刻意设计的安全边界:用户显式选择停止的应用,谁都不能在后台复活。
所以,如果你的产品目标是“绝对不被停止”,我只能说这条路在系统层面就走不通。无障碍服务能解决的是“后台挂机不被自动清理”的问题,而不是“用户主动杀死后我还能复活”的问题。
5. 无障碍服务也会挂,只是概率低很多:常见死亡场景与应对策略
5.1 哪些场景下服务照样会死:四个主要原因
即使在配置得当的情况下,无障碍服务依然可能失效。以下四种场景是我在实际使用中遇到的,按出现频率排序:
第一,应用被“强力清理”。部分 ROM 的“一键加速”或安全中心的深度清理,会把目标进程连同整个任务栈一起杀掉。虽然不是每次都发生,但在用户主动操作清理的情况下,触发概率会明显上升。
第二,系统更新或重启。手机系统升级后,无障碍服务的配置可能被系统重新初始化,用户在设置里的开关状态被重置。小米和华为都出现过这种情况,系统大版本升级后一大批自动化工具的无障碍服务全部变成关闭状态。
第三,应用自身 OOM。无障碍服务能提高存活概率,但如果你在服务里做了一些非常吃内存的操作,比如频繁加载大图片、缓存超大对象,进程依然可能因为内存压力被 LMK(Low Memory Killer)干掉。这个锅不能甩给系统,是你自己把进程搞大的。
第四,用户主动关闭。有些用户对无障碍权限非常敏感,看到系统提示“该服务可以监控你的操作”就直接关了。这种情况代码层面拦不住,只能在服务和说明文案里尽量降低疑虑。
5.2 提升存活率的辅助手段:前台服务、通知、WorkManager
有了无障碍服务基础,还可以叠加一些常规手段把存活率往上再推一档。
我推荐叠加的是一个长期运行的前台服务。让 App 在后台时启动一个带常驻通知的前台服务(Foreground Service),再和无障碍服务一起跑。前台服务本身有较高的进程优先级,加上无障碍系统进程的额外加分,两个机制互相补充之后,绝大多数正常使用场景下进程都不会被轻易回收。
如果你担心前台服务的通知太显眼,可以把通知渠道优先级设为IMPORTANCE_MIN或者IMPORTANCE_LOW。但注意,从 Android 13 开始,发送通知需要动态申请POST_NOTIFICATIONS权限,如果用户拒绝授权,前台服务通知只有一个默认展示空间,不会被折叠,这点需要在产品层面做好预期管理。
还有一个工具是 WorkManager。它适合做“服务挂掉之后的任务补偿”,不适合做保活本身。比如你可以每隔一段时间通过 WorkManager 检查一次无障碍服务状态,如果发现服务已经关闭,就在通知栏弹一条提醒,引导用户重新去设置里打开。这样至少能保证用户知情,而不是糊里糊涂地发现服务失效了。
5.3 被系统回收后的自动恢复方案:能自动拉起来,但要“见好就收”
如果进程是被普通后台清理(不是强停),系统实际上会触发一次START_STICKY重启服务的机会。无障碍服务被回收后,系统会在合适时机重新绑定它,隐私合规上也比较容易通过。但前提是进程至少有一次被成功绑定。
如果进程被清理后系统一直没有自动重启,可以监控ACTION_USER_PRESENT、ACTION_BOOT_COMPLETED等广播。这些广播在进程存活时可以收到,但如果进程已经死了,广播同样收不到。所以这种方案的作用是“服务还活着但暂时空闲时兜底”,而不是“死后复活”。
真正唯一可见的自动恢复手段,是依赖无障碍服务被系统重新绑定的机制。实测中,只要用户没有手动关闭服务、也没有强行停止应用,系统在内存压力缓解后很大概率会在几分钟到几十分钟内重新拉起进程,并再次调用onServiceConnected。我的一次测试中,进程被 LMK 清掉后,过了 3 分钟左右系统自动完成了重新绑定,服务又回来了。
6. 别只拿无障碍服务当保活工具,它本来就该干这些自动化的事
6.1 从事件监听开始:自动点击、自动填写、自动切换的几种基础玩法
如果说保活只是把无障碍服务“物尽其用”的第一步,那么真正值得花时间研究的是它原生的自动化能力。一个已经常驻在后台的无障碍服务,完全可以帮你做很多事情。
最简单的场景是事件监听加窗口识别。利用TYPE_WINDOW_STATE_CHANGED事件,你可以知道当前屏幕是不是某个 App 的某个 Activity;配合rootInActiveWindow,还能拿到界面上的按钮、输入框、列表项。比如一个自动签到工具,可以监听目标 App 的前台入口,等到进入签到页面后自动查找“签到”按钮并触发点击。
更复杂一点的是全局手势模拟。利用dispatchGesture,可以在任意界面上执行点击、滑动、长按,实现类似“自动下翻页面”“自动播放下一集”“自动清除推送角标”的能力。手势模拟在 Android 7.0 以上才支持,目标设备如果系统版本太低,这部分功能要做好降级处理。
再进一步,可以做跨 App 的数据流转。无障碍服务可以读取窗口节点信息,比如拿到页面上显示的验证码、金额、订单号,然后自动填入下一个应用。这种能力解决了不少传统 API 无法覆盖的跨应用场景,但也要小心:读取别的应用内容涉及用户隐私,如果产品没有清晰的场景和用户授权,很容易触碰合规红线。
6.2 隐私边界与合规:无障碍权限是把双刃剑,别把它用歪了
关于无障碍权限的合规要求,我必须多说几句。
从应用市场的审核标准看,如果 App 要申请无障碍权限,必须在隐私政策里明确说明使用目的、使用场景和信息处理方式。很多应用市场对滥用无障碍权限的行为已经建立了自动检测机制,比如检测到服务名称含 “auto”“click”“wechat” 等敏感关键字,就会被直接拒绝上架或下架处理。
从用户角度出发,无障碍权限能读取屏幕上的几乎所有内容。用户对你的信任是非常脆弱的,如果 App 在隐私政策里含糊其辞,或者在运行过程中偷偷读取不该读的界面信息,一旦被用户发现,口碑崩坏的速度会非常快。我建议在代码注释和产品文案里都保持克制:只申请对核心功能必要的权限,只读取当前业务需要的事件类型,不要偷偷把所有事件都抓一遍。
从实现角度讲,你完全可以在onAccessibilityEvent里对包名做一层白名单过滤,只处理目标包名的事件,其他包名直接 return。这既减少了性能消耗,也让用户更容易认可你的权限使用行为。
我在实际使用中的一个习惯是,把所有不必要的accessibilityFlags全部关掉,只保留业务最低要求的那几个。既能减少隐私争议,又能降低系统的压力,还能让服务更稳定。
这套方案做完之后,我最直观的感受就是:后台服务的活着,终于不用再靠运气了。但也要清醒地认识到,无障碍服务保活不是银弹,它只是让进程在系统的层层策略里获得了更高的优先级。你仍然需要关注厂商更新带来的变化、用户主动清理导致的失效,以及最重要的——不要把这套能力用在不该用的地方。如果你也正在做需要后台常驻的自动化工具,我建议先把事件监听的最小闭环跑通,再加保活,再加上层自动化逻辑,一步一步来,每一步的稳定性都会比一次性铺开写更踏实。