ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

AutoJs源码与无障碍服务:从配置到自检的完整指南

AutoJs源码与无障碍服务:从配置到自检的完整指南

简介:面向安卓自动化初学者的AutoJs无障碍开启脚本,提供的是一个可直接运行的脚本模板,用来自动打开系统无障碍服务,解决自动化任务中因权限未开启而无法执行点击、滑动等操作的问题;这类脚本常见于辅助自动抢单、批量处理或日常手机操作等场景,适合依靠Android辅助功能实现自动化交互的开发者直接套用。核心逻辑集中在唯一的源码文件内,支持低版本AutoJs,环境要求不高,无需额外依赖,既能满足临时调用无障碍能力的需求,也便于拆解学习权限开启流程。压缩后大小仅581B,压缩包内只有1个js文件,内容精简,可无缝融入个人自动化项目,或作为排查无障碍调用问题的对照样本。目前已有872人学习下载,虽然体积极小,但结构清晰、开箱即用,对快速理解无障碍服务API写法比较有帮助。此脚本仅供学习与个人研究使用,请勿用于商业场景,并注意遵守相关平台规则。

1. AutoJs源码与开启无障碍:脚本能不能动,关键就在这一步

源码都编译出来了,装到手机上跑 Demo,结果控制台第一句就是“请先开启无障碍服务”——这是不少接触 AutoJs 源码的人遇到的第一道坎。标题里的“AutoJs源码”和“开启无障碍”其实是同一件事的两端:前者是你准备改造成自己工具的自动化框架,后者是让这套框架能真正控制界面的总开关。无障碍服务不开启,Auto.js 能启动但什么都干不了,就像引擎点着了但没挂挡。这篇文章要做的是把无障碍服务从系统机制、源码配置、脚本自检到日常排障整条链路拆开,让你拿到的不是一句“去设置里打开”,而是能自编译、能自检、能兜底的一套完整方案。适合自己改源码打包的人、写自动化脚本的开发者,以及被无障碍服务反复折腾到没脾气的老用户。

2. 无障碍服务在AutoJs源码里的调用链路:从系统API到JS桥接

2.1 无障碍服务是一根系统级别的“外接操纵杆”

无障碍服务的本质是安卓系统对外暴露的一套辅助功能接口。应用声明一个无障碍服务后,系统会往这个服务的回调里持续推送当前界面的事件,包括窗口打开、窗口内容变化、节点获得焦点等。服务拿到这些事件后,可以进一步调用系统提供的 API 去读取界面节点树、获取控件的位置和尺寸,甚至直接注入点击、长按、滑动、手势这些操作。整个过程不需要额外授权弹窗,也不需要设备处于调试状态,所以它成了很多自动化框架首选的底层通道。

Auto.js 之所以选无障碍服务而不是别的方式,核心原因是这套接口同时覆盖了信息读取和操作注入,并且适配面广。普通 App 无法读取其他应用界面,但无障碍服务可以;想让脚本知道当前按钮坐标在哪里,靠截图加 OCR 不仅慢而且受分辨率影响,直接读节点树则稳定得多,事件推送基本是实时的。Auto.js 在源码层面把这一大套系统 API 收敛成了脚本里常见的auto()、auto.waitFor()、click()、text()这些简单接口,脚本作者不需要理解 Binder 和进程通信,只管调用即可。

也正因为这是系统级能力,它的生死不归 Auto.js 主进程掌控。用户手动关闭服务、系统重启后服务没有被自动绑定、省电策略把服务进程杀掉,这些情况都会让代码里的auto.service变成 null。源码能做的只是注册这个服务并在运行时检测状态,真正决定服务是否活着的还是系统侧。把这一点想透,后面很多玄学问题就都好解释了。

2.2 源码里管无障碍的三个部分缺一不可

在常见的 AutoJs 源码工程里,与无障碍能力相关的内容主要分布在三个地方,缺一个都会导致自动化失败。

源码模块承载内容与脚本的关系
无障碍 Service 实现类真正的无障碍服务逻辑,处理系统回调与注入请求编译期注册,运行期由系统绑定
accessibility_service_config.xml告诉系统这个服务要监听什么事件、能否读取窗口、能否手势注入决定服务能力的边界,配置错了服务会显示但功能残缺
桥接与 API 封装层把 Service 能力封装成 JS 可调用的auto、click、swipe等对象脚本直接打交道的这一层

