简介:面向Android开发者的无障碍服务(AccessibilityService)入门源码,基于Java实现,适合需要快速掌握无障碍自动化、UI事件监听与节点操作的学习者。资源围绕无障碍服务核心原理,演示如何继承AccessibilityService、重写onAccessibilityEvent回调,并利用AccessibilityNodeInfo完成自动点击、获取文本、滑动屏幕等常见操作;同时也包含事件过滤、配置ServiceInfo等实践细节,可作为自动化测试或辅助功能开发的参考框架。压缩包共44个文件,大小152KB,主要包含java源码、xml资源与配置、gradle构建脚本、png图标及README说明等,结构精简,方便直接导入Android工程学习。已有2005人学习下载。通过阅读源码可掌握无障碍服务在AndroidManifest中的权限声明、系统设置中手动开启服务的完整流程,并理解事件分发与UI节点查找的实现思路,适合想从零搭建无障碍功能或做自动化操作的开发者参考。 这几年陆陆续续做了一些基于Android无障碍服务的自动化工具,从最开始一个十几行的Demo,到最后能稳定跑几个月的自动打卡脚本,中间踩过不少坑,也把无障碍服务这套源码机制翻了个底朝天。说实话,很多人对无障碍服务的认知停留在“给视障用户读屏”或者“一键抢红包”的层面,但真正看过源码、理解它的设计逻辑之后,你会发现这套机制远比想象中有意思——它是一个运行在系统层面、能“看见”所有界面元素并模拟用户操作的通道。
这篇内容我就围绕无障碍服务的源码实现,从框架原理、完整实现流程、核心源码逻辑解析到踩坑经验,一次性讲清楚,让准备入门或者已经在做相关开发的你少走弯路。
1. 无障碍服务的真实身份:不只是一个“辅助工具”
1.1 服务注册与权限的本质
无障碍服务在系统里的地位很特殊。它在AndroidManifest里注册,拥有独立的特权权限,但它不是一个普通的应用组件,更像是一个“绑定在系统窗口管理器上的观察者”。系统会把所有界面变化、焦点切换、窗口状态变更等事件,源源不断地推送给注册了对应能力的无障碍服务。
关键在于,它是以系统级服务的方式运行的,拥有比普通应用更高的界面访问权限。普通应用只能操作自己的View,而无障碍服务可以读取任何应用当前屏幕上呈现的内容,包括列表项、按钮文字、输入框内容,甚至WebView里的元素。这意味着它天然具备“跨应用操作”的能力,自动填写表单、模拟点击、拦截通知、全局悬停等,都是以这个机制为基础的。
1.2 它到底能做什么、不能做什么
无障碍服务能做的事情,主要有以下几类:
- 监听全局事件:通过
TYPE_WINDOW_STATE_CHANGED、TYPE_VIEW_CLICKED等事件感知界面变化 - 获取当前窗口的节点树:拿到
AccessibilityNodeInfo,遍历整个View层级 - 执行模拟操作:
performAction可以模拟点击、长按、滚动、设置文本 - 读取/操作全局状态:比如截屏、查找通知栏、获取当前焦点
- 悬浮窗叠加:配合
TYPE_ACCESSIBILITY_OVERLAY窗口层级,在任意应用上方绘制内容
但它也有天然的限制:不能绕过应用自身的交互逻辑。如果目标App在代码层面做了反自动化校验,比如检测辅助功能触摸、埋点记录、滑块验证,无障碍服务只能执行“真实存在”的界面操作,不能凭空创造用户身份。
理解这些边界之后,再看源码实现就清晰多了。无障碍服务本身不是“万能外挂”,它只是一个拥有全局视野的自动化执行框架,核心价值在于把你的操作意图翻译成系统可以执行的指令。
2. 源码脚手架:从配置文件到首个可运行的服务
2.1 accessibility-service配置:能力声明
写无障碍服务的第一步不是写Java代码,而是先写好两个配置文件。第一个是应用清单文件中的service声明,第二个是独立的XML能力配置。
在AndroidManifest.xml里这样声明:
<service android:name=".MyAccessibilityService" android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE" android:exported="true"> <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:exported="true"必须开,因为系统需要从外部访问到这个服务组件meta-data指向的XML文件,是真正的功能统治中心
res/xml/accessibility_service_config.xml如下:
<?xml version="1.0" encoding="utf-8"?> <accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged|typeViewClicked" android:accessibilityFeedbackType="feedbackGeneric" android:accessibilityFlags="flagDefault|flagRetrieveInteractiveWindows|flagIncludeNotImportantViews" android:canRetrieveWindowContent="true" android:description="@string/accessibility_service_description" android:notificationTimeout="100" android:settingsActivity="com.example.MainActivity" />逐个解释这里的参数含义:
accessibilityEventTypes:要监听的事件类型。typeWindowStateChanged表示窗口状态切换时回调,typeWindowContentChanged表示窗口内容变化时回调,typeViewClicked表示点击事件。canRetrieveWindowContent:核心开关,只有设置为true,才有权限读取窗口节点内容,否则白搭。accessibilityFlags:一些特殊能力标记。flagRetrieveInteractiveWindows允许获取当前交互窗口,flagIncludeNotImportantViews会把对辅助功能来说不重要的View也包含进来(比如纯装饰性的布局)。notificationTimeout:系统对事件合并处理的时间窗口,值越小回调越及时,代价是更频繁的Binder通信。
很多初级开发遇到“事件收不到”的问题,大概率是这里的配置漏了或写错了,尤其是canRetrieveWindowContent="true"这一行。
2.2 AccessibilityService核心生命周期
写完配置之后,新建一个Service类,继承AccessibilityService:
public class MyAccessibilityService extends AccessibilityService { private static MyAccessibilityService sInstance; public static MyAccessibilityService getInstance() { return sInstance; } @Override protected void onServiceConnected() { super.onServiceConnected(); // 服务连接成功时回调,说明用户已经在系统设置里开启了这个无障碍服务 sInstance = this; } @Override public void onAccessibilityEvent(AccessibilityEvent event) { // 核心回调:所有配置的事件类型都会传到这里 // 根据event.getEventType()区分不同事件再处理 } @Override public void onInterrupt() { // 服务被系统中断时回调,比如用户关闭了服务、系统资源紧张等情况 } @Override public boolean onUnbind(Intent intent) { // 服务解绑时回调,做必要的清理工作 sInstance = null; return super.onUnbind(intent); } }这里有一个非常实用的设计:通过静态实例持有服务引用。因为onAccessibilityEvent是在系统Binder线程回调的,而业务代码里经常需要从任意Activity发起“执行自动化任务”的请求,静态实例就是连接UI层和服务层的桥梁。但要注意,静态引用要记得在onUnbind里置空,否则服务意外销毁之后还持有一个失效实例,后续调用会空指针。
onServiceConnected是一个容易被忽略的时机点。系统在这个回调里已经完成了服务绑定,此时可以主动做一次初始化操作,比如准备节点查找策略、加载任务配置、启动一轮初始探索。有一些场景需要在服务启动后立即执行点击操作(比如自动打开某个App里的功能页),就可以在onServiceConnected里postDelayed一个任务。
3. 核心源码逻辑:事件分发、节点查找与模拟操作
3.1 onAccessibilityEvent事件流:别每帧都处理
无障碍服务看起来像是“全能监控”,但事件流其实非常密集。一个普通的滑动动作,会瞬间产生大量TYPE_VIEW_SCROLLED、TYPE_WINDOW_CONTENT_CHANGED事件。如果对每个事件都做完整逻辑,会直接导致卡顿,甚至触发系统的性能限制。
我的处理模式是状态过滤 + 事件上下文。先判断当前自动任务处于哪个阶段,根据阶段只关心特定的事件类型。比如自动填写表单的场景,第一阶段等待弹窗出现,此时只关注TYPE_WINDOW_STATE_CHANGED;进入填写阶段,只关注TYPE_WINDOW_CONTENT_CHANGED。其余事件直接在入口处丢弃。
事件里携带的event.getClassName()和event.getPackageName()也很有用,可以用来判断当前是不是目标包名,是否切到了我们关心的Activity。这是一个快速过滤的维度,可以大幅减少无效计算:
@Override public void onAccessibilityEvent(AccessibilityEvent event) { if (!"com.target.package".equals(event.getPackageName())) { return; } switch (event.getEventType()) { case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: handleWindowChanged(event); break; case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED: handleContentChanged(event); break; case AccessibilityEvent.TYPE_VIEW_CLICKED: handleViewClicked(event); break; } }3.2 查找节点:findAccessibilityNodeInfosByText的核心逻辑
拿到AccessibilityNodeInfo的根节点,才能在视图树里精确定位目标控件。系统的根节点获取方式:
AccessibilityNodeInfo root = getRootInActiveWindow(); if (root == null) return;然后可以用文本查找:
List<AccessibilityNodeInfo> nodes = root.findAccessibilityNodeInfosByText("确定");这个API返回一个列表,因为界面上可能有多处匹配文本。源码层面的实现逻辑本质上是深度优先遍历整棵View树,对每个节点的text、contentDescription字段做String.contains匹配。注意,findAccessibilityNodeInfosByText匹配的是包含关系,不是精确匹配,这是很多人在处理按钮文案时容易踩的坑。
更精准的方式是手动遍历整棵树,按viewIdResourceName查找:
private AccessibilityNodeInfo findNodeByViewId(AccessibilityNodeInfo root, String viewId) { if (root == null) return null; if (viewId.equals(root.getViewIdResourceName())) { return root; } for (int i = 0; i < root.getChildCount(); i++) { AccessibilityNodeInfo child = root.getChild(i); if (child == null) continue; AccessibilityNodeInfo found = findNodeByViewId(child, viewId); if (found != null) return found; child.recycle(); // 注意回收 } return null; }这里必须提一下recycle()。在Android 4.4之前的版本,AccessibilityNodeInfo使用完后必须主动recycle()释放内存,否则会迅速撑爆Binder内存池。高版本系统已经自动管理引用计数,但养成手动回收的习惯仍能规避潜在的兼容性风险。特别是在遍历大量节点的情况下,不及时回收会导致系统丢出“Binder 缓存已满”的异常,再想恢复只能重启App。
3.3 模拟点击与手势:performAction和dispatchGesture
定位到节点之后,最常用的操作是模拟点击:
node.performAction(AccessibilityNodeInfo.ACTION_CLICK);这一个调用会通知系统在对应坐标执行一次“可信的点击事件”,表现和真实手指点击几乎一致。类似的还有:
ACTION_LONG_CLICK长按ACTION_SCROLL_FORWARD/ACTION_SCROLL_BACKWARD滚动ACTION_SET_TEXT设置输入框文本(配合Bundle传参)ACTION_FOCUS获取焦点
但有些时候,目标界面不是标准的View树(比如视频播放器、SurfaceView绘制的界面),节点树里找不到可点击节点。这时候就需要走路径手势:
Path swipePath = new Path(); swipePath.moveTo(startX, startY); swipePath.lineTo(endX, endY); GestureDescription.Builder builder = new GestureDescription.Builder(); builder.addStroke(new GestureDescription.StrokeDescription(swipePath, 0, 500)); dispatchGesture(builder.build(), null, null);dispatchGesture支持自定义Path,可以模拟任意轨迹的滑动,包括曲线、多指手势。这是目前做滑动操作最接近真实输入的方式。我遇到过一个场景:某App的验证码滑块,只有滑动轨迹足够“像人”才能通过,单纯从中心点到目标点直线滑动会被判定为机器操作。后来给Path加入了三阶贝塞尔曲线,模拟手部拖动时的微小偏移,通过率提升了一大截。
设置文本的操作也比较特殊,写法如下:
AccessibilityNodeInfo editNode = findNodeByViewId(root, "editTextId"); Bundle arguments = new Bundle(); arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, "需要填入的内容"); editNode.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments);这种方式会直接给输入框赋值,不经过键盘输入,速度很快。但要注意,有些App自定义输入框不响应ACTION_SET_TEXT,还是得老老实实通过dispatchGesture点击坐标触发系统键盘输入。
4. 无障碍服务最容易踩的坑:从系统限制到厂商定制
4.1 权限与用户心里的那层顾虑
无障碍服务从Android 10开始,不能继续通过ADB直接开启;Android 13开始,用户开启无障碍服务时会看到更醒目的安全警告弹窗。这是谷歌刻意收紧了权限边界——无障碍权限能做的事情太底层了,读屏幕、模拟点击、读取通知,组合在一起理论上可以窃取敏感信息、自动转账。
所以源码里几样东西别做:未经用户明确授权就开始读取通知栏内容、上传任何节点信息到远程服务器、在主线程执行大量节点遍历导致UI卡顿。一旦系统判定某应用滥用无障碍权限,直接下架、限制安装、甚至推送安全告警给用户,都是常见后果。开发者必须把合规性放在所有技术方案之前。
4.2 多窗口、悬浮窗、系统弹窗下的节点获取问题
getRootInActiveWindow()返回的是“当前活动窗口”的节点树,但在实际使用中有几个坑:
首先是分屏模式。分屏状态下存在两个可见窗口,系统只会返回焦点所在的那个窗口。如果目标App被用户短暂的没有焦点,此时执行节点查找,得到的结果是null。这就是为什么长时间运行的自动化任务经常在快结束时突然“找不到控件”。
其次是系统弹窗遮挡。当存在TYPE_APPLICATION_OVERLAY类型的悬浮窗盖在目标App上方时,无障碍服务的活跃窗口有可能会变成悬浮窗本身,进而拿不到目标App的节点。解决办法是在查找节点前,检测根节点的包名是不是目标包名,不是就直接放弃本次操作,等待下一次事件再试:
AccessibilityNodeInfo root = getRootInActiveWindow(); if (root == null || !TARGET_PACKAGE.equals(root.getPackageName())) { return; }4.3 厂商ROM的差异化处理
这是安卓生态绕不开的痛。国内定制ROM对无障碍服务的限制五花八门:
- 小米:默认开启“后台弹出界面”限制,服务被拉起后自动任务无法弹起Activity;需要在MIUI的“自启动管理”里手动允许。
- 华为:鸿蒙系统对长驻后台管控更严格,服务进程容易被回收,要在“应用启动管理”里关闭“自动管理”,手动设置为“允许”全部三项。
- vivo/OPPO:Funtouch OS/ColorOS对通知栏相关能力有特殊限制,无障碍服务监听通知需要额外申请“通知使用权”。
我的建议是,在服务启动阶段做一次能力和状态自检,把系统限制、权限缺失、服务未开启等情况逐一检测并提示用户,而不是等用户跑完整个流程回来发现毫无效果。自检逻辑可以放在MainActivity,也可以做成引导页,核心是让用户知道要手动去设置-更多设置-无障碍里把服务和相关的辅助权限都打开。
5. 从Demo到自动化工具:任务编排与稳定性加固
5.1 任务状态机与核心调度设计
Demo级的无障碍服务用if-else堆逻辑就能跑,但真正的自动化工具需要一套任务调度机制。我的做法是维护一个任务状态机:
public enum AutomateState { IDLE, LAUNCHING_APP, WAITING_PAGE, FILLING_FORM, SUBMITTING, FINISHED }每个状态对应一组处理逻辑和一个超时上限。状态之间的流转依赖事件回调:比如LAUNCHING_APP状态下,收到窗口切换事件且目标是FormPage时,流转到FILLING_FORM;如果超过5秒还在闪烁等待,记一次失败日志并重试。
超时处理是稳定性设计中容易被忽略的环节。我遇到的真实情况是,目标App在弱网环境下多加载了两秒,自动化任务没等页面出现就发起查找节点操作,底层拿到null后直接走异常分支,导致整个流程失败。最终方案是给每个状态加了独立的超时时间,超时后允许重试N次(一般3次),仍失败则停止并通知用户,确保不会因为一次抖动就卡死在半成品的填表界面。
5.2 记录运行日志,这比功能本身更重要
做自动化工具的都知道,核心难点不在“能不能执行”,而在“执行失败后怎么定位原因”。我的做法是在所有关键步骤点打日志:发出点击动作、收到点击事件、节点查找耗时、状态流转原因……日志输出到本地文件,按天滚动覆盖。
日志维度至少包含三块:
- 事件日志:收到了哪些类型的事件,事件来自于哪个包名
- 状态流转日志:任务状态机的每次流转时间和触发原因
- 异常日志:节点查找失败、Binder异常、权限丢失等异常快照
有了这三类日志,远程排查能省下大量时间。我甚至会在项目里加一个Debug模式,打开后可以在悬浮窗实时显示当前节点的层级、文本、坐标、状态,配合日志分析可以快速定位节点查找不到的原因。
5.3 连接能力的边界与耗时控制
无障碍服务的主回调虽然是异步的,但所有操作本质上仍然运行在App的死循环和Binder线程池上。如果主线程持续执行重量级节点遍历、反射调用、频繁内存解析,App很容易触发ANR,导致整个服务被杀。
可以遵循几个纪律:不把一次性查找的节点List保存在内存里超过2秒;需要多次查找的场景,每次直接调用方法而不是缓存;复杂操作放在后台线程,但要注意AccessibilityNodeInfo不是线程安全对象,不能跨线程传递,只能在获取它的线程中操作。这个限制很反直觉,我第一版就吃过亏,后台线程拿到节点再回来执行点击,直接抛了RejectedExecutionException。
回到开头的话题,无障碍服务源码说复杂,核心机制就是事件分发+节点树遍历+模拟操作三大块;说简单,又必须面对系统约束、厂商差异、线程安全这些工程细节。做这个东西,技术挑战只是一部分,更考验的是对系统机制的理解深度和对异常路径的处理耐心。
如果你准备上手,个人建议第一版别追求大而全,找一个具体的场景(比如自动打开App并完成一次签到),把流程跑通,然后逐步加入状态管理、异常恢复和日志收集。当你把一套流程打磨到两周不出异常的时候,再回来看无障碍服务的源码,很多设计逻辑自然就通了。
本文还有配套的精品资源,点击获取