文章目录
- [深入理解 Android 无障碍服务](#深入理解 Android 无障碍服务)
-
- 一、先从一个例子说起
- 二、整体架构:三个进程的故事
-
- [2.1 无障碍服务涉及哪些进程?](#2.1 无障碍服务涉及哪些进程?)
- [2.2 这三个进程分别负责什么?](#2.2 这三个进程分别负责什么?)
- [2.3 组件之间的调用关系](#2.3 组件之间的调用关系)
- 三、服务生命周期:容易忽略的起点
-
- [3.1 必须在 AndroidManifest.xml 中声明](#3.1 必须在 AndroidManifest.xml 中声明)
- [3.2 onServiceConnected:真正的初始化位置](#3.2 onServiceConnected:真正的初始化位置)
- [四、事件流转:完整的 9 步链路](#四、事件流转:完整的 9 步链路)
-
- [4.1 完整流程](#4.1 完整流程)
- [4.2 关键代码路径(对应 AOSP)](#4.2 关键代码路径(对应 AOSP))
- [4.3 为什么有时候收不到事件?](#4.3 为什么有时候收不到事件?)
- [五、核心 API 详解](#五、核心 API 详解)
-
- [5.1 AccessibilityEvent 的核心属性](#5.1 AccessibilityEvent 的核心属性)
- [5.2 常用事件类型](#5.2 常用事件类型)
- [5.3 AccessibilityNodeInfo:查找与操作节点](#5.3 AccessibilityNodeInfo:查找与操作节点)
- [5.4 常用节点操作](#5.4 常用节点操作)
- 六、手势注入原理
-
- [6.1 dispatchGesture 的工作方式](#6.1 dispatchGesture 的工作方式)
- [6.2 手势在系统中的流转](#6.2 手势在系统中的流转)
- [6.3 手势的限制与陷阱](#6.3 手势的限制与陷阱)
- 七、配置文件详解
-
- [7.1 核心配置项](#7.1 核心配置项)
- 八、内存管理与性能
-
- [8.1 节点的本质:Binder 对象,必须回收](#8.1 节点的本质:Binder 对象,必须回收)
- [8.2 减少 rootInActiveWindow 调用](#8.2 减少 rootInActiveWindow 调用)
- 九、常见问题与解决方案
-
- [Q1: 收不到事件?](#Q1: 收不到事件?)
- [Q2: rootInActiveWindow 返回 null?](#Q2: rootInActiveWindow 返回 null?)
- [Q3: 手势不生效?](#Q3: 手势不生效?)
- [Q4: performAction 返回 true 但没效果?](#Q4: performAction 返回 true 但没效果?)
- 十、总结
深入理解 Android 无障碍服务
从高级开发者视角,深度解析 AccessibilityService 的内部工作机制
一、先从一个例子说起
当你点击手机上的一个按钮,TalkBack 是如何知道的?
这个看似简单的问题,隐藏着 Android 最复杂的系统机制之一。从手指触碰屏幕到你的 AccessibilityService 收到事件,数据经历了:
- View → ViewRootImpl → AccessibilityManager → AccessibilityManagerService → AccessibilityServiceConnection → 你的 Service
- 至少 2 次 Binder IPC 调用(事件传递链路本身;节点查询时额外产生第 3 次)
- 跨 2 个进程边界
- 涉及 5+ 个系统组件
理解这个链路,才能真正知道"为什么收不到事件"、"为什么操作没生效"。本文将逐层拆解每个环节。
二、整体架构:三个进程的故事
2.1 无障碍服务涉及哪些进程?
Android 无障碍服务横跨 3 个进程,这是很多开发者没有意识到的:

关键点:你的 Service 根本不知道被监控的应用在哪里,它只通过 Binder 的 AIDL 接口与 system_server 通信。被监控的应用也不知道有人在"监听"它,这一切都由系统透明代理。
2.2 这三个进程分别负责什么?
| 进程 | 负责内容 | 关键类 |
|---|---|---|
| 你的应用进程 | 运行你写的 AccessibilityService,处理事件、执行操作 | AccessibilityService(你继承) |
| system_server | 管理所有无障碍服务的注册/注销,聚合并分发事件 | AccessibilityManagerService |
| 被监控的应用进程 | 产生 UI 事件,响应节点查询,构建 View 树快照 | ViewRootImpl、View |
2.3 组件之间的调用关系

一句话理解:system_server 是中间人,它既是被监控应用的"事件收件箱",也是你的 Service 的"事件邮差"。你和被监控的应用永远不会直接对话。
三、服务生命周期:容易忽略的起点
很多文章直接讲事件监听,跳过了服务的启动流程。但 onServiceConnected() 才是真正的入口。
3.1 必须在 AndroidManifest.xml 中声明
没有这个声明,系统根本不会启动你的服务:
xml
<service
android:name=".MyAccessibilityService"
android:exported="true"
android:label="@string/service_label"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<!-- 指向 res/xml/accessibility_service_config.xml -->
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessibility_service_config" />
</service>
BIND_ACCESSIBILITY_SERVICE 权限是关键------它确保只有系统(system_server)才能绑定你的服务,防止第三方应用伪装成系统来控制你的 Service。
3.2 onServiceConnected:真正的初始化位置
kotlin
class MyAccessibilityService : AccessibilityService() {
override fun onServiceConnected() {
// 这里是服务成功连接到系统后的回调
// 也是动态修改服务配置的唯一时机
// 如果你需要在运行时调整监听的事件类型或包名,在这里做:
serviceInfo = serviceInfo.apply {
// 动态添加额外监听的包名(配置文件里的是静态的)
packageNames = arrayOf("com.target.app")
// 确保手势注入权限开启
flags = flags or AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE
}
Log.d("A11y", "服务已连接,配置生效")
}
override fun onAccessibilityEvent(event: AccessibilityEvent) {
// 事件处理(见第四节)
}
override fun onInterrupt() {
// 系统中断服务时回调(如用户在设置中关闭服务)
// 应在此停止所有正在进行的操作
}
}
为什么不在构造函数或 onCreate 里初始化? 因为
serviceInfo在onServiceConnected()之前为 null,此时修改没有意义,系统还没完成绑定。
四、事件流转:完整的 9 步链路
4.1 完整流程
当用户点击一个按钮,事件这样传递:

为什么是异步的? 步骤 3 中 ViewRootImpl 通过 Handler 异步投递,目的是不阻塞 UI 线程。这意味着从用户点击到你的回调,有轻微延迟(通常 < 5ms),但在高负载时可能更长。
4.2 关键代码路径(对应 AOSP)
步骤 2:View 触发事件
java
// frameworks/base/core/java/android/view/View.java
public boolean performClick() {
notifyAutofillManagerOnClick();
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
注意:sendAccessibilityEvent 是在 onClick 回调之后才调用的,所以你的业务逻辑先执行,无障碍事件后发出。
步骤 3:ViewRootImpl 的异步投递
java
// frameworks/base/core/java/android/view/ViewRootImpl.java
// AccessibilityInteractionController 负责异步处理
// 通过 mHandler 投递,不阻塞调用方
void scheduleAccessibilityEvent(View view, int eventType) {
// 实际代码路径经过 AccessibilityDelegate
// 最终调用 AccessibilityManager.sendAccessibilityEvent
}
步骤 5:系统服务的事件聚合
java
// frameworks/base/services/accessibility/AccessibilityManagerService.java
private void sendAccessibilityEventLocked(AccessibilityEvent event, int userId) {
// 关键:按事件类型独立节流
// 每个 (serviceConnection, eventType) 组合维护独立的上次发送时间
// 而不是所有事件类型共用一个 notificationTimeout 计时器
for (AccessibilityServiceConnection service : mBoundServices) {
service.notifyAccessibilityEventLocked(event);
}
}
4.3 为什么有时候收不到事件?
事件经过 4 层过滤,任意一层不通过就会被丢弃:
五、核心 API 详解
5.1 AccessibilityEvent 的核心属性
kotlin
override fun onAccessibilityEvent(event: AccessibilityEvent) {
// 事件类型:决定这是什么操作
val eventType = event.eventType
// 包名:哪个应用触发了这个事件
val packageName = event.packageName
// 类名:触发事件的 View 的类型(如 android.widget.Button)
val className = event.className
// 文本列表:View 上的文字(可能是列表,如 ListView item)
val text = event.text.joinToString()
// contentDescription:View 的无障碍描述(图标按钮常用此字段)
val desc = event.contentDescription
// source:触发事件的 View 对应的节点(用完必须 recycle)
val source = event.source
// windowId:所在窗口的 ID(多窗口场景有用)
val windowId = event.windowId
// 注意:event 本身不需要 recycle,但 event.source 需要
source?.recycle()
}
5.2 常用事件类型
kotlin
// UI 交互
AccessibilityEvent.TYPE_VIEW_CLICKED // 点击
AccessibilityEvent.TYPE_VIEW_LONG_CLICKED // 长按
AccessibilityEvent.TYPE_VIEW_FOCUSED // 获得输入焦点
AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED // 输入框文字变化
AccessibilityEvent.TYPE_VIEW_SCROLLED // 滚动
// 窗口变化(最常用的两个,注意区别)
AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED // 新 Activity/Dialog 出现
AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED // 当前窗口内容局部更新
// 其他
AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED // 通知栏变化
AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED // 无障碍焦点移动
TYPE_WINDOW_STATE_CHANGED vs TYPE_WINDOW_CONTENT_CHANGED 的区别 :
前者是页面级变化(跳转了新页面),后者是内容级变化(同一页面的列表刷新、弹窗出现)。
判断"是否进入了某个页面"用前者;监听动态内容更新用后者,但要注意后者触发频率极高。
5.3 AccessibilityNodeInfo:查找与操作节点
kotlin
// 获取根节点(当前前台窗口的顶层 View)
// 每次调用都有 Binder IPC 开销,不要在循环里调用
val rootNode = rootInActiveWindow ?: return
// 方式1: 按文本模糊查找(contains 语义)
val nodes = rootNode.findAccessibilityNodeInfosByText("确认")
// 方式2: 按 View ID 精确查找(推荐,稳定性高于文本)
val buttons = rootNode.findAccessibilityNodeInfosByViewId("com.app:id/btn_confirm")
// 方式3: 查找当前输入焦点(做自动填写时有用)
val focused = rootNode.findFocus(AccessibilityNodeInfo.FOCUS_INPUT)
// 方式4: 递归遍历(最灵活,可自定义匹配条件)
fun traverseNode(node: AccessibilityNodeInfo, depth: Int = 0) {
println("${" ".repeat(depth)}[${node.className}] text=${node.text} clickable=${node.isClickable}")
for (i in 0 until node.childCount) {
node.getChild(i)?.let { child ->
traverseNode(child, depth + 1)
child.recycle()
}
}
}
traverseNode(rootNode)
rootNode.recycle()
为什么按 ViewId 查找比文本更稳定? 文本会随多语言、A/B 测试变化;ViewId 通常只随重构变化,且语义明确,更不容易误匹配。
5.4 常用节点操作
kotlin
// 点击(优先找可点击的节点,必要时向上找父节点)
fun clickNode(node: AccessibilityNodeInfo): Boolean {
if (node.isClickable) {
return node.performAction(AccessibilityNodeInfo.ACTION_CLICK)
}
// 有些文字放在不可点击的 TextView 里,但父容器可点击
var parent = node.parent
while (parent != null) {
if (parent.isClickable) {
val result = parent.performAction(AccessibilityNodeInfo.ACTION_CLICK)
parent.recycle()
return result
}
val next = parent.parent
parent.recycle()
parent = next
}
return false
}
// 输入文字(直接写入,无需节点先获得焦点)
fun inputText(node: AccessibilityNodeInfo, text: String): Boolean {
val args = Bundle().apply {
putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text)
}
return node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args)
}
// 向下滚动
fun scrollDown(node: AccessibilityNodeInfo): Boolean {
return node.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD)
}
// 全局操作(不依赖特定节点)
fun pressBack() = performGlobalAction(GLOBAL_ACTION_BACK)
fun goHome() = performGlobalAction(GLOBAL_ACTION_HOME)
fun openRecents() = performGlobalAction(GLOBAL_ACTION_RECENTS)
六、手势注入原理
6.1 dispatchGesture 的工作方式
手势注入和节点操作的本质区别:节点操作是"语义层"的(告诉系统"点击这个按钮"),手势注入是"输入层"的(模拟真实手指触摸坐标)。有些应用会检测节点操作(performAction 的来源),手势注入因为走 InputDispatcher,更难被检测。
kotlin
// 模拟从屏幕中间向上滑动(上划手势)
fun swipeUp() {
val displayMetrics = resources.displayMetrics
val centerX = displayMetrics.widthPixels / 2f
val startY = displayMetrics.heightPixels * 0.8f
val endY = displayMetrics.heightPixels * 0.2f
val path = Path().apply {
moveTo(centerX, startY)
lineTo(centerX, endY)
}
val gesture = GestureDescription.Builder()
.addStroke(
GestureDescription.StrokeDescription(
path,
0L, // 延迟开始(ms)
500L // 持续时间(ms),影响滑动速度
)
)
.build()
dispatchGesture(gesture, object : GestureResultCallback() {
override fun onCompleted(gestureDescription: GestureDescription?) {
// 手势成功注入
}
override fun onCancelled(gestureDescription: GestureDescription?) {
// 注入被取消(如系统资源不足)
}
}, null)
}
6.2 手势在系统中的流转

6.3 手势的限制与陷阱
- 坐标是屏幕绝对坐标 ,不是 View 内部坐标。获取节点位置用
node.getBoundsInScreen(rect) - flagRequestTouchExplorationMode 必须开启 ,否则
dispatchGesture会静默失败 - StrokeDescription 的持续时间影响速度:太短会被识别为点击而非滑动;滑动一般 300~800ms 比较自然
- 悬浮窗可能拦截:如果目标坐标被其他窗口覆盖,事件会发给覆盖的窗口
kotlin
// 正确获取节点屏幕坐标再注入手势
fun clickNodeByGesture(node: AccessibilityNodeInfo) {
val rect = Rect()
node.getBoundsInScreen(rect)
val centerX = rect.centerX().toFloat()
val centerY = rect.centerY().toFloat()
val path = Path().apply { moveTo(centerX, centerY) }
val gesture = GestureDescription.Builder()
.addStroke(GestureDescription.StrokeDescription(path, 0, 50)) // 50ms = 点击
.build()
dispatchGesture(gesture, null, null)
}
七、配置文件详解
7.1 核心配置项
xml
<!-- res/xml/accessibility_service_config.xml -->
<accessibility-service
xmlns:android="http://schemas.android.com/apk/res/android"
<!-- 监听哪些事件类型(多个用 | 连接,typeAllMask 监听全部) -->
android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged|typeViewClicked"
<!-- 反馈方式(辅助功能对用户的输出方式) -->
android:accessibilityFeedbackType="feedbackGeneric"
<!-- 额外能力标志 -->
android:accessibilityFlags="flagDefault|flagRequestTouchExplorationMode|flagReportViewIds"
<!-- 允许读取窗口内容(必须为 true 才能用 rootInActiveWindow) -->
android:canRetrieveWindowContent="true"
<!-- 允许执行手势(API 24+) -->
android:canPerformGestures="true"
<!-- 事件聚合间隔(ms):同类型事件在此时间内只投递一次 -->
android:notificationTimeout="100"
<!-- 用户在设置中看到的服务说明 -->
android:description="@string/service_description"
<!-- 点击"设置"时跳转的页面(可选) -->
android:settingsActivity="com.example.SettingsActivity" />
flagReportViewIds 的作用 :默认情况下
AccessibilityNodeInfo.getViewIdResourceName()返回 null,开启此 flag 后才能获取到 View 的 ID(如com.app:id/btn_ok),按 ViewId 查找节点时必须开启。
八、内存管理与性能
8.1 节点的本质:Binder 对象,必须回收
AccessibilityNodeInfo 不是普通 Java 对象,它的底层是 Binder 代理(指向 system_server 里的实际节点)。系统为了避免 Binder 句柄耗尽,强制要求你手动回收。不回收不会立刻崩溃,但会缓慢泄漏系统资源,最终导致 AccessibilityNodeInfo 返回 null 或行为异常。
kotlin
// ❌ 泄漏:拿到节点但忘记 recycle
val nodes = rootNode.findAccessibilityNodeInfosByText("确认")
val first = nodes.firstOrNull()
// first 用完了,但没有 recycle
// ✅ 方式1: 手动回收所有节点
val nodes = rootNode.findAccessibilityNodeInfosByText("确认")
try {
nodes.firstOrNull()?.let { node ->
node.performAction(AccessibilityNodeInfo.ACTION_CLICK)
node.recycle()
}
} finally {
// 其余未使用的节点也要回收
nodes.drop(1).forEach { it.recycle() }
}
// ✅ 方式2: Kotlin 扩展(兼容全版本,命名避开标准库的 use)
inline fun <T> AccessibilityNodeInfo.useNode(block: (AccessibilityNodeInfo) -> T): T =
try { block(this) } finally { recycle() }
// 使用方式:
rootNode.findAccessibilityNodeInfosByText("确认")
.forEach { node -> node.useNode { it.performAction(AccessibilityNodeInfo.ACTION_CLICK) } }
8.2 减少 rootInActiveWindow 调用
每次调用 rootInActiveWindow 都会触发 Binder IPC,让 system_server 重新为你序列化当前窗口的 View 树快照。在 onAccessibilityEvent 里高频调用会明显拖慢性能。
kotlin
// ❌ 每次事件都重新获取整棵树
override fun onAccessibilityEvent(event: AccessibilityEvent) {
val root = rootInActiveWindow ?: return // 每次都有 IPC 开销
val node = root.findAccessibilityNodeInfosByViewId("com.app:id/btn_ok")
// ...
root.recycle()
}
// ✅ 缓存根节点,仅在窗口切换时刷新
private var cachedRoot: AccessibilityNodeInfo? = null
override fun onAccessibilityEvent(event: AccessibilityEvent) {
// 只有窗口级别切换(新 Activity/Dialog)才刷新缓存
// ⚠️ 不在 TYPE_WINDOW_CONTENT_CHANGED 时刷新:该事件触发极频繁,
// 每次刷新都会触发一次 Binder IPC,与减少调用的目的相悖
if (event.eventType == TYPE_WINDOW_STATE_CHANGED) {
cachedRoot?.recycle()
cachedRoot = rootInActiveWindow
}
val root = cachedRoot ?: return
val nodes = root.findAccessibilityNodeInfosByViewId("com.app:id/btn_ok")
// ... 用完回收 nodes
}
// ⚠️ 缓存的风险:TYPE_WINDOW_CONTENT_CHANGED 触发时,部分子树已变化
// 如果发现节点信息不是最新的,在那次事件里刷新缓存即可
九、常见问题与解决方案
Q1: 收不到事件?
按顺序检查:
- 系统设置 → 无障碍 → 你的服务,是否已开启?
accessibilityEventTypes是否包含你期望的事件类型?packageNames是否漏掉了目标应用的包名?(null = 监听全部)notificationTimeout是否太长导致事件被丢弃?- 目标应用是否是系统应用或关闭了无障碍支持(
importantForAccessibility="no")?
Q2: rootInActiveWindow 返回 null?
最常见原因:窗口刚切换,View 树还没渲染完成。
kotlin
override fun onAccessibilityEvent(event: AccessibilityEvent) {
if (event.eventType == TYPE_WINDOW_STATE_CHANGED) {
// 延迟 300ms 等待页面渲染完成再获取节点
// 注意:这个 Handler 在主线程,不会有线程安全问题
Handler(Looper.getMainLooper()).postDelayed({
val root = rootInActiveWindow ?: return@postDelayed
// 此时树已稳定
root.recycle()
}, 300)
}
}
其他原因:
- 目标是系统 UI(如锁屏),默认不对无障碍服务开放
canRetrieveWindowContent没有设为true
Q3: 手势不生效?
按顺序检查:
- 配置文件里有
android:canPerformGestures="true"吗? onServiceConnected里有设置FLAG_REQUEST_TOUCH_EXPLORATION_MODE吗?- 坐标是否用
getBoundsInScreen()获取的屏幕绝对坐标? - StrokeDescription 的持续时间是否合理(点击 50ms,滑动 300ms+)?
Q4: performAction 返回 true 但没效果?
返回 true 只代表系统接收了请求 ,不代表目标应用执行了操作。原因可能是:
- 节点已不在屏幕上(stale 引用)
- 目标应用自定义了
AccessibilityDelegate并拦截了操作 - 在错误的线程调用(确保在主线程)
十、总结
核心要点
- 三进程模型:你的进程 ↔ system_server ↔ 被监控进程,你和目标应用永不直接通信
- 事件完整链路:View.performClick → ViewRootImpl 异步打包 → AccessibilityManager → system_server 聚合过滤 → 你的 Service
- 四层过滤:事件类型、notificationTimeout(按类型独立计时)、包名、反馈类型,任意一层不过就收不到
- 节点是 Binder 对象:必须 recycle,否则缓慢泄漏系统资源
- onServiceConnected 是真正的起点:动态配置 serviceInfo 只能在这里做