Service 实现类通常会放在源码里类似core/accessibility的目录下,作用是继承系统的无障碍服务基类,并重写onAccessibilityEvent和onInterrupt两个核心方法。前者是事件入口,Auto.js 在这里做节点缓存、事件分发;后者是服务被系统中断时的兜底回调,一般用来释放资源。脚本层面的auto()最终会通过桥接层去绑定这个 Service,绑定成功后返回一个封装好的服务对象,之后脚本每次查找控件都会通过它向系统请求节点快照。

配置 XML 是另一个容易忽略的坑。这里描述的是“这个服务想拥有哪些能力”,系统会在用户点开无障碍开关前展示给用户看,并把配置落实到运行期。如果你在自编译时把canRetrieveWindowContent设置为 false,那么即使服务开启,脚本也没法读取界面节点,只会得到一个空节点树。后面我专门列了一张参数表,逐项说明改动后果。

2.3 为什么不是 root,不是 ADB,而是无障碍

有人会问,源码都到自己手里了,直接塞 root 权限干活不是更干脆?Root 方案确实能绕过很多限制,但它的用户门槛太高,国内很多设备市场版本根本不开放 bootloader,普通用户拿到源码编译产物后装到手机上,大概率没有 root 环境。ADB 方案则需要电脑端持续连接或依赖无线调试,不适合手机作为独立工具长期运行。无障碍方案不需要越狱、不需要数据线、不需要额外守护进程,用户只需要在系统设置里点一个开关,这是它成为 AutoJs 默认操作通道的根本原因。

但这不意味着无障碍方案毫无代价。系统对无障碍服务的限制这些年一直在收紧,一部分设备上服务会进入“耗电优化名单”,屏幕关闭一段时间后服务进程被冻结;部分系统还会在用户手动停止应用时强制回收无障碍服务。因此围绕“开启无障碍”这个动作,真正重要的不只是设置里的那一下点击,而是源码配置正确、脚本检测到位、异常恢复及时这三件事都做好。

3. 自编译源码时把无障碍声明对:Manifest、配置XML与三个打包盲区

3.1 在 AndroidManifest 里注册 Service,并把绑定权限写对

如果是从官方发布包直接安装的用户,无障碍服务早就注册好了,不需要关心 Manifest。但如果你拿到了源码想改包名、改界面、精简体积,重新编译后第一步就是把无障碍服务重新声明进清单文件。常见的注册模板长这样:

<service android:name=".service.MyAccessibilityService" android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE" android:exported="false" android:label="自动化脚本辅助服务"> <intent-filter> <action android:name="android.accessibilityservice.AccessibilityService" /> </intent-filter> <meta-data android:name="android.accessibilityservice" android:resource="@xml/accessibility_service_config" /> </service>

这里最关键的一行是android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"。无障碍服务不能像普通权限那样由应用主动申请,必须声明这个绑定权限,系统才有资格在用户开启开关时把这个服务绑到自己的进程里。android:name要替换成你源码里实际的服务类完整路径,如果改了包名但忘了同步这里,编译能过,装上去之后设置页里根本看不到这个服务。

android:exported="false"看起来和直觉相反,一个系统要绑定的服务怎么不导出呢?实际上无障碍服务的绑定走的是系统内部的特殊通道,不依赖 exported 导出,保持 false 反而能减少外部应用恶意调用。如果你在自编译时把 exported 改成 true,部分系统会直接拒绝在无障碍列表里展示这个服务。这个容易踩,先记住了。

3.2 accessibility_service_config.xml 的参数决定了服务是“能用”还是“虚胖”

注册好服务之后,还要在res/xml目录下准备好配置文件,名字和 Manifest 里meta-data指定的保持一致。下面是一份适合 Auto.js 场景的最小配置:

<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged|typeWindowStateChanged" android:accessibilityFeedbackType="feedbackGeneric" android:notificationTimeout="100" android:canRetrieveWindowContent="true" android:canPerformGestures="true" android:isAccessibilityTool="true" android:description="@string/accessibility_service_description" />

逐个参数说清楚:

参数推荐值不这样配置的后果
accessibilityEventTypes至少包含typeWindowStateChanged和typeWindowContentChanged事件收不到,脚本只能用轮询方式找节点,延迟明显
accessibilityFeedbackTypefeedbackGeneric只影响系统提示形式,不配可能导致服务无法启用
notificationTimeout100事件合并窗口,值太大会让节点刷新变慢
canRetrieveWindowContenttrue不设为 true,脚本拿到的是空节点树,findOne 永远失败
canPerformGesturestrue不设为 true,gesture、swipe等接口全部失效
isAccessibilityTooltrue部分系统会让服务更不容易被省电策略杀掉,但兼容性需实测
packageNames留空留空代表监听所有应用,适合通用自动化;只填单个包名则服务只在那个应用里生效

packageNames这个参数需要多说一句。如果你只打算自动化某个特定应用,填上包名能减少系统事件推送量,服务也会更省电;但对于 Auto.js 这种通用框架,通常建议留空,否则脚本跳到其他应用时服务会处于“不监听”状态,节点查询照样失败。

3.3 自编译打包时三个最容易翻车的盲区

第一个盲区是混淆规则。源码默认不开混淆通常没事,但如果你为了缩小包体开启了混淆,一定要在 ProGuard 规则里 keep 住无障碍 Service 类以及它重写的系统回调方法。混淆之后类名和方法名被改写,而系统绑定服务时是按 Manifest 里的类名去反射加载的,类名对不上直接找不到服务,症状是设置里看不到入口。常见做法是加一条-keep class * extends android.accessibilityservice.AccessibilityService,把整个继承体系保下来。

第二个盲区是资源合并。有的源码工程里已经有一份accessibility_service_config.xml,你在做界面定制时不小心改了resConfig或清理了 “无用资源”,编译时很容易把这份配置丢掉或者被其他依赖模块的同名资源覆盖。排查方法很直接:编译产物打包后用解压工具看 APK 里的res/xml目录,确保服务配置和 Manifest 引用对得上,文件不在的话装上去根本不会出现在无障碍列表。

第三个盲区是签名问题。自编译的 APK 如果没有正确签名,部分系统会拒绝将无障碍服务识别为可绑定状态,表现就是开关能点但立刻弹回关闭状态。这不是服务代码的锅,是签名校验没过。务必在构建流程里配置好 release 签名,不要拿 debug 签名应付长期使用的设备,debug 签名在部分定制系统上同样会有兼容问题。

4. 用JS脚本拉起无障碍并自检:从auto()到运行态检测

4.1 为什么源码配好了,脚本里还得写 auto()

源码和配置文件解决的是“系统允许这个服务存在”的问题,但脚本运行又是另一回事。Auto.js 脚本运行在应用自己的进程里,而无障碍服务是系统绑定到应用进程里的一个独立组件,脚本进程不会自动持有它的引用。因此每个脚本在真正开始操作界面之前,都必须先执行一次auto(),让桥接层去获取无障碍服务实例,并把服务绑定到脚本可调用的对象上。

auto()和auto.waitFor()的差别很多新手没搞懂。auto()调用后会立刻返回,如果此时服务没开启,它可能只会弹一个提示,脚本继续往下走,等你访问节点时才报错;auto.waitFor()则会进入等待状态,在服务可用之前阻塞当前脚本。我一般建议脚本入口统一使用auto.waitFor(),避免后面所有节点操作都建立在空服务上。如果你写的是定时任务脚本,在服务未就绪时宁可卡住等待,也不要带着 null 服务跑进业务逻辑然后留下一堆找不到节点的报错。

4.2 一套可复制的服务自检最小代码

把下面这段放到你脚本的最前面,可以解决大部分“开着服务但脚本不认”的问题:

// 等待无障碍服务就绪,最多等 10 秒 auto.waitFor(); // 如果超时仍未拿到服务,主动引导用户去设置页 if (!auto.service) { toast("未检测到无障碍服务,现在为你打开设置页"); app.startActivity({ action: "android.settings.ACCESSIBILITY_SETTINGS" }); exit(); } // 确认服务不仅能拿到,而且能读取窗口内容 if (!auto.service.isAccessibilityEnabled()) { toast("无障碍服务已开启但未生效,请尝试重新开关一次"); exit(); } log("无障碍服务状态正常,脚本继续执行");

这段代码的价值不在长,而在把“服务状态”这个黑匣子显式化了。auto.waitFor()会阻塞当前脚本,默认等待时间是系统决定的下一次服务回调,但如果用户一直没有去设置页打开服务,它会一直等下去,所以要在等待之后判断auto.service是否为 null。isAccessibilityEnabled()是服务对象提供的实时状态查询方法,返回 false 代表当前虽然拿到了对象,但系统实际没把它标记为运行中,这种状态通常发生在服务被系统强制停止但应用进程还活着的时候。

跳转设置页那一段用的是标准系统 Intent,action指定为无障碍设置页。不同设备可能对入口名称有差异,但大多数系统都认这个 action。如果你的源码定制过国际化字符串,这里的 toast 文本可以换成对应语言的文案,逻辑不用动。这里有一点容易误解:auto.waitFor()等待成功并不代表服务和当前脚本处于同一个上下文,比如定时任务触发时屏幕还锁着,服务虽然活着但界面节点不可用,所以业务逻辑前还要做界面可用性检查。

4.3 场景化的状态检测:定时任务启动前与页面切换后

