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 等 对应节点失效

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


相关推荐
千里马学框架4 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台4 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone4 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc4 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo4 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077004 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼4 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone4 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen4 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone4 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui