简介:面向Android开发者的一份无障碍服务(AccessibilityService)快速开发库源码包,解决从零配置服务、重复编写事件监听与节点操作逻辑的痛点。通过一行代码即可启用服务,适合需要实现自动点击、滑动、界面遍历等复杂自动化业务,以及辅助工具和自动化测试场景的开发者参考学习。资源共67个文件,压缩包仅223KB,其中Java源码承载服务核心逻辑与Abllib封装,XML文件用于清单声明、布局与无障碍服务配置,PNG图片以界面截图和流程图示为主,另含Gradle构建脚本、ProGuard混淆规则及gradle wrapper,结构清晰便于直接导入工程或抽取改造。资源已有4162人学习下载,从中可获取库的整体封装思路、事件分发与节点匹配的写法,以及如何将常用操作链封装成便捷API;结合源码目录和示例,可降低无障碍服务开发门槛,提升自动化业务落地效率。
开头
做Android自动化的同学,对AccessibilityService基本又爱又恨。爱的是它是正规军,不需要root权限就能做很多页面上能做的操作,抓节点、模拟点击、读取内容都没问题;恨的是接入流程太繁琐,清单文件要配、xml要写、服务要注册、还要在系统设置里手动开启,再加上事件回调、节点查找、权限判断这一套组合拳下来,光是把“能用”的架子搭起来就要折腾大半天,更别说真正去写业务逻辑了。
我之前在几个项目里反复踩过这套流程的坑,后来干脆把公共部分抽成了一个无障碍服务库,把“注册服务、检查状态、跳转授权、事件分发、节点查找、模拟操作”全部封装好,业务侧只需要一行代码就能启动服务,然后直接在业务代码里写自动操作逻辑,什么自动打卡、批量处理、页面巡检都能快速实现。这篇就详细聊聊这个库的设计思路、核心API、直接可用的接入步骤,以及我在真机调试和线上反馈中踩过的那些坑。
1. 为什么要把无障碍服务封装成一行代码的库
1.1 无障碍服务开发到底难在哪
先说原生开发的问题。AccessibilityService看起来只是继承一个Service,但真正跑起来要过好几道关卡。
第一道是配置关卡。你需要在Manifest里声明Service,加上android.permission.BIND_ACCESSIBILITY_SERVICE权限,指定intent-filter,再引入一个meta-data指向res/xml下的配置文件。这个xml里有一堆属性要理解,比如accessibilityEventTypes控制你监听哪些事件,accessibilityFlags控制你能否读取窗口内容,canRetrieveWindowContent决定你能不能拿到节点树。随便漏一个,服务能启动但就是拿不到东西。
第二道是授权关卡。服务写好了不会自动生效,用户必须去系统设置里手动打开。问题是你不知道用户什么时候打开,也不知道他打没打开。想跳转过去,不同品牌的设置页路径还不太一样,很多国产rom还会做二次确认或者后台限制,兼容处理本身就是一块工作量。
第三道是事件分发关卡。你写的自动操作不是广播式的,是事件驱动的。用户在界面上的每一个变化都会触发onAccessibilityEvent,你需要在里面做类型过滤、包名过滤,然后分发给对应的业务handler。这块不封装好,代码会越写越乱,事件多了还会卡顿。
第四道是节点操作关卡。找按钮、找文本、判断是否可点击、执行点击、输入文字、滚动列表,这些单拎出来代码量不大,但组合在一起就非常容易出错。比如findAccessibilityNodeInfosByText找出来一个不可点击的父节点,你点下去没反应;又比如节点还没加载出来就去找,返回空列表。
这些痛点叠在一起,会导致一个结果:写功能的时间远少于配环境的时间,而且每换一台新机型,环境部分的bug还会重新冒出来。
1.2 封装方案要解决的核心问题
所以这个库的核心目标就三个:
- 把初始化压到一行代码。业务方不需要碰
AccessibilityService生命周期,也不需要关心xml配置和权限声明,库内部通过Manifest占位符和代码检测自动处理。 - 把事件分发做成开箱即用。业务方只需注册监听器或者继承一个暴露了回调的服务基类,收到统一封装后的
WindowEvent对象,里面带包名、类名、事件类型、根节点等关键信息。 - 把节点查找和操作做成高内聚API。查找、点击、输入、滑动、等待、断言,全部封装成同步方法,内部做好轮询等待和空值判断,让业务代码看起来像脚本语言一样简单。
这样设计之后,你在业务里基本不会感觉到自己是在跟一个系统服务打交道,而是像在操作一个页面机器人。日常开发效率能提升一个量级。
2. 项目的整体架构与核心设计
2.1 模块划分
这个库我分成了四层:
- 服务层:真正的
AccessibilityService实现,负责绑定系统、接收事件、执行手势。对外暴露getInstance()和全局操作的静态方法。 - 配置层:用注解或Builder模式接收业务方的服务类、事件类型、超时时间等参数,自动生成并注入xml配置。
- 事件分发层:一个线程安全的分发器,把系统事件过滤后转成统一结构,同步或异步回调给业务方。同时维护一个“当前窗口状态”缓存,业务方随时可以查询。
- 操作层:封装节点查找、点击、滑动、文本输入、等待、返回等所有原子操作,以及串行执行“查找-操作-验证”这种组合动作。
这四层各自职责独立,服务层不关心业务逻辑,操作层不关心事件来源,业务方也不需要知道服务层细节。依赖方向是单向的:配置层 → 服务层 → 事件分发层 → 操作层。
2.2 一行代码启用的实现思路
“一行代码启用”的原理其实不复杂,主要是把这四件事封装进一个方法里:
AutoAccessibility.init(this, AutoAccessibilityService::class.java)这个方法内部依次完成:
- 分析目标Service类上的注解,读取监听事件类型、是否需要读取窗口内容等配置。
- 检查系统里是否已经开启该服务,判断逻辑是读取
Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES字段,匹配包名/服务类名。 - 如果没开启,跳转系统无障碍设置页,同时注册一个
AccessibilityServiceStateObserver,轮询或者监听Uri变化来感知开启状态。 - 如果已开启,直接回调
onReady,通知业务方服务已可操作。
这里有一个容易被忽略的细节:配置信息其实不需要写成xml文件再让系统读。AccessibilityServiceInfo在运行时可以通过setAccessibilityEventTypes()、setFlags()、setCanRetrieveWindowContent()等等方法动态装配。用动态装配的好处是配置集中在一个地方,而且方便按不同环境做差异化调整。但注意,动态配置必须写在onServiceConnected()里,并且在调用setServiceInfo()之后生效。
关于判断服务是否开启,网上很多方案是读ENABLED_ACCESSIBILITY_SERVICES字符串。这个字段在大部分机型上都能用,但三星部分系统版本和服务多开时会出现截断。更稳妥的方式是配合AccessibilityManager.getEnabledAccessibilityServiceList(),遍历已开启的服务列表做精确类名比对,再配合字符串字段双保险。
2.3 事件分发为什么不能直接回调
刚开始我做事件分发时,直接把onAccessibilityEvent回调给业务方。结果发现一个问题:业务方拿到事件后根本不知道当前处于哪个页面,尤其在Activity跳转过程中,窗口状态变化非常频繁,回调里充满了无效事件。
后来我改成把事件先经过一层状态机处理。状态机维护三个字段:当前前台应用包名、当前Activity类名、当前窗口根节点缓存。事件到达后,先把这些字段更新掉,再判断事件类型和包名是否命中业务方注册的过滤器。只有命中才回调。这样业务方收到回调时,直接getRootNode()拿到的就是真实可用的当前页面节点,不用自己再去判断一遍。
还有一点是节流。TYPE_WINDOW_CONTENT_CHANGED事件非常密集,如果你在onAccessibilityEvent里做耗时操作或者频繁拉根节点,很容易卡顿掉帧。我在状态机里加了一个通知间隔字段,默认100ms,也就是一秒钟最多分发10次内容变化事件。窗口状态变化事件优先级最高,不参与节流。
3. 快速接入:一行代码启用完整配置
3.1 Gradle依赖与基本配置
这个库发布到Maven Central,接入方式和普通库一样:
implementation 'com.yourgroup:auto-accessibility:1.0.0'需要关心的是minSdkVersion,建议21以上。因为21以下系统的节点API和手势API差异很大,维护成本高,现在15以上的项目占比已经非常高了,没必要为了老版本拖累自己。
Manifest不需要手动注册Service,库的AAR里已经声明了Service组件,并且用占位符${accessibilityServiceClass}标记了类名。Gradle编译时通过ManifestPlaceholder动态替换,把业务方的服务类全路径注入进去。这个方案要注意一点:一个库里只能注册一个AccessibilityService,如果将来产品里需要同时存在“辅助自动操作”和“OCR取词”等多个可独立开关的无障碍能力,就要做一个代理模式,用一个总服务分发到多个能力模块。
3.2 业务侧一行代码接入
接入流程分两步。第一步,在Application的onCreate()里初始化:
AutoAccessibility.init(application, AutoAccessibilityService::class.java)第二步,在MainActivity的onResume()里做一次状态确认,未开启则引导用户去设置页:
override fun onResume() { super.onResume() if (!AutoAccessibility.isServiceEnabled()) { AutoAccessibility.startSettingsActivity(this) } }init()方法内部会自动处理这种场景:如果直接调用时服务还没开启,会先跳设置页,用户返回后库内部会重新检查状态并通过回调通知。所以你只需要关心界面上的入口提示,不需要写状态轮询。
补充一个细节:跳转系统无障碍设置页不要用隐式Intent直接startActivity,因为部分国产rom会把Settings.ACTION_ACCESSIBILITY_SETTINGS劫持或者不响应。我在库内部做了一个三级降级方案:先尝试打开具体品牌的无障碍设置页(比如ColorOS、MIUI、EMUI各自的路由),失败再回落到系统标准页面,最终兜底打开应用详情页里面手动去引导。这个跳转兼容处理是真正在几十台真机上测出来的,直接复用比自己写稳。
3.3 无障碍服务自身的配置
写业务前还必须搞清楚AccessibilityService的xml配置,不然服务开了也拿不到节点。
我常用的配置如下:
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged" android:accessibilityFeedbackType="feedbackGeneric" android:accessibilityFlags="flagDefault|flagRetrieveInteractiveWindows|flagIncludeNotImportantViews" android:canRetrieveWindowContent="true" android:notificationTimeout="100" android:description="@string/accessibility_desc" />关键属性逐一说明:
canRetrieveWindowContent是重中之重。不设这个,getRootInActiveWindow()永远返回null。accessibilityEventTypes建议只监听窗口状态变化和内容变化。不用监听触摸事件,开启typeTouchExploration会增加系统负担,而且不是自动操作必需。flagRetrieveInteractiveWindows和flagIncludeNotImportantViews一定要加。前者让Windows层面的不可点击窗口也能被检索,后者让一些标记为不重要(比如纯装饰的view)的节点也能进入节点树。不加这两个flag,你会遇到“明明界面上有按钮,findAccessibilityNodeInfosByText就是找不到”的情况。notificationTimeout表示两次事件回调的最小间隔,单位毫秒。设100比较平衡,太短频繁回调,太长对内容变化响应慢。
4. 核心API与自动操作实战解析
4.1 节点查找的几种方式
节点查找是自动操作的地基,所有的点击、输入都是先找到节点再操作。库内提供了三类查找方法:
// 按文本查找,支持模糊匹配 val nodes = AutoAccessibility.findNodesByText("打卡") // 按ViewId查找,适合原生控件固定id的场景 val node = AutoAccessibility.findNodeById("com.xxx:id/btn_confirm") // 按ContentDescription查找,适合无文本纯图标按钮 val node = AutoAccessibility.findNodeByDesc("返回")这三个方法内部都实现了等待机制:默认最多轮询3秒,间隔100ms,直到找到目标节点或者超时。为什么必须加等待?因为页面切换和列表刷新是异步的,你点击了一个按钮,下一个页面的节点可能几十毫秒后才出现在树里。不加等待直接查,结果就是你踩到“偶发找不到节点”的坑。
还有一点经验:不要只找一个节点就觉得万事大吉。当一个页面存在多个匹配节点时,findAccessibilityNodeInfosByText返回的列表顺序在部分机型上不稳定。所以库内提供了一个findNodeByTextAndClickable方法,优先返回可点击的那个节点。我遇到过一个真实案例,自动打卡按钮在小屏手机上折叠进了“更多”菜单,普通文本查找会同时命中菜单标题和折叠选项,导致误点。最终策略是:如果当前页面有多个匹配项,先看有没有可点击的,没有再检查是否在折叠菜单里,需要就先展开再找。
4.2 执行操作的封装
找到节点后,执行操作是另一大块。我封装了以下常用动作:
// 点击节点 AutoAccessibility.clickNode(node) // 长按节点 AutoAccessibility.longClickNode(node) // 向输入框设置文本 AutoAccessibility.setText(node, "你好") // 按坐标点击,适合页面无节点但知道位置的情况 AutoAccessibility.clickPoint(x, y) // 滑动 AutoAccessibility.swipe(x1, y1, x2, y2, 300) // 模拟返回键 AutoAccessibility.back() // 打开通知栏 AutoAccessibility.openNotificationBar()每个方法内部都做了能力判断和兜底。比如clickNode里会先isClickable()判断,如果目标节点本身不可点击,就往上找父节点或者子节点中可点击的。这里多说一句为什么要往上找父节点:很多自定义控件的点击事件注册在父容器而不是TextView上,你按文本找到了TextView,直接对它执行ACTION_CLICK是没反应的。这是新手最容易踩的坑之一。
4.3 业务编排与防重复
自动操作业务通常是一个流水线:打开App → 等待页面加载 → 查找按钮 → 点击 → 验证结果。这个流程如果写成一坨顺序代码,看起来很直白,但实际跑起来会有很多意外。
我在这个库上推荐一种“阶段 + 超时 + 验证”的编排方式:
val result = AutoAccessibility.performSequence( // 第一阶段:打开App,等待主页面 openApp("com.example.app"), waitNode("首页标识"), // 第二阶段:点击进入打卡页 clickNodeByText("打卡"), waitNode("确认打卡"), // 第三阶段:执行打卡并验证 clickNodeByText("确认打卡"), verifyNode("打卡成功") )这里每一个步骤都是库封装的Action对象,内部统一处理了等待、重试和异常。整个sequence是串行执行的,中间任何一步失败都会返回失败步骤名称和原因,不会继续往下跑。这样既保证了业务流程可控,也方便排查问题。
防重复也是自动操作业务的老大难。比如用户手动点到一半,脚本又跑了一遍,就可能重复提交。我提供了一套全局幂等方案:在业务里定义一个标志位,比如打卡页面有个“今日已打卡”的节点,如果有就停止执行,不继续往下走。这个逻辑在verifyNode里可以显式声明为“如果命中则跳过后续步骤”,比起你去拿SharedPreferences记录状态要可靠得多,因为它看的是页面真实状态。
5. 实战案例:自动打卡业务复现
拿一个最简单的自动打卡业务为例,完整过一遍这个库的用法。假设目标App里有一个打卡入口,点击后进入打卡页,页面上有一个“立即打卡”按钮,打卡成功会出现“今日打卡完成”的toast或文本。
首先,业务服务类继承库的基类,并按需重写事件回调:
class AutoAccessibilityService : BaseAutoService() { override fun onWindowChanged(packageName: String, className: String, root: AutoNode?) { // 在这里做页面级状态判断,比如记录当前处于哪个页面 super.onWindowChanged(packageName, className, root) } fun doClockIn() { // 打开目标App val opened = AutoAccessibility.openApp("com.example.workapp") if (!opened) return // 点击首页的“打卡”入口,等待二级页面出现 val clicked = AutoAccessibility.clickNodeByText("打卡", waitTimeout = 3000L) if (!clicked) { log("未找到打卡入口") return } // 等待“立即打卡”节点出现,点击后验证结果 val done = AutoAccessibility.sequence { step { clickNodeByText("立即打卡") } step { waitNodeByText("今日打卡完成", timeout = 3000L) } }.call() if (done) notifyUser("打卡成功") } }注意openApp()这个封装:它通过Intent拉起目标App的启动Activity,如果App已经在前台则跳过。但有些目标App检测到外部启动会走冷启动流程,反而更慢。实测中,在脚本开头加一个“如果已经在目标页面就跳过打开”的判断,能让整体耗时下降30%左右。
这里还有个非常常见的问题:目标App的“打卡”按钮出现在WebView或者Flutter渲染的页面上,通过文本查找往往找不到。因为WebView里的节点默认不会被findAccessibilityNodeInfosByText遍历到,Flutter更是只有语义节点才可见。遇到这种场景就需要退化为坐标点击。我在库内提供了findNodeByText和clickPoint结合的模式:先尝试找节点,找不到就根据屏幕尺寸比例预估按钮位置。这个预估位置需要真机测试校准,不同屏幕比例差异还是很大的,建议用运行时获取的WindowManager宽高计算百分比坐标,而不是写死像素值。
6. 常见问题与排查技巧实录
6.1 服务已开启但收不到事件
这是打开这个功能后最迷的一个问题。表现是:设置里明明显示服务已开启,但业务侧没有任何回调,抓根节点也是null。
排查顺序如下:
- 先确认Manifest里Service有没有注册成功。用
adb shell dumpsys package 包名去查,看Service Resolver Table里有没有目标Service,以及android.permission.BIND_ACCESSIBILITY_SERVICE权限是否带上了。 - 确认xml配置里
canRetrieveWindowContent="true"。这条在运行时动态装配时很容易漏。 - 排除系统安全软件的“省电策略”限制。部分国产rom会在App长时间后台运行时,把无障碍服务给杀掉或者冻结,表现就是设置里还显示“开启”,但实际已经收不到事件了。解决办法:把你的App加入系统的“自启动白名单”、“后台运行白名单”、“电池不优化白名单”。这个一定要在用户引导页里写清楚。
6.2 跳转系统设置页失败或闪退
不同品牌的Android系统设置App包名和Activity路径差异很大,比如部分系统的无障碍设置页从Android 12开始改名了。如果直接跳Settings.ACTION_ACCESSIBILITY_SETTINGS,部分旧机型会弹“未找到处理该Intent的Activity”。
我的降级方案已经在章节3.2提过了,这里补充一个细节:跳转之前先查一下系统版本,Android 11及以上优先尝试Settings.Panel.ACTION_INTERNET_CONNECTIVITY这种分段面板的兄弟字段——Settings.Panel里其实有ACTION_ACCESSIBILITY这个常量,效果是直接弹出无障碍设置的面板对话框,比跳一整个设置页更聚焦,用户体验更好。
6.3 节点就是找不到
如果findAccessibilityNodeInfosByText返回空,不要急着认为是代码问题,先打开系统的“开发者选项 → 显示布局边界”看一眼。如果按钮区域没有边界线,说明目标控件可能不是原生View绘制的,或者页面整体是用SurfaceView/TextureView渲染的视频流。
遇到这种情况,能用的手段很少。常见的方案是:
- 开启
flagIncludeNotImportantViews后再刷新一次节点树,可能就出来了。 - 把查找从
getRootInActiveWindow()切到getWindows()根窗口列表,因为部分弹窗和对话框挂载在不同窗口上。 - 如果在WebView里,尝试用
AccessibilityNodeInfo的ACTION_ACCESSIBILITY_FOCUS和ACTION_SET_TEXT做组合操作。
6.4 频繁回调导致卡顿
这是个容易忽略的性能坑。无障碍服务跑在系统进程的Binder线程上,你如果在onAccessibilityEvent里直接做耗时同步操作,比如访问网络、读写数据库、遍历大量节点,会拖慢整个系统的无障碍事件分发,表现出来就是系统层面掉帧、触摸响应变慢。
我的处理方式是两段式:第一段只做轻量过滤和状态更新,约几毫秒;第二段把真正需要业务处理的逻辑通过Handler切到后台线程执行。节点查找和点击操作都建议用独立线程去跑,不要在系统回调线程里执行。
7. 兼容性、性能与合规思考
无障碍服务能做很多普通API做不到的事情,也正因为如此,各大应用市场对上架应用的审核越来越严格。经验之谈是:如果你的App并不需要从系统全局读取窗口内容,就不要申请canRetrieveWindowContent,即使这个功能很强,也会让用户和审核方都怀疑你的动机。这个库在初始化时允许按需关闭节点读取能力,只用“辅助点击”这类不读取内容的场景就不开。
性能和功耗方面,自动操作业务通常不是持续运行的,而是用户触发一次跑一次流程。这一点在设计上要注意:业务流程执行完,主动调用AutoAccessibility.release()释放短暂持有的线程池和缓存,避免库一直占用系统资源。我在库内部还加了一个空闲自动回收机制,如果连续30秒没有任何事件和操作指令,就自动清理缓存节点树和操作队列,只保留服务本身。实测这个设计能把常驻内存占用控制在比较低的水平。
另外,多个自动操作同时触发的问题也要考虑。我的方案是给库加了一把全局互斥锁,同一时间只有一个业务序列在执行。如果用户连续点击了好几次“开始打卡”,实际上只有第一个序列完整跑完,后面的序列会被通知“已有任务在执行”。这个互斥锁的粒度是序列级的,如果业务方想在内部做更小粒度的并发控制,库也提供了withLock这种局部锁方法。
最后分享一点个人体会
无障碍服务这个领域,文档少、坑多、还常年不被重视。把它封装成库之后,我最大的感受是:业务开发同学终于不用再去研究系统服务的配置和机型差异了,他们只需要理解“找节点、点节点、等节点”这九个字。但作为库的维护者,恰恰是那些“不用研究”的细节决定了这个库的上限——事件分发是否快、节点查找是否稳、跳转设置是否兼容、执行完是否回收资源,这些才是真正值得打磨的地方。如果你们正在做类似的需求,建议先把这一层基础打牢,后面加业务会轻松非常多。
本文还有配套的精品资源,点击获取