服务自检不只在脚本开头做一次,页面切换后也应该有个“服务仍然有效”的确认机制。比如一个脚本先切到微信,再切回桌面,再进另一个应用,中间如果服务被系统回收,auto.service的引用可能已经失效,但脚本不会主动感知。这时再调用findOne会走一个很长的超时,或者直接抛异常。

常见的做法是封装一个ensureService()工具函数,在每次关键节点操作前调用:

function ensureService() { if (auto.service && auto.service.isAccessibilityEnabled()) { return true; } log("服务已掉线,重新连接"); auto.waitFor(); return auto.service != null; } // 每次切完应用后先确认服务在线 app.startActivity({ packageName: "target.package.name" }); sleep(1200); ensureService();

注意这里的auto.waitFor()在服务掉线后重新调用,并不会自动重新连接系统侧服务,它只能等待服务重新可被获取。真正的重连动作发生在用户重新打开服务,或者系统自己恢复了服务绑定之后。所以这个函数的意义是让你尽早发现掉线,而不是帮你修好系统侧的问题。发现掉线后,要么提示用户手动处理,要么让你的脚本主动跳回设置页引导重开。

5. 开启无障碍后的五类高频问题:现象、根因与排查

5.1 设置页显示已开启,脚本却检测不到服务

现象:用户在系统设置里明明看到无障碍服务开关是绿色的,但脚本跑起来auto.service仍然是 null,或者isAccessibilityEnabled()返回 false。很多人会怀疑是源码没配好,进源码里反复检查,变量一直是好的。

原因:这类问题常见于系统重启后。手机重启时,系统恢复无障碍服务的顺序和应用自启动的顺序不一致,应用进程先被拉起来了,但服务还没来得及绑定,设置页显示的“已开启”只是保存了用户偏好,不代表服务此刻已运行。另一种原因是前置版本的应用被覆盖安装,老服务实例已失效但系统设置里残留下一次绑定状态。

解决:在代码里不要只判断一次,启动后每隔 2 秒轮询一次,最长等 10 秒。多数情况下系统会在应用进程稳定后自动重绑服务,轮询可以等到那个时机。如果一直等不到,引导用户把服务关掉再打开一次,强制系统重新执行绑定流程。源码层面也可以做一点优化:在主进程启动时主动请求服务分类,但不同系统行为差异较大,不要指望一条命令修复所有问题。

5.2 半夜屏幕熄灭后,自动任务怎么都跑不醒

现象:定时任务设置的是夜间两点执行,日志显示脚本确实被系统调度起来了,但所有节点操作全部失败,提示无障碍服务不可用。手动解锁屏幕后再点一次任务执行,又能正常完成任务。

原因:很多系统对无障碍服务有省电优化。屏幕熄灭一段时间后,系统把包含服务进程的应用放进了“待机模式”,进程还在但服务被冻结,或者应用整体被暂停,JS 常驻任务无法触发。定时任务由 JobScheduler 或系统闹钟拉起时,进程被唤醒的速度和服务恢复绑定的速度之间存在竞态窗口。

解决:在一个源码工程里,常做的方法是给应用申请“忽略电池优化”权限,同时在系统设置里把应用加入“自启动名单”和“后台运行不受限制名单”。代码层可以在任务执行前先强制点亮屏幕并解锁,让系统从待机状态完整恢复,再执行自动化操作。点亮屏幕可以调用device.wakeUpIfNeeded()之类封好的接口,如果没有现成接口就直接模拟电源键,短按点亮后再继续。

5.3 服务处于“已停止”状态,开关怎么也打不开

现象:在系统无障碍列表里,服务选项显示“已停止”或“耗电异常”,点击开关后它会立刻弹回关闭状态,甚至没有任何错误提示。重装 App 也一样,只有重启手机后才能短暂恢复。

原因:这种情况高发于系统更新之后,定制 ROM 对无障碍服务的管控策略变了,例如新系统要求无障碍服务必须在前台运行时才能绑定,或者要求服务声明新增权限字段。旧版本源码构建出的 APK 没有适配新规则,绑定请求被系统静默拒绝。

解决:先在配置 XML 里补上android:isAccessibilityTool="true",这个字段告诉系统该服务属于辅助工具,部分系统会因此走更宽松的白名单逻辑。接着检查 Manifest 里有没有申请FOREGROUND_SERVICE相关权限,并把无障碍服务进程提升为前台服务,防止被杀。如果你在源码里能定位到服务拉起逻辑,改成在前台启动时主动请求绑定,带上通知栏常驻通知,掉线概率会明显降低。

