系列目录 :第一篇:全景图与架构概览 | [第二篇:AccessibilityManagerService 启动与初始化](#第二篇:AccessibilityManagerService 启动与初始化) | 第三篇:无障碍服务的注册与绑定 | [第四篇:AccessibilityEvent 的产生与投递](#第四篇:AccessibilityEvent 的产生与投递) | [第五篇:AccessibilityNodeInfo 的构建与查询](#第五篇:AccessibilityNodeInfo 的构建与查询) | [第六篇:输入事件拦截与 TouchExplorer](#第六篇:输入事件拦截与 TouchExplorer) | [第七篇:手势分发与 KeyEvent 处理](#第七篇:手势分发与 KeyEvent 处理) | 第八篇:放大镜与辅助功能特性
上一篇我们分析了 AccessibilityEvent 如何从 View 层到达无障碍服务。但事件只告诉服务"发生了什么变化",服务要知道"当前界面有哪些控件、每个控件什么属性",就需要 AccessibilityNodeInfo。本篇分析节点树的数据结构、跨进程查询机制、批量预取策略与缓存设计。
一次 node.getChild(0) 要跨几次进程?TalkBack 遍历一屏控件需要几十次 Binder 往返,为什么用户感觉不到延迟?答案藏在"一次查询返回一批节点 + 客户端缓存"的组合设计里。
一、AccessibilityNodeInfo:节点树的数据结构
AccessibilityNodeInfo 是 View 树在无障碍框架中的镜像节点,与 AccessibilityEvent 一样继承自 AccessibilityRecord。
源码路径 :frameworks/base/core/java/android/view/accessibility/AccessibilityNodeInfo.java
java
public class AccessibilityNodeInfo implements Parcelable {
// ===== 节点定位(不是对象引用,是可跨进程的 ID)=====
private long mSourceNodeId = ROOT_NODE_ID; // 本节点 ID
private long mParentNodeId = ROOT_NODE_ID; // 父节点 ID
private long mLabelForId = ROOT_NODE_ID; // "为谁做标签"
private long mLabeledById = ROOT_NODE_ID; // "被谁标注"
private long mTraversalBefore = ROOT_NODE_ID; // 遍历顺序:之前
private long mTraversalAfter = ROOT_NODE_ID; // 遍历顺序:之后
private int mConnectionId = UNDEFINED_CONNECTION_ID;
// ===== 属性(与 AccessibilityRecord 共享语义)=====
private CharSequence mPackageName;
private CharSequence mClassName;
private CharSequence mText;
private CharSequence mContentDescription;
private CharSequence mViewIdResourceName;
private boolean mCheckable, mChecked, mFocusable, mFocused, mSelected;
private boolean mClickable, mLongClickable, mEnabled, mPassword;
private boolean mScrollable, mDismissable, mEditable, mVisibleToUser;
private boolean mAccessibilityFocused, mMultiLine, mCanOpenPopup;
// ... 坐标 Rect mBoundsInParent / mBoundsInScreen、动作列表、文本选择等 ...
// ===== 预取标志(查询时的"附带要求")=====
public static final int FLAG_PREFETCH_PREDECESSORS = 0x00000001; // 预取祖先
public static final int FLAG_PREFETCH_SIBLINGS = 0x00000002; // 预取兄弟
public static final int FLAG_PREFETCH_DESCENDANTS = 0x00000004; // 预取后代
public static final int FLAG_PREFETCH_UNINTERRUPTIBLE = 0x00000008;
// ...
}
关键设计 :节点之间的父子关系存的是 ID 而非对象引用 。
getChild()返回的"节点"只有 ID 是可靠的,属性字段是否新鲜取决于缓存策略(见第五节)。整个结构可以一次性序列化跨进程,这是"按值传递"的节点树。
二、sourceNodeId 的编码:viewId + virtualDescendantId
一个 long 类型的节点 ID 由两部分拼成------低位是真实 View 的 accessibilityViewId,高位是虚拟后代 ID:
java
public class AccessibilityNodeInfo implements Parcelable {
// ...
public static final int UNDEFINED_ITEM_ID = -1;
public static final long ROOT_NODE_ID =
makeNodeId(UNDEFINED_ITEM_ID, UNDEFINED_ITEM_ID);
public static int getAccessibilityViewId(long accessibilityNodeId) {
return (int) accessibilityNodeId; // 截断取低 32 位
}
public static int getVirtualDescendantId(long accessibilityNodeId) {
return (int) ((accessibilityNodeId & VIRTUAL_DESCENDANT_ID_MASK)
>> VIRTUAL_DESCENDANT_ID_SHIFT);
}
public static long makeNodeId(int accessibilityViewId, int virtualDescendantId) {
// We changed the value for undefined node to positive due to wrong
// global id composition (two 32-bin ints into one 64-bit long) but
// the value used for the host node provider view has id -1 so we
// remap it here.
if (virtualDescendantId == AccessibilityNodeProvider.HOST_VIEW_ID) {
virtualDescendantId = UNDEFINED_ITEM_ID;
}
return (((long) virtualDescendantId) << VIRTUAL_DESCENDANT_ID_SHIFT)
| accessibilityViewId;
}
}
注意:窗口 ID 不在节点 ID 里 ,它是 AccessibilityRecord 的独立字段(mWindowId)。"哪个窗口的哪个 View 的哪个虚拟节点"需要 (windowId, nodeId) 二元组才能唯一定位------所有查询 API 都同时传这两个参数。
accessibilityViewId 是 View 首次无障碍交互时分配的自增 ID(View.getAccessibilityViewId()),进程内唯一;虚拟节点 ID 由 AccessibilityNodeProvider 的实现者自定义。
2.1 虚拟节点:AccessibilityNodeProvider
WebView、自定义日历控件等内部结构不是 Android View 树,通过 provider 暴露"虚拟子节点":
java
// 源码路径:frameworks/base/core/java/android/view/accessibility/AccessibilityNodeProvider.java
public abstract class AccessibilityNodeProvider {
public static final int HOST_VIEW_ID = -1;
public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
return null;
}
public boolean performAction(int virtualViewId, int action, Bundle arguments) {
return false;
}
public AccessibilityNodeInfo findFocus(int focus) {
return null;
}
// ...
}
View 树 无障碍节点树
──────── ────────────
ViewGroup Node(viewId=1) "日历容器"
└── CalendarView (provider) ├─ Node(viewId=2, virtual=101) "3月1日"
内部自绘,无子 View ├─ Node(viewId=2, virtual=102) "3月2日"
└─ Node(viewId=2, virtual=103) "3月3日"
查询到达 App 进程后,AccessibilityInteractionController 先按 viewId 找到宿主 View,再发现它有 provider,转而调用 provider.createAccessibilityNodeInfo(virtualViewId)(第五节)。
三、View 侧的节点构建:createAccessibilityNodeInfo()
源码路径 :frameworks/base/core/java/android/view/View.java
java
public class View implements Drawable.Callback, KeyEvent.Callback,
AccessibilityEventSource {
// ...
public AccessibilityNodeInfo createAccessibilityNodeInfo() {
if (mAccessibilityDelegate != null) {
return mAccessibilityDelegate.createAccessibilityNodeInfo(this);
} else {
return createAccessibilityNodeInfoInternal();
}
}
public AccessibilityNodeInfo createAccessibilityNodeInfoInternal() {
AccessibilityNodeProvider provider = getAccessibilityNodeProvider();
if (provider != null) {
return provider.createAccessibilityNodeInfo(
AccessibilityNodeProvider.HOST_VIEW_ID); // 有 provider,交给 provider
} else {
AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(this);
onInitializeAccessibilityNodeInfo(info); // 常规 View,自己填充
return info;
}
}
}
onInitializeAccessibilityNodeInfo() 是属性填充的集散点:基类填类名/包名/状态/文本/坐标;ViewGroup 覆写时追加 info.addChild(child) 建立父子关系;TextView 追加输入类型、错误信息;CompoundButton 追加 checked。开发者用 AccessibilityDelegate 或覆写插入定制信息。
四、跨进程查询:三方 Binder 协作
查询涉及三个进程角色,比事件投递复杂:
- 无障碍服务进程 :发起查询,持有
IAccessibilityServiceConnection(AMS 内部Service的代理) - system_server(AMS) :中转 + 安全校验,持有每个窗口的
IAccessibilityInteractionConnection - 目标 App 进程 :真正构建节点,由
ViewRootImpl提供
第三个 Binder 端点的注册时机在无障碍开关切换时:
源码路径 :frameworks/base/core/java/android/view/ViewRootImpl.java
java
public class ViewRootImpl extends Handler implements ViewParent,
View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
// ...
final class AccessibilityInteractionConnectionManager
implements AccessibilityStateChangeListener {
@Override
public void onAccessibilityStateChanged(boolean enabled) {
if (enabled) {
ensureConnection(); // 向 AMS 注册本窗口的查询端点
// 有窗口焦点时补发窗口状态与焦点事件(第四篇的事件回灌)
if (mAttachInfo.mHasWindowFocus) {
mView.sendAccessibilityEvent(
AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
View focusedView = mView.findFocus();
if (focusedView != null && focusedView != mView) {
focusedView.sendAccessibilityEvent(
AccessibilityEvent.TYPE_VIEW_FOCUSED);
}
}
} else {
ensureNoConnection();
mHandler.obtainMessage(MSG_CLEAR_ACCESSIBILITY_FOCUS_HOST).sendToTarget();
}
}
public void ensureConnection() {
final boolean registered =
mAttachInfo.mAccessibilityWindowId != AccessibilityNodeInfo.UNDEFINED_ITEM_ID;
if (!registered) {
mAttachInfo.mAccessibilityWindowId =
mAccessibilityManager.addAccessibilityInteractionConnection(
mWindow, mContext.getPackageName(),
new AccessibilityInteractionConnection(ViewRootImpl.this));
}
}
}
// AMS 持有的端点:每个窗口一个
static final class AccessibilityInteractionConnection
extends IAccessibilityInteractionConnection.Stub {
private final WeakReference<ViewRootImpl> mViewRootImpl;
// ... 各查询方法转给 AccessibilityInteractionController ...
}
}
AMS 把它存进 UserState.mInteractionConnections(SparseArray,按 windowId 索引,第二篇 UserState 字段表里的"非瞬态状态"之一)。
4.1 客户端:AccessibilityInteractionClient
源码路径 :frameworks/base/core/java/android/view/accessibility/AccessibilityInteractionClient.java
java
public final class AccessibilityInteractionClient
extends IAccessibilityInteractionConnectionCallback.Stub {
private static final long TIMEOUT_INTERACTION_MILLIS = 5000;
private static final AccessibilityCache sAccessibilityCache = new AccessibilityCache();
// 每个查询线程一个实例(按 threadId 存取,事件驱动的等待/唤醒模型)
// ...
public AccessibilityNodeInfo findAccessibilityNodeInfoByAccessibilityId(
int connectionId, int accessibilityWindowId, long accessibilityNodeId,
boolean bypassCache, int prefetchFlags) {
if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_SIBLINGS) != 0
&& (prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_PREDECESSORS) == 0) {
throw new IllegalArgumentException(
"FLAG_PREFETCH_SIBLINGS requires FLAG_PREFETCH_PREDECESSORS");
}
try {
IAccessibilityServiceConnection connection = getConnection(connectionId);
if (connection != null) {
// 1. 查缓存
if (!bypassCache) {
AccessibilityNodeInfo cachedInfo = sAccessibilityCache.getNode(
accessibilityWindowId, accessibilityNodeId);
if (cachedInfo != null) {
return cachedInfo;
}
}
// 2. 发起异步查询,自己作为回调
final int interactionId = mInteractionIdCounter.getAndIncrement();
final String[] packageNames;
final long identityToken = Binder.clearCallingIdentity();
try {
packageNames = connection.findAccessibilityNodeInfoByAccessibilityId(
accessibilityWindowId, accessibilityNodeId, interactionId,
this /*callback*/, prefetchFlags, Thread.currentThread().getId());
} finally {
Binder.restoreCallingIdentity(identityToken);
}
// 3. 阻塞等待回调送达(同线程闭环,见下文)
if (packageNames != null) {
List<AccessibilityNodeInfo> infos =
getFindAccessibilityNodeInfosResultAndClear(interactionId);
finalizeAndCacheAccessibilityNodeInfos(
infos, connectionId, bypassCache, packageNames);
if (infos != null && !infos.isEmpty()) {
for (int i = 1; i < infos.size(); i++) {
infos.get(i).recycle(); // 只返回第一个,其余进缓存
}
return infos.get(0);
}
}
}
} catch (RemoteException re) {
Log.e(LOG_TAG, "Error while calling remote"
+ " findAccessibilityNodeInfoByAccessibilityId", re);
}
return null;
}
}
这段代码藏着三个精妙设计:
- 单向 Binder + 回调 + 本地等待 :查询不是同步 Binder 往返,而是"发出单向请求 → 阻塞等回调"。
interactionId配对请求与响应,getFindAccessibilityNodeInfosResultAndClear()在超时窗口(5 秒)内等待,结果通过setFindAccessibilityNodeInfosResult()回调送达后唤醒。为什么这么绕?见下一条。 - 同进程快捷路径(
setSameThreadMessage) :目标 App 自己就是查询者(如 App 内用无障碍 API 自测)时,App 进程的处理消息若走 Binder 线程池再回主线程,而查询线程恰好就是主线程------会死锁。源码的解法是让 App 进程把"待处理消息"暂存在AccessibilityInteractionClient.mSameThreadMessage,查询调用返回后同一线程立即取出来执行,全程零线程切换。 - 一次查询返回一批 :
getFindAccessibilityNodeInfosResultAndClear()拿到的是 List------目标进程不只构建被查询的节点,还按prefetchFlags把祖先/兄弟/子孙一并打包(第五节详解)。服务端只见一次调用,缓存里却已躺着一整片子树。
performAccessibilityAction() 复用同一套 interactionId/回调/等待机制:
java
public boolean performAccessibilityAction(int connectionId, int accessibilityWindowId,
long accessibilityNodeId, int action, Bundle arguments) {
try {
IAccessibilityServiceConnection connection = getConnection(connectionId);
if (connection != null) {
final int interactionId = mInteractionIdCounter.getAndIncrement();
final boolean success = connection.performAccessibilityAction(
accessibilityWindowId, accessibilityNodeId, action, arguments,
interactionId, this, Thread.currentThread().getId());
if (success) {
return getPerformAccessibilityActionResultAndClear(interactionId);
}
}
} catch (RemoteException re) { /* ... */ }
return false;
}
五、App 进程:AccessibilityInteractionController 与批量预取
AMS 校验权限、找到 RemoteAccessibilityConnection 后把请求转给目标进程。ViewRootImpl.AccessibilityInteractionConnection(每个窗口的 Stub)再转给核心控制器:
源码路径 :frameworks/base/core/java/android/view/AccessibilityInteractionController.java
java
final class AccessibilityInteractionController {
// ...
public void findAccessibilityNodeInfoByAccessibilityIdClientThread(
long accessibilityNodeId, Region interactiveRegion, int interactionId,
IAccessibilityInteractionConnectionCallback callback, int flags,
int interrogatingPid, long interrogatingTid, MagnificationSpec spec) {
Message message = mHandler.obtainMessage();
message.what = PrivateHandler.MSG_FIND_ACCESSIBILITY_NODE_INFO_BY_ACCESSIBILITY_ID;
// ... 打包 viewId / virtualViewId / interactionId / callback / spec ...
// If the interrogation is performed by the same thread as the main UI
// thread in this process, set the message as a static reference so
// after this call completes the same thread but in the interrogating
// client can handle the message to generate the result.
if (interrogatingPid == mMyProcessId && interrogatingTid == mMyLooperThreadId) {
AccessibilityInteractionClient.getInstanceForThread(
interrogatingTid).setSameThreadMessage(message); // 同进程快捷路径
} else {
mHandler.sendMessage(message); // 常规:切到 UI 线程处理
}
}
private void findAccessibilityNodeInfoByAccessibilityIdUiThread(Message message) {
// ... 解包 ...
List<AccessibilityNodeInfo> infos = mTempAccessibilityNodeInfoList;
infos.clear();
try {
if (mViewRootImpl.mView == null || mViewRootImpl.mAttachInfo == null) {
return;
}
mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = flags;
View root = null;
if (accessibilityViewId == AccessibilityNodeInfo.UNDEFINED_ITEM_ID) {
root = mViewRootImpl.mView; // 查询根节点
} else {
root = findViewByAccessibilityId(accessibilityViewId); // O(1) 索引表定位
}
if (root != null && isShown(root)) {
mPrefetcher.prefetchAccessibilityNodeInfos(root, virtualDescendantId, flags, infos);
}
} finally {
// ... 应用缩放、可见性裁剪 ...
callback.setFindAccessibilityNodeInfosResult(infos, interactionId); // 回调唤醒客户端
}
}
}
预取器是性能的功臣------node.getChild() 表面上查一个节点,实际一次带回一片:
java
private final class AccessibilityPrefetcher {
private static final int MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE = 50;
public void prefetchAccessibilityNodeInfos(View view, int virtualViewId,
int fetchFlags, List<AccessibilityNodeInfo> outInfos) {
AccessibilityNodeProvider provider = view.getAccessibilityNodeProvider();
if (provider == null) {
AccessibilityNodeInfo root = view.createAccessibilityNodeInfo();
if (root != null) {
outInfos.add(root);
if ((fetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_PREDECESSORS) != 0) {
prefetchPredecessorsOfRealNode(view, outInfos); // 一路向上取祖先
}
if ((fetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_SIBLINGS) != 0) {
prefetchSiblingsOfRealNode(view, outInfos);
}
if ((fetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS) != 0) {
prefetchDescendantsOfRealNode(view, outInfos); // 递归取子孙(上限 50)
}
}
} else {
// 虚拟节点分支:provider.createAccessibilityNodeInfo(virtualViewId) 等
}
if (ENFORCE_NODE_TREE_CONSISTENT) {
enforceNodeTreeConsistent(outInfos); // 开发期校验:重复节点/重复焦点直接抛异常
}
}
// ...
}
关键设计 :TalkBack 线性浏览时念完一个控件问"下一个是什么"------大概率是兄弟或子孙。预取让这些后续查询全部命中缓存,总 Binder 次数从 O(节点数) 降到 O(子树数) 。上限 50 防止一个嵌套地狱 View 把整批预算吃光。
FLAG_PREFETCH_SIBLINGS requires FLAG_PREFETCH_PREDECESSORS的参数校验也说明:兄弟预取没有祖先链就没有导航语义。
六、缓存:AccessibilityCache 与事件驱动失效
缓存是进程级单例 (sAccessibilityCache),按 (windowId, nodeId) 索引。关键在失效机制------它不能靠超时,必须靠事件:
java
// AccessibilityInteractionClient.java
public void onAccessibilityEvent(AccessibilityEvent event) {
sAccessibilityCache.onAccessibilityEvent(event);
// ...
}
public void clearCache() {
sAccessibilityCache.clear();
}
AccessibilityCache.onAccessibilityEvent() 按事件类型增量失效:
| 事件类型 | 缓存动作 |
|---|---|
TYPE_WINDOW_CONTENT_CHANGED |
精确失效来源节点子树 |
TYPE_WINDOW_STATE_CHANGED |
整窗失效,新窗口列表重建 |
TYPE_VIEW_SCROLLED 等 |
对应节点失效 |
这也解释了第四篇末尾的伏笔:IAccessibilityServiceClientWrapper 的 DO_ON_ACCESSIBILITY_EVENT 分支里,回调开发者之前先执行 AccessibilityInteractionClient.getInstance().onAccessibilityEvent(event)------事件先喂缓存,再给业务代码 。开发者 onAccessibilityEvent() 里调 getSource() 时,事件携带的节点信息已在缓存里,甚至不必跨进程。
全局失效由 AMS 触发:窗口切换、服务断连等场景,AMS 调用 mServiceInterface 的 Binder 方法通知客户端 clearAccessibilityCache()(DO_CLEAR_ACCESSIBILITY_CACHE 分支)。
七、写操作:performAction 的执行链
java
// ViewRootImpl.AccessibilityInteractionConnection(App 进程 Stub)
// performAccessibilityAction(...) → AccessibilityInteractionController:
public void performAccessibilityActionClientThread(long accessibilityNodeId,
int action, Bundle arguments, int interactionId,
IAccessibilityInteractionConnectionCallback callback,
int interrogatingPid, long interrogatingTid) {
// ... 打包消息,同线程判断与查询完全一致 ...
}
private void performAccessibilityActionUiThread(Message message) {
// ... 定位 View / provider ...
// 真正执行:
// succeeded = target.performAccessibilityAction(action, arguments)
// View 基类实现(部分):
// ACTION_CLICK → performClick()
// ACTION_LONG_CLICK → performLongClick()
// ACTION_FOCUS → requestFocus()
// ACTION_ACCESSIBILITY_FOCUS → requestAccessibilityFocus()
// ACTION_CLEAR_ACCESSIBILITY_FOCUS → clearAccessibilityFocus()
// ACTION_SELECT / SCROLL_FORWARD / SET_TEXT ... 各控件按能力实现
}
AMS 侧的 performAccessibilityAction()(IAccessibilityServiceConnection 的实现,第三篇第五节的 Service 类)在转发前执行 mSecurityPolicy.canPerformActionLocked() 系列校验------例如 ACTION_ACCESSIBILITY_FOCUS 只对声明了 canRetrieveWindowContent 的服务放行,触摸探索相关动作还要在 mTouchExplorationGrantedServices 白名单里(用户手动授权,第二篇配置表)。
八、完整链路回顾
AccessibilityService.onAccessibilityEvent(event) [服务进程]
→ event.getSource() / node.getChild(i) / getRootInActiveWindow()
→ AccessibilityInteractionClient.findAccessibilityNodeInfoByAccessibilityId()
→ ① sAccessibilityCache 命中 → 直接返回(绝大多数情况到此为止)
→ ② 未命中:单向 Binder + interactionId + 本地阻塞等待
IAccessibilityServiceConnection(AMS.Service) [system_server]
→ ③ mSecurityPolicy 权限校验(canGetAccessibilityNodeInfoLocked)
→ ④ resolveWindowId → mInteractionConnections.get(windowId)
IAccessibilityInteractionConnection(ViewRootImpl 内部类) [目标 App 进程]
→ ⑤ 同进程?setSameThreadMessage 快捷路径 : 切 UI 线程
→ ⑥ findViewByAccessibilityId(viewId) O(1) 定位
→ ⑦ Prefetcher:目标节点 + 祖先/兄弟/子孙(≤50)一次打包
→ ⑧ callback.setFindAccessibilityNodeInfosResult(infos, interactionId)
回到 ② 的等待点被唤醒 [服务进程]
→ ⑨ 批量入缓存,返回第一个;其余 recycle
← AccessibilityNodeInfo(属性 + 一片已缓存的邻域)
写路径 performAction 复用 ①~⑨ 的骨架,只是第 ⑦ 步换成 UI 线程执行动作并返回布尔结果。
九、本篇核心源码文件
| 文件 | 角色 |
|---|---|
frameworks/base/core/java/android/view/accessibility/AccessibilityNodeInfo.java |
节点结构、ID 编解码、预取标志 |
frameworks/base/core/java/android/view/accessibility/AccessibilityInteractionClient.java |
客户端查询代理、等待/唤醒、缓存入口 |
frameworks/base/core/java/android/view/accessibility/AccessibilityCache.java |
事件驱动失效的节点缓存 |
frameworks/base/core/java/android/view/ViewRootImpl.java |
窗口端点注册、AccessibilityInteractionConnection Stub |
frameworks/base/core/java/android/view/AccessibilityInteractionController.java |
App 进程查询调度、Prefetcher |
frameworks/base/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java |
中转、安全校验 |
十、小结
- ID 而非引用 :
(windowId, nodeId)二元组定位节点,nodeId = viewId | virtualDescendantId,跨进程、可缓存、可比较; - 三方 Binder:服务进程 → AMS(校验+路由)→ 目标 App(UI 线程构建),单向调用 + 回调 + interactionId 配对,同进程场景有零切换快捷路径;
- 预取 + 缓存:一次查询带回 ≤50 个节点的邻域,事件流驱动缓存精确失效------这是无障碍体验"不卡"的根基;
- 读写同构 :
performAction与查询共用同一套调度骨架,安全校验集中在 AMS 中转层。
下一篇转向输入侧:AccessibilityInputFilter 如何把触摸/按键事件先截给 TouchExplorer,手势识别为无障碍手势后如何经 AMS 回流到服务的 onGesture() 回调。