Android 7系统无障碍服务(五)AccessibilityNodeInfo 的构建与查询

系列目录第一篇:全景图与架构概览 | [第二篇: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;
    }
}

这段代码藏着三个精妙设计:

  1. 单向 Binder + 回调 + 本地等待 :查询不是同步 Binder 往返,而是"发出单向请求 → 阻塞等回调"。interactionId 配对请求与响应,getFindAccessibilityNodeInfosResultAndClear() 在超时窗口(5 秒)内等待,结果通过 setFindAccessibilityNodeInfosResult() 回调送达后唤醒。为什么这么绕?见下一条。
  2. 同进程快捷路径(setSameThreadMessage :目标 App 自己就是查询者(如 App 内用无障碍 API 自测)时,App 进程的处理消息若走 Binder 线程池再回主线程,而查询线程恰好就是主线程------会死锁。源码的解法是让 App 进程把"待处理消息"暂存在 AccessibilityInteractionClient.mSameThreadMessage,查询调用返回后同一线程立即取出来执行,全程零线程切换。
  3. 一次查询返回一批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 对应节点失效

这也解释了第四篇末尾的伏笔:IAccessibilityServiceClientWrapperDO_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() 回调。


相关推荐
今天要早睡_1 小时前
C++ 核心语法速过:命名空间、引用、函数重载与 nullptr 深度解析
android·java·c++
啦啦啦~~~2222 小时前
PC端+安卓端阅读器推荐!开源本地小说阅读器软件,
android·论文阅读·windows·开源软件·福昕阅读器
我命由我123453 小时前
Android Jetpack Compose 开发问题:Unresolved reference ‘Icons‘.
android·java-ee·kotlin·android studio·android jetpack·android-studio·android runtime
爱笑鱼3 小时前
Android 系统启动机制(六):Zygote 创建 App 为什么走 Socket?哪些是源码事实,哪些是架构解释?
android·linux
AI2中文网3 小时前
安卓模拟器启动卡在进度条?两步解决硬件加速冲突
android
IT毕设实战小研3 小时前
基于大数据的DAX40成分股金融新闻情感趋势可视化分析
android·大数据·python·考研·金融·课程设计
Android-Flutter4 小时前
flutter async/await 详解
android·flutter
步行cgn4 小时前
Spring Boot 中的 YAML 完全指南
android·spring boot·firefox
事圆则缓5 小时前
Java 常见数据结构与 Android 使用场景
android·java·数据结构