5.4 多开应用抢走了无障碍服务的绑定

现象:手机里有应用多开功能,其中一个分身装了 Auto.js,原版也装了。两个版本都出现在无障碍列表里,但只能开启一个,开启另一个时前一个自动关闭。脚本运行时的行为和预期不一致,时好时坏。

原因:部分系统的无障碍框架限制同一应用签名下只能有一个服务实例处于绑定状态。两个包名不同但服务名相同的 APK 同时注册,系统把它们视为同一服务的不同版本,互相抢占绑定。这个问题在普通安装场景下不常见,但自编译后改过包名的人很容易踩到。

解决:不要在同一个设备里保留两个自编译变体。如果必须共存,把其中一个的service类名和配置 XML 改成另一个不同值,像给服务换个“名字”一样,让系统认为这是两个完全不同的服务。改名后注意混淆规则同步更新,否则又会出现 3.3 小节里的问题。

5.5 服务在线,节点能找到,但点击和滑动没有反应

现象:日志里findOne能正常拿到控件对象,坐标也打出来了,但执行click()后界面没有任何响应,也没有报错。复杂手势gesture()有时生效有时不生效,看着非常随机。

原因:节点能读到,说明canRetrieveWindowContent没问题;点击没反应,大概率是手势注入权限没拿到,或者注入的目标窗口不是当前活跃窗口。部分系统要求无障碍服务必须同时声明canPerformGestures才能在 Android 11 以上的设备上执行手势,老版本构建产物只声明了节点读取权限,自然只能看不能摸。另一个因素是目标界面处于半透明浮层或输入法弹窗上方,注入坐标被其他窗口拦截。

解决:先确认配置 XML 里canPerformGestures是否为 true,不是就改掉并重新编译。接着检查click()的坐标点是否属于有实际点击响应的区域,比如某些控件只把“点击区域”绘制在一个小的子 View 上,直接点击整个父布局的中心点是没有效果的。更稳的做法是调用控件对象的click()而不是先拿坐标再注入,让 Auto.js 自己计算注入位置。

6. 让无障碍服务长期在线:每个早上的定时自检方案

前面所有问题都指向一个事实:无障碍服务不是一个“开一次就一劳永逸”的东西。源码配好了,用户也开启了,系统一次重启、一次省电清理、一次应用更新都可能把它从运行态里踢出去。与其等任务翻车了再去排查,不如让脚本自己承担巡检职责。这里分享一个我常用的定时自检方案:每天早上 8 点跑一个轻量脚本,检查服务状态,发现异常就跳转到设置页并弹窗提醒。代码很简单,但能把很多隐患消灭在任务高峰之前。

auto.waitFor(); // 自检重点:不只是服务对象存在,还要确认能读取窗口内容 let serviceOk = auto.service != null && auto.service.isAccessibilityEnabled(); if (serviceOk) { // 再尝试获取当前界面的根节点,能拿到才算真正可用 let root = auto.service.getRootNode(); if (root == null) { log("根节点为空,服务可能处于休眠状态"); serviceOk = false; } } if (!serviceOk) { toast("自检发现无障碍服务异常,正在打开设置页"); app.startActivity({ action: "android.settings.ACCESSIBILITY_SETTINGS" }); } else { toast("无障碍服务运行正常"); }

为什么还要额外取一次根节点?因为isAccessibilityEnabled()只代表服务被系统标记为运行中,不代表它此刻能拿到界面节点。比如屏幕处于锁屏状态下,无障碍服务虽然在线,但当前窗口属于系统锁屏,应用上下文里拿不到预期节点,取一次root就能把这层休眠状态暴露出来。getRootNode()在节点不可用时通常返回 null,不需要特殊处理。

验证这套自检是否有效,可以分三步:第一,保持服务开启,运行脚本,应输出“运行正常”且弹一次 toast;第二,手动关闭无障碍服务,再运行脚本,应看到自动跳转设置页;第三,开启服务但不解锁屏幕,执行一次定时任务,看是否能在日志里看到“根节点为空”的分支。三步都通过,就说明这个脚本确实在监控真实可用性,而不只是检查开关状态。

我自己的习惯是把这段脚本挂成每日早上的定时任务,同时把设备关闭“电池优化”。生活里总有意外,比如某次手机重启后被丢在抽屉里,服务一直没恢复,等中午我拿起来才发现定时同步全失败了。从那以后我明白了一个道理:任何依赖无障碍服务的自动化方案,都应该先假定“服务一定会掉”,再围绕这个假定设计兜底。这套自检脚本就是我的后悔药,每天花不到五秒,省去的不只是排查时间,还有任务失败后的连带补救。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进