自动化工具 uiautomatorviewer 新增功能 compressed 之 Device 端细节

易寒 · 2015年01月16日 · 最后由 测试小书童 回复于 2016年05月03日 · 255 次阅读
本帖已被设为精华帖!

昨天了解了 uiautoamtorviewer 新增功能 dump --compressed,是一个直接发送到设备端的命令,那么这个命令发送到设备端后,设备端是如何操作的呢?我又成了 10 万个为什么了?继续源码研究......

源码地址

google source

这个 jar 包最新的版本只到了 4.4.2。说明 5.0 后的 uiautomator 设备端是没有改变的,那么说明 dump --compressed 之前就有,只是我不知道罢鸟。结论:

dump --compressed 命令 4.4.2 时代就有,只是年少无知没发现

源码环境搭建

解压以后项目结果如下所示:

直接用 eclipse 的 import 功能导入,整体导入。导入 eclipse 后,如下图所示,感叹号是因为没有添加android.jar造成的,加上就好了。

源码分析

当我们在命令行下输入下面命令的时候,android 系统就会调用cmds目录下的Launcher类中的main方法中

/system/bin/uiautomator dump --compressed /data/local/tmp/uidump.xml

Launcher

所以我们从 main 开始我们的大餐:

public static void main(String[] args) {
        // show a meaningful process name in `ps`
        Process.setArgV0("uiautomator");
        if (args.length >= 1) {
            Command command = findCommand(args[0]);
            if (command != null) {
                String[] args2 = {};
                if (args.length > 1) {
                    // consume the first arg
                    args2 = Arrays.copyOfRange(args, 1, args.length);
                }
                command.run(args2);
                return;
            }
        }
        HELP_COMMAND.run(args);
    }

下面一步一步解释上面的代码的意思:
1.首先在进程信息中添加上uiautomator信息,这样你在命令行中敲adb shell ps就能查看到uiautomator进程的信息了。
2.判断参数数量是否大于 0,其中要了解的是上面的命令中 dump 算第一个参数。不要把 system/bin/uiautmator 当成了第一个参数。
3.当参数数量大于 0 时,获得第一个参数的值 args[0],其中 findCommand() 方法根据命令的名称得到命令的类型。总共有四个命令:helpeventsruntestdump,你如果想知道各个命令是干什么的,你可以在命令行下敲一下看看输出就知道了。

private static Command[] COMMANDS = new Command[] {
        HELP_COMMAND,
        new RunTestCommand(),
        new DumpCommand(),
        new EventsCommand(),
    };

4.如果命令不为空,就要执行相应的命令,但是还要将剩下的参数 (可能为空的参数,但不是 null 值) 传入run方法中,让各个类型自己处理。我们的命令是 dump 命令,所以下一步进入DumpCommand中。
5.如果不带参数的话,直接执行help命令。

DumpCommand

public void run(String[] args) {
        File dumpFile = DEFAULT_DUMP_FILE;
        boolean verboseMode = true;

        for (String arg : args) {
            if (arg.equals("--compressed"))
                verboseMode = false;
            else if (!arg.startsWith("-")) {
                dumpFile = new File(arg);
            }
        }

        UiAutomationShellWrapper automationWrapper = new UiAutomationShellWrapper();
        automationWrapper.connect();
        if (verboseMode) {
            // default
            automationWrapper.setCompressedLayoutHierarchy(false);
        } else {
            automationWrapper.setCompressedLayoutHierarchy(true);
        }

        // It appears that the bridge needs time to be ready. Making calls to the
        // bridge immediately after connecting seems to cause exceptions. So let's also
        // do a wait for idle in case the app is busy.
        try {
            UiAutomation uiAutomation = automationWrapper.getUiAutomation();
            uiAutomation.waitForIdle(1000, 1000 * 10);
            AccessibilityNodeInfo info = uiAutomation.getRootInActiveWindow();
            if (info == null) {
                System.err.println("ERROR: null root node returned by UiTestAutomationBridge.");
                return;
            }

            Display display =
                    DisplayManagerGlobal.getInstance().getRealDisplay(Display.DEFAULT_DISPLAY);
            int rotation = display.getRotation();
            Point size = new Point();
            display.getSize(size);
            AccessibilityNodeInfoDumper.dumpWindowToFile(info, dumpFile, rotation, size.x, size.y);
        } catch (TimeoutException re) {
            System.err.println("ERROR: could not get idle state.");
            return;
        } finally {
            automationWrapper.disconnect();
        }
        System.out.println(
                String.format("UI hierchary dumped to: %s", dumpFile.getAbsolutePath()));
    }

run 方法执行的步骤有点长,没关系,慢慢来。
1.首先创建文件用来保存 dump 下来的信息,这个时候需要注意getLegacyExternalStorageDirectory是个隐藏的方法,官网上的 api 没有这个方法的解释,可以在源码上找到,我贴在这里,帮助理解,该文件的路径为/storage/emulated/legacy/window_dump.xml

/** {@hide} */
    public static File getLegacyExternalStorageDirectory() {
        return new File(System.getenv(ENV_EXTERNAL_STORAGE));
    }

2.然后解析传入的参数得到保存的路径以及是否压缩。
3.然后创建UiAutomationShellWrapper对象,启动 Handler 线程,创建 Uiautomation 对象,并建立连接。然后设置了压缩属性。这个UiAutomationShellWrapper也是隐藏的,也只能到源码环境下查看。
4.然后我们得到了 Uiautomation 的对象实例

UiAutomation uiAutomation = automationWrapper.getUiAutomation();
            uiAutomation.waitForIdle(1000, 1000 * 10);
            AccessibilityNodeInfo info = uiAutomation.getRootInActiveWindow();

等待 UI 界面处于稳定后 (idle 状态),然后我们调用getRootInActiveWindow方法获得结果的根节点。这个时候我们整个流程差不多结束了,我们 care 的--compressed 还没看到。

compressed

经过一路追踪,发现 compressed 属性在整个过程中的作用是给AccessibilityServiceInfo对象添加了一个 FLAG_INCLUDE_NOT_IMPORTANT_VIEWS 标志位。其他的就是正常获取 dump 信息流程,这个标志位对获取信息时候的影响有多大,留到以后来解释 (没有源码环境,不好调试啊,头疼......)。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 19 条回复 时间 点赞

好文. 这块我也不太懂, 我一直没有比较过压缩前还压缩后的元素差异. 不重要的元素到底指什么?

#1 楼 @seveniruby 一些容器类控件,比如一些 FrameLayout、LinearLayout 等

#2 楼 @doctorq 我也一直怀疑是这些. 但是没有证据.没有 linear 不太可能. 怀疑是会去掉 linear/linear 多层中的一个. 最基础的 linear 还是会保留. 你有跟对比的结果吗, 看看具体少了多少.

#3 楼 @seveniruby 对,刨除掉一些不太重要,有些是有保留的

全获取:
压缩版:

看了一下源码,但还没搭建好调试环境,所以跟踪到某一步后就跟踪不下去了。在此仅分享一下找的过程:
先接着 doctorq 的思路,去找 android 源码中的getRootInActiveWindow
android.app.UiAutomation

/**
 * Gets the root {@link AccessibilityNodeInfo} in the active window.
 *
 * @return The root info.
 */
public AccessibilityNodeInfo getRootInActiveWindow() {
    final int connectionId;
    synchronized (mLock) {
        throwIfNotConnectedLocked();
        connectionId = mConnectionId;
    }
    // Calling out without a lock held.
    return AccessibilityInteractionClient.getInstance()
            .getRootInActiveWindow(connectionId);
}

接着找 return 中的getRootInActiveWindow
android.view.acessibility.AccessibilityInteractionClient

/**
 * Gets the root {@link AccessibilityNodeInfo} in the currently active window.
 *
 * @param connectionId The id of a connection for interacting with the system.
 * @return The root {@link AccessibilityNodeInfo} if found, null otherwise.
 */
public AccessibilityNodeInfo getRootInActiveWindow(int connectionId) {
    return findAccessibilityNodeInfoByAccessibilityId(connectionId,
            AccessibilityNodeInfo.ACTIVE_WINDOW_ID, AccessibilityNodeInfo.ROOT_NODE_ID,
            false, AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS);
}

继续找findAccessibilityNodeInfoByAccessibilityId:
android.view.acessibility.AccessibilityInteractionClient

/**
     * Finds an {@link AccessibilityNodeInfo} by accessibility id.
     *
     * @param connectionId The id of a connection for interacting with the system.
     * @param accessibilityWindowId A unique window id. Use
     *     {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID}
     *     to query the currently active window.
     * @param accessibilityNodeId A unique view id or virtual descendant id from
     *     where to start the search. Use
     *     {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
     *     to start from the root.
     * @param bypassCache Whether to bypass the cache while looking for the node.
     * @param prefetchFlags flags to guide prefetching.
     * @return An {@link AccessibilityNodeInfo} if found, null otherwise.
     */
    public AccessibilityNodeInfo findAccessibilityNodeInfoByAccessibilityId(int connectionId,
            int accessibilityWindowId, long accessibilityNodeId, boolean bypassCache,
            int prefetchFlags) {
        try {
            IAccessibilityServiceConnection connection = getConnection(connectionId);
            if (connection != null) {
                if (!bypassCache) {
                    AccessibilityNodeInfo cachedInfo = sAccessibilityNodeInfoCache.get(
                            accessibilityNodeId);
                    if (cachedInfo != null) {
                        return cachedInfo;
                    }
                }
                final int interactionId = mInteractionIdCounter.getAndIncrement();
                final boolean success = connection.findAccessibilityNodeInfoByAccessibilityId(
                        accessibilityWindowId, accessibilityNodeId, interactionId, this,
                        prefetchFlags, Thread.currentThread().getId());
                // If the scale is zero the call has failed.
                if (success) {
                    List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear(
                            interactionId);
                    finalizeAndCacheAccessibilityNodeInfos(infos, connectionId);
                    if (infos != null && !infos.isEmpty()) {
                        return infos.get(0);
                    }
                }
            } else {
                if (DEBUG) {
                    Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
                }
            }
        } catch (RemoteException re) {
            if (DEBUG) {
                Log.w(LOG_TAG, "Error while calling remote"
                        + " findAccessibilityNodeInfoByAccessibilityId", re);
            }
        }
        return null;
    }

注意中间的connection.findAccessibilityNodeInfoByAccessibilityId,我用的 IDE 是 Android Studio,findAccessibilityNodeInfoByAccessibilityId方法显示为红色,表示找不到它的声明位置。
然后我们看看 connection 的声明:
·IAccessibilityServiceConnection connection = getConnection(connectionId);·
它是通过getConnection(connectionId)获得的实例,继续找:

public IAccessibilityServiceConnection getConnection(int connectionId) {
    synchronized (sConnectionCache) {
        return sConnectionCache.get(connectionId);
    }
}

再找sConnectionCache

// The connection cache is shared between all interrogating threads.
private static final SparseArray<IAccessibilityServiceConnection> sConnectionCache =
    new SparseArray<IAccessibilityServiceConnection>();

继续IAccessibilityServiceConnection,这时候只能找到一个相关的 import 语句了:

import android.accessibilityservice.IAccessibilityServiceConnection;

到这里为止,寻找方法声明位置的这种方法找不下去了,因为源码里找不到IAccessibilityServiceConnection声明的位置。
大致猜测是本来每个 Node 其实都带有一定的信息 (具体有哪些信息可以在android.accessibilityservice.AccessibilityServiceInfo找到) 表示它是否影响显示,--compress 插入的标志位会在存储 Node 时不存储这些不影响显示的 Node。具体是如何实现的等研究好如何搭建调试环境后继续研究。

#6 楼 @chenhengjie123 IAccessibilityServiceConnection 应该是一个 aidl

@doctorq 谢谢指导,之前还真没了解过 aidl。今晚研究一下。

#8 楼 @chenhengjie123 静待你的研究成果

终于勉强有一个调试环境了。因为完整的 android 源码下载下来太慢了,所以我采用远程调试 uiautomator 脚本的方法进行调试,缺少的 IAccessibilityServiceConnection 文件也在GrepCode找到了(是编译后的.java 文件,对于这种调试基本足够了)。
先附上 uiautomator 脚本的关键代码:

...
        UiDevice device = getUiDevice();
        device.waitForIdle();

        //set compress, the same as what --compress did
        device.setCompressedLayoutHeirarchy(true);

        //dump Hierarchy file
        device.dumpWindowHierarchy("dumpFromEclipse.xml");
...

开始调试过程。这里我主要把关键过程的调试信息附上:

setCompressedLayoutHierarchy

device.setCompressedLayoutHeirarchy(true);方法代码:

/**
 * Enables or disables layout hierarchy compression.
 *
 * If compression is enabled, the layout hierarchy derived from the Acessibility
 * framework will only contain nodes that are important for uiautomator
 * testing. Any unnecessary surrounding layout nodes that make viewing
 * and searching the hierarchy inefficient are removed.
 *
 * @param compressed true to enable compression; else, false to disable
 * @since API Level 18
 */
public void setCompressedLayoutHeirarchy(boolean compressed) {
    getAutomatorBridge().setCompressedLayoutHierarchy(compressed);
}

这里getAutomatorBridge()获取了对QueryControllerInteractionController的访问连接,然后调用了它的setCompressedLayoutHierarchy(compressed)方法。

public void setCompressedLayoutHierarchy(boolean compressed) {
    AccessibilityServiceInfo info = mUiAutomation.getServiceInfo();
    if (compressed)
        info.flags &= ~AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
    else
        info.flags |= AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
    mUiAutomation.setServiceInfo(info);
}

在调试时看到:

  1. 在设定AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS标志位前,info.flags=18
  2. 设定后 info.flags=16(这里使用的是位运算。关于位运算的资料可以查看位运算简介及实用技巧(一):基础篇
  3. setServiceInfo方法会把添加标志位后的info发给AccessibilityInteractionClient:

    /**
     * Sets the {@link AccessibilityServiceInfo} that describes how this
     * UiAutomation will be handled by the platform accessibility layer.
     *
     * @param info The info.
     *
     * @see AccessibilityServiceInfo
     */
    public final void setServiceInfo(AccessibilityServiceInfo info) {
        final IAccessibilityServiceConnection connection;
        synchronized (mLock) {
            throwIfNotConnectedLocked();
            AccessibilityInteractionClient.getInstance().clearCache();
            connection = AccessibilityInteractionClient.getInstance()
                    .getConnection(mConnectionId);
        }
        // Calling out without a lock held.
        if (connection != null) {
            try {
                connection.setServiceInfo(info);
            } catch (RemoteException re) {
                Log.w(LOG_TAG, "Error while setting AccessibilityServiceInfo", re);
            }
        }
    }
    

    getConnection()的返回值类型为IAccessibilityServiceConnection,通过 find in path,得知com.android.server.accessibility.AccessibilityManagerService中的Service类继承并实现了IAccessibilityServiceConnection.Stub。其中setService源码如下:

    @Override
    public void setServiceInfo(AccessibilityServiceInfo info) {
        final long identity = Binder.clearCallingIdentity();
        try {
            synchronized (mLock) {
                // If the XML manifest had data to configure the service its info
                // should be already set. In such a case update only the dynamically
                // configurable properties.
                AccessibilityServiceInfo oldInfo = mAccessibilityServiceInfo;
                if (oldInfo != null) {
                    oldInfo.updateDynamicallyConfigurableProperties(info);
                    setDynamicallyConfigurableProperties(oldInfo);
                } else {
                    setDynamicallyConfigurableProperties(info);
                }
                UserState userState = getUserStateLocked(mUserId);
                onUserStateChangedLocked(userState);
            }
        } finally {
            Binder.restoreCallingIdentity(identity);
        }
    }
    

    这里主要做的事情就是执行了setDynamicallyConfigurableProperties方法来更新infosetDynamicallyConfigurableProperties方法源码如下:

    public void setDynamicallyConfigurableProperties(AccessibilityServiceInfo info) {
            mEventTypes = info.eventTypes;
            mFeedbackType = info.feedbackType;
            String[] packageNames = info.packageNames;
            if (packageNames != null) {
                mPackageNames.addAll(Arrays.asList(packageNames));
            }
            mNotificationTimeout = info.notificationTimeout;
            mIsDefault = (info.flags & DEFAULT) != 0;
    
            if (mIsAutomation || info.getResolveInfo().serviceInfo.applicationInfo.targetSdkVersion
                    >= Build.VERSION_CODES.JELLY_BEAN) {
                if ((info.flags & AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS) != 0) {
                    mFetchFlags |= AccessibilityNodeInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
                } else {
                    mFetchFlags &= ~AccessibilityNodeInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
                }
            }
    
            if ((info.flags & AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS) != 0) {
                mFetchFlags |= AccessibilityNodeInfo.FLAG_REPORT_VIEW_IDS;
            } else {
                mFetchFlags &= ~AccessibilityNodeInfo.FLAG_REPORT_VIEW_IDS;
            }
    
            mRequestTouchExplorationMode = (info.flags
                    & AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE) != 0;
            mRequestEnhancedWebAccessibility = (info.flags
                    & AccessibilityServiceInfo.FLAG_REQUEST_ENHANCED_WEB_ACCESSIBILITY) != 0;
            mRequestFilterKeyEvents = (info.flags
                    & AccessibilityServiceInfo.FLAG_REQUEST_FILTER_KEY_EVENTS)  != 0;
        }
    

    这里做的事情就是根据 info 更新局部变量,如mEventTyesmFetchFlags。其中mFetchFlags同样检查了AccessibilityNodeInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS。当info.flag中含有FLAG_INCLUDE_NOT_IMPORTANT_VIEWS时,执行mFetchFlags &= ~AccessibilityNodeInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;,把这个标志位添加到mFetchFlags中。
    至此,device.setCompressedLayoutHeirarchy(true);完成任务了,AccessibilityInteractionClient已经知道后面执行的操作都基于设定了AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS标志的前提下

device.dumpWindowHierarchy("dumpFromEclipse.xml");

首先进入dumpWindowHierarchy

/**
 * Helper method used for debugging to dump the current window's layout hierarchy.
 * The file root location is /data/local/tmp
 *
 * @param fileName
 * @since API Level 16
 */
public void dumpWindowHierarchy(String fileName) {
    Tracer.trace(fileName);
    AccessibilityNodeInfo root =
            getAutomatorBridge().getQueryController().getAccessibilityRootNode();
    if(root != null) {
        Display display = getAutomatorBridge().getDefaultDisplay();
        Point size = new Point();
        display.getSize(size);
        AccessibilityNodeInfoDumper.dumpWindowToFile(root,
                new File(new File(Environment.getDataDirectory(), "local/tmp"), fileName),
                display.getRotation(), size.x, size.y);
    }
}

Tracer.trace(fileName);与找 node 的过程无关,略过,然后我们执行到AccessibilityNodeInfo root =getAutomatorBridge().getQueryController().getAccessibilityRootNode();

  1. getAutomatorBridge()和第一步一样,获取连接
  2. getQueryController()获取QueryController实例
  3. getAccessibilityRootNode()这是重点。咱们进去看看:

getAccessibilityRootNode

源码:

public AccessibilityNodeInfo getAccessibilityRootNode() {
    return mUiAutomatorBridge.getRootInActiveWindow();
}

这里开始和我上面的getRootInActiveWindowconnection.findAccessibilityNodeInfoByAccessibilityId的过程是一致的。其中findAccessibilityNodeInfoByAccessibilityId方法比较复杂,因此从这里开始详述:

/**
     * Finds an {@link AccessibilityNodeInfo} by accessibility id.
     *
     * @param connectionId The id of a connection for interacting with the system.
     * @param accessibilityWindowId A unique window id. Use
     *     {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID}
     *     to query the currently active window.
     * @param accessibilityNodeId A unique view id or virtual descendant id from
     *     where to start the search. Use
     *     {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
     *     to start from the root.
     * @param bypassCache Whether to bypass the cache while looking for the node.
     * @param prefetchFlags flags to guide prefetching.
     * @return An {@link AccessibilityNodeInfo} if found, null otherwise.
     */
    public AccessibilityNodeInfo findAccessibilityNodeInfoByAccessibilityId(int connectionId,
            int accessibilityWindowId, long accessibilityNodeId, boolean bypassCache,
            int prefetchFlags) {
        try {
            IAccessibilityServiceConnection connection = getConnection(connectionId);
            if (connection != null) {
                if (!bypassCache) {
                    AccessibilityNodeInfo cachedInfo = sAccessibilityNodeInfoCache.get(
                            accessibilityNodeId);
                    if (cachedInfo != null) {
                        return cachedInfo;
                    }
                }
                final int interactionId = mInteractionIdCounter.getAndIncrement();
                final boolean success = connection.findAccessibilityNodeInfoByAccessibilityId(
                        accessibilityWindowId, accessibilityNodeId, interactionId, this,
                        prefetchFlags, Thread.currentThread().getId());
                // If the scale is zero the call has failed.
                if (success) {
                    List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear(
                            interactionId);
                    finalizeAndCacheAccessibilityNodeInfos(infos, connectionId);
                    if (infos != null && !infos.isEmpty()) {
                        return infos.get(0);
                    }
                }
            } else {
                if (DEBUG) {
                    Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
                }
            }
        } catch (RemoteException re) {
            if (DEBUG) {
                Log.w(LOG_TAG, "Error while calling remote"
                        + " findAccessibilityNodeInfoByAccessibilityId", re);
            }
        }
        return null;
    }

咱们一行一行来:

IAccessibilityServiceConnection connection = getConnection(connectionId);

获取连接。获取后 connection 不是 null,且 bypassCache 为 false,因此跑到这一行:

AccessibilityNodeInfo cachedInfo = sAccessibilityNodeInfoCache.get(
        accessibilityNodeId);

调试过程中发现缓存中没有对应信息,cachedInfo 为 null,因此进入这一行:

final int interactionId = mInteractionIdCounter.getAndIncrement();

获取了 interactionId,值为 0。进入下一行

final boolean success = connection.findAccessibilityNodeInfoByAccessibilityId(
        accessibilityWindowId, accessibilityNodeId, interactionId, this,
        prefetchFlags, Thread.currentThread().getId());

这里做的事情比较多。为方便后面说明,先把目前的各变量及对应值贴一下:

然后咱们进入之前卡住了的connection.findAccessibilityNodeInfoByAccessibilityId

@Override
public boolean findAccessibilityNodeInfoByAccessibilityId(
    int accessibilityWindowId, long accessibilityNodeId,
    int interactionId,
    android.view.accessibility.IAccessibilityInteractionConnectionCallback callback,
    int flags, long threadId) throws android.os.RemoteException {
    android.os.Parcel _data = android.os.Parcel.obtain();
    android.os.Parcel _reply = android.os.Parcel.obtain();
    boolean _result;

    try {
        _data.writeInterfaceToken(DESCRIPTOR);
        _data.writeInt(accessibilityWindowId);
        _data.writeLong(accessibilityNodeId);
        _data.writeInt(interactionId);
        _data.writeStrongBinder((((callback != null))? (callback.asBinder()) : (null)));
        _data.writeInt(flags);
        _data.writeLong(threadId);
        mRemote.transact(Stub.TRANSACTION_findAccessibilityNodeInfoByAccessibilityId,
            _data, _reply, 0);
        _reply.readException();
        _result = (0 != _reply.readInt());
    } finally {
        _reply.recycle();
        _data.recycle();
    }

    return _result;
}

开始三行获取了 2 个Parcel实例 (Parcel 是一个存储通过 IBinder 传输的消息的容器)_data_reply,创建了一个 boolean 类型的变量_result
然后开始往_data写入数据:

_data.writeInterfaceToken(DESCRIPTOR);
_data.writeInt(accessibilityWindowId);
_data.writeLong(accessibilityNodeId);
_data.writeInt(interactionId);
_data.writeStrongBinder((((callback != null))? (callback.asBinder()) : (null)));
_data.writeInt(flags);
_data.writeLong(threadId);

执行完后各数据的值:

其中DESCRIPTOR的值如下:

private static final java.lang.String DESCRIPTOR = "android.accessibilityservice.IAccessibilityServiceClient";

然后执行mRemote.transact(Stub.TRANSACTION_findAccessibilityNodeInfoByAccessibilityId, _data, _reply, 0);
此时 step into 进入不了这个方法(我用的是 remote debug,这部分代码的内容已经不在 build path 里面了,所以跟不进去),直接去到下一个语句_reply.readException();。此时 callback 的内部变量值有了变化,增加了两个变量:


可以看到mFindAccessibilityNodeInfosResult里面已经含有各个 node 的相关信息了。而且这里的信息已经 compress 过了,所以过滤是在mRemote.transact里面做的。

接下来我们看看这里实际做了什么了。
还是在IAccessibilityServiceConnection里面:

@Override
public boolean onTransact(int code, android.os.Parcel data,
    android.os.Parcel reply, int flags)
    throws android.os.RemoteException {
    switch (code) {
    ...

    case TRANSACTION_findAccessibilityNodeInfoByAccessibilityId: {
        data.enforceInterface(DESCRIPTOR);

        int _arg0;
        _arg0 = data.readInt();

        long _arg1;
        _arg1 = data.readLong();

        int _arg2;
        _arg2 = data.readInt();

        android.view.accessibility.IAccessibilityInteractionConnectionCallback _arg3;
        _arg3 = android.view.accessibility.IAccessibilityInteractionConnectionCallback.Stub.asInterface(data.readStrongBinder());

        int _arg4;
        _arg4 = data.readInt();

        long _arg5;
        _arg5 = data.readLong();

        boolean _result = this.findAccessibilityNodeInfoByAccessibilityId(_arg0,
                _arg1, _arg2, _arg3, _arg4, _arg5);
        reply.writeNoException();
        reply.writeInt(((_result) ? (1) : (0)));

        return true;
    }

从这里开始由于没有实际调试,主要是猜测。大家看看就好。等我下载完完整的 android source code 后才能调试下去。

这里data.enforceInterface(DESCRIPTOR);进行了数据的解包,然后把各个数据分别放到_arg0~_arg5中,其中_arg3=android.view.accessibility.IAccessibilityInteractionConnectionCallback.Stub.asInterface(data.readStrongBinder());把 callback(getRootInActiveWindowfindAccessibilityNodeInfoByAccessibilityId)转变成IAccessibilityInteractionConnectionCallback的 instance(相当于findAccessibilityNodeInfoByAccessibilityIdIAccessibilityInteractionConnectionCallback的实现)。然后调用了findAccessibilityNodeInfoByAccessibilityId(此处调用的应该是继承了IAccessibilityInteractionConnection.Stub的实现类,通过查找源码发现在com.android.server.accessibility.AccessibilityManagerService里面),并把结果写到 reply 中。
查看com.android.server.accessibility.AccessibilityManagerService相关源码:

@Override
        public boolean findAccessibilityNodeInfoByAccessibilityId(
                int accessibilityWindowId, long accessibilityNodeId, int interactionId,
                IAccessibilityInteractionConnectionCallback callback, int flags,
                long interrogatingTid) throws RemoteException {
            final int resolvedWindowId;
            IAccessibilityInteractionConnection connection = null;
            synchronized (mLock) {
                final int resolvedUserId = mSecurityPolicy
                        .resolveCallingUserIdEnforcingPermissionsLocked(
                        UserHandle.getCallingUserId());
                if (resolvedUserId != mCurrentUserId) {
                    return false;
                }
                mSecurityPolicy.enforceCanRetrieveWindowContent(this);
                resolvedWindowId = resolveAccessibilityWindowIdLocked(accessibilityWindowId);
                final boolean permissionGranted =
                    mSecurityPolicy.canGetAccessibilityNodeInfoLocked(this, resolvedWindowId);
                if (!permissionGranted) {
                    return false;
                } else {
                    connection = getConnectionLocked(resolvedWindowId);
                    if (connection == null) {
                        return false;
                    }
                }
            }
            final int interrogatingPid = Binder.getCallingPid();
            final long identityToken = Binder.clearCallingIdentity();
            MagnificationSpec spec = getCompatibleMagnificationSpec(resolvedWindowId);
            try {
                connection.findAccessibilityNodeInfoByAccessibilityId(accessibilityNodeId,
                        interactionId, callback, mFetchFlags | flags, interrogatingPid,
                        interrogatingTid, spec);
                return true;
            } catch (RemoteException re) {
                if (DEBUG) {
                    Slog.e(LOG_TAG, "Error calling findAccessibilityNodeInfoByAccessibilityId()");
                }
            } finally {
                Binder.restoreCallingIdentity(identityToken);
            }
            return false;
        }

这里的connection和之前的不一样,它是通过getConnectionLocked获得的。查看getConnectionLocked

private IAccessibilityInteractionConnection getConnectionLocked(int windowId) {
    if (DEBUG) {
        Slog.i(LOG_TAG, "Trying to get interaction connection to windowId: " + windowId);
    }
    AccessibilityConnectionWrapper wrapper = mGlobalInteractionConnections.get(windowId);
    if (wrapper == null) {
        wrapper = getCurrentUserStateLocked().mInteractionConnections.get(windowId);
    }
    if (wrapper != null && wrapper.mConnection != null) {
        return wrapper.mConnection;
    }
    if (DEBUG) {
        Slog.e(LOG_TAG, "No interaction connection to window: " + windowId);
    }
    return null;
}

这里的 connection 是IAccessibilityInteractionConnection类型的,而前面的 connection 则是IAccessibilityServiceConnection类型的。那么,这里的findAccessibilityNodeInfoByAccessibilityId具体做了什么?留待搭建完完整的 android source code 调试环境后继续研究。

易寒 #11 · 2015年02月08日 Author

#10 楼 @chenhengjie123 这个远程调试很屌啊,debug 的功底也不弱,佩服。期待关于标志位 FLAG_INCLUDE_NOT_IMPORTANT_VIEWS 对 dump 更深的解释,看谁快啊~~~~

@doctorq 好,不过估计你会比较快。我是边看边查相关资料,看得很慢的。

易寒 #13 · 2015年02月09日 Author

#12 楼 @chenhengjie123 我也一样,如果会的东西,我就懒得研究了。只有不会的我才有兴趣追

今天终于把真正查 node 的函数找到了,不过还没完全搞懂查 node 的过程和 compress 在中间造成的不同。先把找的过程贴上来一下:
提醒一下:前文的connection.findAccessibilityNodeInfoByAccessibilityId中传的 flag 是进行了一个运算后再传给findAccessibilityNodeInfoByAccessibilityIdmFetchFlags | flags,这里其实就是把我们之前预先告诉AccessibilityInteractionClient要添加的AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS信息加入到 flags 中。

然后 find in path,找到IAccessibilityServiceConnection的实现类android.view.ViewRootImpl

/**
     * This class is an interface this ViewAncestor provides to the
     * AccessibilityManagerService to the latter can interact with
     * the view hierarchy in this ViewAncestor.
     */
    static final class AccessibilityInteractionConnection
            extends IAccessibilityInteractionConnection.Stub {
        private final WeakReference<ViewRootImpl> mViewRootImpl;

        AccessibilityInteractionConnection(ViewRootImpl viewRootImpl) {
            mViewRootImpl = new WeakReference<ViewRootImpl>(viewRootImpl);
        }

        @Override
        public void findAccessibilityNodeInfoByAccessibilityId(long accessibilityNodeId,
                int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags,
                int interrogatingPid, long interrogatingTid, MagnificationSpec spec) {
            ViewRootImpl viewRootImpl = mViewRootImpl.get();
            if (viewRootImpl != null && viewRootImpl.mView != null) {
                viewRootImpl.getAccessibilityInteractionController()
                    .findAccessibilityNodeInfoByAccessibilityIdClientThread(accessibilityNodeId,
                            interactionId, callback, flags, interrogatingPid, interrogatingTid,
                            spec);
            } else {
                // We cannot make the call and notify the caller so it does not wait.
                try {
                    callback.setFindAccessibilityNodeInfosResult(null, interactionId);
                } catch (RemoteException re) {
                    /* best effort - ignore */
                }
            }
        }

这里第一次出现了callback。当 viewRootImpl 为 null 或 viewRootImpl.mView 为 null 时,调用了callback.setFindAccessibilityNodeInfoResult方法,把 null 作为结果返回给callback了。在异步编程里面,callback 的作用相当于同步编程的 return,把处理结果返回给这个函数的调用者。这个函数名称是不是很眼熟?对,它在AccessibilityInteractionClient里出现过,就是咱们一开始建立 connection 并调用其findAccessibilityNodeInfoByAccessibilityId方法那里,同时咱们前面调试时存储mFindAccessibilityNodeInfosResult结果的也是这个类。于是咱们跟踪进去看看
此时callbackIAccessibilityInteractionConnectionCallback类型的,find in path 发现android.view.accessibility.AccessibilityInteractionClient刚好实现了IAccessibilityInteractionConnectionCallback,其中setsetFindAccessibilityNodeInfosResult方法源码如下:

/**
     * {@inheritDoc}
     */
    public void setFindAccessibilityNodeInfosResult(List<AccessibilityNodeInfo> infos,
                int interactionId) {
        synchronized (mInstanceLock) {
            if (interactionId > mInteractionId) {
                if (infos != null) {
                    // If the call is not an IPC, i.e. it is made from the same process, we need to
                    // instantiate new result list to avoid passing internal instances to clients.
                    final boolean isIpcCall = (Binder.getCallingPid() != Process.myPid());
                    if (!isIpcCall) {
                        mFindAccessibilityNodeInfosResult =
                            new ArrayList<AccessibilityNodeInfo>(infos);
                    } else {
                        mFindAccessibilityNodeInfosResult = infos;
                    }
                } else {
                    mFindAccessibilityNodeInfosResult = Collections.emptyList();
                }
                mInteractionId = interactionId;
            }
            mInstanceLock.notifyAll();
        }
    }

看到了吗?这里把第一个参数 infos 赋给了 mFindAccessibilityNodeInfosResult,也就是说,就是这里把结果存储到mFindAccessibilityNodeInfosResult的。但此时因为 info 是 null,所以其实存的是一个空列表,和我们调试看到的不一样。
好了,先记下来,callback.setFindAccessibilityNodeInfosResult就是把找到的结果存储到mFindAccessibilityNodeInfosResult的函数,下次见到它记得多留意一下。

咱们回到android.view.ViewRootImplfindAccessibilityNodeInfoByAccessibilityId方法:

/**
     * This class is an interface this ViewAncestor provides to the
     * AccessibilityManagerService to the latter can interact with
     * the view hierarchy in this ViewAncestor.
     */
    static final class AccessibilityInteractionConnection
            extends IAccessibilityInteractionConnection.Stub {
        private final WeakReference<ViewRootImpl> mViewRootImpl;

        AccessibilityInteractionConnection(ViewRootImpl viewRootImpl) {
            mViewRootImpl = new WeakReference<ViewRootImpl>(viewRootImpl);
        }

        @Override
        public void findAccessibilityNodeInfoByAccessibilityId(long accessibilityNodeId,
                int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags,
                int interrogatingPid, long interrogatingTid, MagnificationSpec spec) {
            ViewRootImpl viewRootImpl = mViewRootImpl.get();
            if (viewRootImpl != null && viewRootImpl.mView != null) {
                viewRootImpl.getAccessibilityInteractionController()
                    .findAccessibilityNodeInfoByAccessibilityIdClientThread(accessibilityNodeId,
                            interactionId, callback, flags, interrogatingPid, interrogatingTid,
                            spec);
            } else {
                // We cannot make the call and notify the caller so it does not wait.
                try {
                    callback.setFindAccessibilityNodeInfosResult(null, interactionId);
                } catch (RemoteException re) {
                    /* best effort - ignore */
                }
            }
        }

前面的分析说明函数没有跑到 else 那里,那咱们直接看一下 if 里面的语句:

viewRootImpl.getAccessibilityInteractionController()
    .findAccessibilityNodeInfoByAccessibilityIdClientThread(accessibilityNodeId,
            interactionId, callback, flags, interrogatingPid, interrogatingTid,
            spec);

这里的getAccessibilityInteractionController就是获取了一个AccessibilityInteractionController的实例,没有对flags进行任何运算,略过。
findAccessibilityNodeInfoByAccessibilityIdClientThreadAccessibilityInteractionController的子方法,源码如下:

public void findAccessibilityNodeInfoByAccessibilityIdClientThread(
            long accessibilityNodeId, int interactionId,
            IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid,
            long interrogatingTid, MagnificationSpec spec) {
        Message message = mHandler.obtainMessage();
        message.what = PrivateHandler.MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID;
        message.arg1 = flags;

        SomeArgs args = SomeArgs.obtain();
        args.argi1 = AccessibilityNodeInfo.getAccessibilityViewId(accessibilityNodeId);
        args.argi2 = AccessibilityNodeInfo.getVirtualDescendantId(accessibilityNodeId);
        args.argi3 = interactionId;
        args.arg1 = callback;
        args.arg2 = spec;
        message.obj = args;

        // 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);
        }
    }

这里做的事情就是把传进来的参数封装到message里面,然后把这个message放到一个队列里面(这里做了一个线程判断,如果是同一线程调用setSameThreadMessage,否则mHandler.sendMessage)。这是 Android IPC(进程间通讯)中的 Messager 通讯方式,这个队列会被 looper 不断查找,如果有内容则逐个交给 handler 处理。因此跟进去sendMessage没有意义(它只是放到队列里面,具体执行不关它的事)。我们留意到这里设了一个作为标识变量:message.what = PrivateHandler.MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID;

既然实际上处理的函数是 handler,那么咱们去找 handler。
AccessibilityInteractionController的构造函数中,我们发现了 mHandler 的初始化语句:

public AccessibilityInteractionController(ViewRootImpl viewRootImpl) {
        Looper looper =  viewRootImpl.mHandler.getLooper();
        mMyLooperThreadId = looper.getThread().getId();
        mMyProcessId = Process.myPid();
        mHandler = new PrivateHandler(looper);
        mViewRootImpl = viewRootImpl;
        mPrefetcher = new AccessibilityNodePrefetcher();
    }

它是一个PrivateHandler的实例,所以它使用的 handler 应该也是PrivateHandler里的 handler。
根据 Android IPC 里面 Messager 的介绍,sendMessage后的执行是由 handler 负责的,因此咱们直接去看看PrivateHandler里面的handleMessage方法:

private class PrivateHandler extends Handler {
        private final static int MSG_PERFORM_ACCESSIBILITY_ACTION = 1;
        private final static int MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID = 2;
        private final static int MSG_FIND_ACCESSIBLITY_NODE_INFOS_BY_VIEW_ID = 3;
        private final static int MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT = 4;
        private final static int MSG_FIND_FOCUS = 5;
        private final static int MSG_FOCUS_SEARCH = 6;

        public PrivateHandler(Looper looper) {
            super(looper);
        }

        ...

        @Override
        public void handleMessage(Message message) {
            final int type = message.what;
            switch (type) {
                case MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID: {
                    findAccessibilityNodeInfoByAccessibilityIdUiThread(message);
                } break;
                case MSG_PERFORM_ACCESSIBILITY_ACTION: {
                    perfromAccessibilityActionUiThread(message);
                } break;
                case MSG_FIND_ACCESSIBLITY_NODE_INFOS_BY_VIEW_ID: {
                    findAccessibilityNodeInfosByViewIdUiThread(message);
                } break;
                case MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_TEXT: {
                    findAccessibilityNodeInfosByTextUiThread(message);
                } break;
                case MSG_FIND_FOCUS: {
                    findFocusUiThread(message);
                } break;
                case MSG_FOCUS_SEARCH: {
                    focusSearchUiThread(message);
                } break;
                default:
                    throw new IllegalArgumentException("Unknown message type: " + type);
            }
        }
    }

这里由于我们的message.whatPrivateHandler.MSG_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID,因此执行findAccessibilityNodeInfoByAccessibilityIdUiThread

private void findAccessibilityNodeInfoByAccessibilityIdUiThread(Message message) {
        final int flags = message.arg1;

        SomeArgs args = (SomeArgs) message.obj;
        final int accessibilityViewId = args.argi1;
        final int virtualDescendantId = args.argi2;
        final int interactionId = args.argi3;
        final IAccessibilityInteractionConnectionCallback callback =
            (IAccessibilityInteractionConnectionCallback) args.arg1;
        final MagnificationSpec spec = (MagnificationSpec) args.arg2;

        args.recycle();

        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) {
                root = mViewRootImpl.mView;
            } else {
                root = findViewByAccessibilityId(accessibilityViewId);
            }
            if (root != null && isShown(root)) {
                mPrefetcher.prefetchAccessibilityNodeInfos(root, virtualDescendantId, flags, infos);
            }
        } finally {
            try {
                mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = 0;
                applyAppScaleAndMagnificationSpecIfNeeded(infos, spec);
                if (spec != null) {
                    spec.recycle();
                }
                callback.setFindAccessibilityNodeInfosResult(infos, interactionId);
                infos.clear();
            } catch (RemoteException re) {
                /* ignore - the other side will time out */
            }
        }
    }

这里我们又见到了callback.setFindAccessibilityNodeInfosResult方法,因此可以确定找 node 的具体工作都是这里来完成的,自然过滤也是。
这里的过程比较复杂,目前还没完全看懂。后面看懂后再把分析贴上来。

今天看了findAccessibilityNodeInfoByAccessibilityIdUiThread,虽然还没完全了解它的流程,但基本找到--compress影响的位置了。
这里
先回到上次的地方:

private void findAccessibilityNodeInfoByAccessibilityIdUiThread(Message message) {
        final int flags = message.arg1;

        SomeArgs args = (SomeArgs) message.obj;
        final int accessibilityViewId = args.argi1;
        final int virtualDescendantId = args.argi2;
        final int interactionId = args.argi3;
        final IAccessibilityInteractionConnectionCallback callback =
            (IAccessibilityInteractionConnectionCallback) args.arg1;
        final MagnificationSpec spec = (MagnificationSpec) args.arg2;

        args.recycle();

        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) {
                root = mViewRootImpl.mView;
            } else {
                root = findViewByAccessibilityId(accessibilityViewId);
            }
            if (root != null && isShown(root)) {
                mPrefetcher.prefetchAccessibilityNodeInfos(root, virtualDescendantId, flags, infos);
            }
        } finally {
            try {
                mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = 0;
                applyAppScaleAndMagnificationSpecIfNeeded(infos, spec);
                if (spec != null) {
                    spec.recycle();
                }
                callback.setFindAccessibilityNodeInfosResult(infos, interactionId);
                infos.clear();
            } catch (RemoteException re) {
                /* ignore - the other side will time out */
            }
        }
    }

这里对flags进行了几个操作:

  1. 取出 message 里面存储的各个变量。其中有我们最关注的flags
  2. flags赋值给mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags
  3. 在查出 root 且root != null && isShown(root)(这里暂时先不探究root是什么),执行mPrefetcher.prefetchAccessibilityNodeInfos方法。isShown(root)是判断当前节点是否会显示在界面上,相关源码:
    android.view.AccessibilityInteractionController

    private boolean isShown(View view) {
        // The first two checks are made also made by isShown() which
        // however traverses the tree up to the parent to catch that.
        // Therefore, we do some fail fast check to minimize the up
        // tree traversal.
        return (view.mAttachInfo != null
                && view.mAttachInfo.mWindowVisibility == View.VISIBLE
                && view.isShown());
    }
    

    里面的view.isShown()源码:
    android.view.View

    /**
     * Returns the visibility of this view and all of its ancestors
     *
     * @return True if this view and all of its ancestors are {@link #VISIBLE}
     */
    public boolean isShown() {
        View current = this;
        //noinspection ConstantConditions
        do {
            if ((current.mViewFlags & VISIBILITY_MASK) != VISIBLE) {
                return false;
            }
            ViewParent parent = current.mParent;
            if (parent == null) {
                return false; // We are not attached to the view root
            }
            if (!(parent instanceof View)) {
                return true;
            }
            current = (View) parent;
        } while (current != null);
    
        return false;
    }
    

回到正题,我们来看看mPrefetcher.prefetchAccessibilityNodeInfos方法:

/**
     * This class encapsulates a prefetching strategy for the accessibility APIs for
     * querying window content. It is responsible to prefetch a batch of
     * AccessibilityNodeInfos in addition to the one for a requested node.
     */
    private class AccessibilityNodePrefetcher {

        private static final int MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE = 50;

        private final ArrayList<View> mTempViewList = new ArrayList<View>();

        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);
                    }
                }
            } else {
                AccessibilityNodeInfo root = provider.createAccessibilityNodeInfo(virtualViewId);
                if (root != null) {
                    outInfos.add(root);
                    if ((fetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_PREDECESSORS) != 0) {
                        prefetchPredecessorsOfVirtualNode(root, view, provider, outInfos);
                    }
                    if ((fetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_SIBLINGS) != 0) {
                        prefetchSiblingsOfVirtualNode(root, view, provider, outInfos);
                    }
                    if ((fetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS) != 0) {
                        prefetchDescendantsOfVirtualNode(root, provider, outInfos);
                    }
                }
            }
        }

其中fetchFlags就是之前的flags,根据前面return findAccessibilityNodeInfoByAccessibilityId(connectionId,AccessibilityNodeInfo.ACTIVE_WINDOW_ID, AccessibilityNodeInfo.ROOT_NODE_ID,false, AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS);,此处进入的是(fetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS) != 0为 true 后进入的方法。
至于是进入prefetchDescendantsOfRealNode还是prefetchDescendantsOfVirtualNode,目前还不能确定(不是调试环境,确定不了 root 的值)。咱们逐个看:

prefetchDescendantsOfRealNode:

private void prefetchDescendantsOfRealNode(View root,
                List<AccessibilityNodeInfo> outInfos) {
            if (!(root instanceof ViewGroup)) {
                return;
            }
            HashMap<View, AccessibilityNodeInfo> addedChildren =
                new HashMap<View, AccessibilityNodeInfo>();
            ArrayList<View> children = mTempViewList;
            children.clear();
            try {
                root.addChildrenForAccessibility(children);
                final int childCount = children.size();
                for (int i = 0; i < childCount; i++) {
                    if (outInfos.size() >= MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) {
                        return;
                    }
                    View child = children.get(i);
                    if (isShown(child)) {
                        AccessibilityNodeProvider provider = child.getAccessibilityNodeProvider();
                        if (provider == null) {
                            AccessibilityNodeInfo info = child.createAccessibilityNodeInfo();
                            if (info != null) {
                                outInfos.add(info);
                                addedChildren.put(child, null);
                            }
                        } else {
                            AccessibilityNodeInfo info = provider.createAccessibilityNodeInfo(
                                   AccessibilityNodeInfo.UNDEFINED);
                            if (info != null) {
                                outInfos.add(info);
                                addedChildren.put(child, info);
                            }
                        }
                    }
                }
            } finally {
                children.clear();
            }
            if (outInfos.size() < MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) {
                for (Map.Entry<View, AccessibilityNodeInfo> entry : addedChildren.entrySet()) {
                    View addedChild = entry.getKey();
                    AccessibilityNodeInfo virtualRoot = entry.getValue();
                    if (virtualRoot == null) {
                        prefetchDescendantsOfRealNode(addedChild, outInfos);
                    } else {
                        AccessibilityNodeProvider provider =
                            addedChild.getAccessibilityNodeProvider();
                        prefetchDescendantsOfVirtualNode(virtualRoot, provider, outInfos);
                    }
                }
            }
        }
  1. 判断root是不是ViewGroup的实例(这里应该是,否则返回值就是空了)
  2. 建立两个变量addedChildren, children(此时 children 使用过 clear() 方法,所以已经是空列表了)
  3. 通过root.addChildrenForAccessibility(children)获取root的子节点并添加到children列表中。
  4. 后面的语句主要就是遍历children的元素,然后把所有会显示的元素都加到outInfosoutInfos就是最终会返回到 callback 中的节点列表。
  5. 如果找到的节点数outInfos小于MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE,那就在前面找到的 child 节点中继续找(递归),直到root不是ViewGroup实例或者outInfos大小达标。

所以关键语句就是root.addChildrenForAccessibility(children)。咱们进去看看:
android.view.View

/**
 * Adds the children of a given View for accessibility. Since some Views are
 * not important for accessibility the children for accessibility are not
 * necessarily direct children of the view, rather they are the first level of
 * descendants important for accessibility.
 *
 * @param children The list of children for accessibility.
 */
public void addChildrenForAccessibility(ArrayList<View> children) {
    if (includeForAccessibility()) {
        children.add(this);
    }
}

然后进去includeForAccessibility():

/**
 * Whether to regard this view for accessibility. A view is regarded for
 * accessibility if it is important for accessibility or the querying
 * accessibility service has explicitly requested that view not
 * important for accessibility are regarded.
 *
 * @return Whether to regard the view for accessibility.
 *
 * @hide
 */
public boolean includeForAccessibility() {
    //noinspection SimplifiableIfStatement
    if (mAttachInfo != null) {
        return (mAttachInfo.mAccessibilityFetchFlags
                & AccessibilityNodeInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS) != 0
                || isImportantForAccessibility();
    }
    return false;
}

比较接近了,这个地方就是判断这个 view 对 accessibility 而言是否重要的地方。如果不重要且AccessibilityNodeInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS标志位为 0(我们的 compress 就是做了这件事),那就返回 false。
既然第一个条件mAttachInfo.mAccessibilityFetchFlags & AccessibilityNodeInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS) != 0的结果我们已经知道是 false 了,那就去看isImportantForAccessibility()

/**
 * Gets whether this view should be exposed for accessibility.
 *
 * @return Whether the view is exposed for accessibility.
 *
 * @hide
 */
public boolean isImportantForAccessibility() {
    final int mode = (mPrivateFlags2 & PFLAG2_IMPORTANT_FOR_ACCESSIBILITY_MASK)
            >> PFLAG2_IMPORTANT_FOR_ACCESSIBILITY_SHIFT;
    if (mode == IMPORTANT_FOR_ACCESSIBILITY_NO
            || mode == IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) {
        return false;
    }

    // Check parent mode to ensure we're not hidden.
    ViewParent parent = mParent;
    while (parent instanceof View) {
        if (((View) parent).getImportantForAccessibility()
                == IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) {
            return false;
        }
        parent = parent.getParent();
    }

    return mode == IMPORTANT_FOR_ACCESSIBILITY_YES || isActionableForAccessibility()
            || hasListenersForAccessibility() || getAccessibilityNodeProvider() != null
            || getAccessibilityLiveRegion() != ACCESSIBILITY_LIVE_REGION_NONE;
}

这就是prefetchDescendantsOfRealNode最终的判断位置了。主要有 3 部分:

  1. 判断 mode 的值 final int mode = (mPrivateFlags2 & PFLAG2_IMPORTANT_FOR_ACCESSIBILITY_MASK) >> PFLAG2_IMPORTANT_FOR_ACCESSIBILITY_SHIFT; if (mode == IMPORTANT_FOR_ACCESSIBILITY_NO || mode == IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) { return false; } 其中IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS的解释: /** * The view is not important for accessibility, nor are any of its * descendant views. */ public static final int IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS = 0x00000004;
  2. 判断父节点及祖先节点是否为 IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS(对 ACCESSIBILITY 不重要且对它的后代而言也不重要) // Check parent mode to ensure we're not hidden. ViewParent parent = mParent; while (parent instanceof View) { if (((View) parent).getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) { return false; } parent = parent.getParent(); }
  3. 判断 mode 是否为IMPORTANT_FOR_ACCESSIBILITY_YES及是否具有其它和 Accessibility 相关的特性: return mode == IMPORTANT_FOR_ACCESSIBILITY_YES || isActionableForAccessibility() || hasListenersForAccessibility() || getAccessibilityNodeProvider() != null || getAccessibilityLiveRegion() != ACCESSIBILITY_LIVE_REGION_NONE; 先记着。咱们去看另一个路线prefetchDescendantsOfVirtualNode

prefetchDescendantsOfVirtualNode:

private void prefetchDescendantsOfVirtualNode(AccessibilityNodeInfo root,
        AccessibilityNodeProvider provider, List<AccessibilityNodeInfo> outInfos) {
    SparseLongArray childNodeIds = root.getChildNodeIds();
    final int initialOutInfosSize = outInfos.size();
    final int childCount = childNodeIds.size();
    for (int i = 0; i < childCount; i++) {
        if (outInfos.size() >= MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) {
            return;
        }
        final long childNodeId = childNodeIds.get(i);
        AccessibilityNodeInfo child = provider.createAccessibilityNodeInfo(
                AccessibilityNodeInfo.getVirtualDescendantId(childNodeId));
        if (child != null) {
            outInfos.add(child);
        }
    }
    if (outInfos.size() < MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) {
        final int addedChildCount = outInfos.size() - initialOutInfosSize;
        for (int i = 0; i < addedChildCount; i++) {
            AccessibilityNodeInfo child = outInfos.get(initialOutInfosSize + i);
            prefetchDescendantsOfVirtualNode(child, provider, outInfos);
        }
    }
}

流程差不多,先找 children 节点,然后遍历,最后如果 size 不够,继续在 child 节点中找,直到节点数达标。咱们来看`:
android.view.accessibility.AccessibilityNodeInfo`

/**
 * @return The ids of the children.
 *
 * @hide
 */
public SparseLongArray getChildNodeIds() {
    return mChildNodeIds;
}

很直接,直接返回一个列表。
注意这里并没有校验这个 node 对 Accessibility 重要。估计是因为 VirtualNode 通常出现在自定义的 view,这些 view 的元素不一定都有 flag 或者可以判断是否重要的属性。

这里说明一下,每个 view 里面的 node 既可以作为在整个页面中都可以找到的真正的 node(ReadNode),也可以使用仅在 view 内有效、仅能通过 view 的 provider 来查找的的 node(VirtualNode,详情可看AccessibilityNodeProvider)。因此会存在两个遍历 node 的方法。

至于为什么前面容器类控件(FrameLayout 等)会被 compress 干掉,目前还没找到确切的原因,不过已经可以肯定是在下面三个地方其中一个确定的:

  1. 判断 mode 的值 final int mode = (mPrivateFlags2 & PFLAG2_IMPORTANT_FOR_ACCESSIBILITY_MASK) >> PFLAG2_IMPORTANT_FOR_ACCESSIBILITY_SHIFT; if (mode == IMPORTANT_FOR_ACCESSIBILITY_NO || mode == IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) { return false; } 其中IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS的解释: /** * The view is not important for accessibility, nor are any of its * descendant views. */ public static final int IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS = 0x00000004;
  2. 判断父节点及祖先节点是否为 IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS(对 ACCESSIBILITY 不重要且对它的后代而言也不重要) // Check parent mode to ensure we're not hidden. ViewParent parent = mParent; while (parent instanceof View) { if (((View) parent).getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) { return false; } parent = parent.getParent(); }
  3. 判断 mode 是否为IMPORTANT_FOR_ACCESSIBILITY_YES及是否具有其它和 Accessibility 相关的特性: return mode == IMPORTANT_FOR_ACCESSIBILITY_YES || isActionableForAccessibility() || hasListenersForAccessibility() || getAccessibilityNodeProvider() != null || getAccessibilityLiveRegion() != ACCESSIBILITY_LIVE_REGION_NONE;

但在android.view.View里面找到一个有点关系的属性:

/**
 * Shift for the bits in {@link #mPrivateFlags2} related to the
 * "importantForAccessibility" attribute.
 */
static final int PFLAG2_IMPORTANT_FOR_ACCESSIBILITY_SHIFT = 20;

由于这部分地方涉及到不少位运算,暂时留待后面详细研究。

易寒 #16 · 2015年02月11日 Author

#15 楼 @chenhengjie123 非常好!基本问题差不多确定了,主要的关键点在 AccessibilityNodePrefetcher 这个类的三个方法,这三个方法定义获取不同的控件信息。那么根据 flag 不同重定向不同的方法中,那你现在只需要研究出控件的性质分类,就是哪些定义为重要,哪些被定义为不重要。这些研究出来后,这个问题就全部通了。这些研究资料你最好全部放到博客中,这是一笔好的财富。相信不会有这么详细的研究过程,别浪费了。或者发个总结帖,描述的时候不要带这么多代码,要用通俗易懂的语言描述每个方法的区别,以及控件的分类。

@doctorq 谢谢支持!后面我会放到我自己的 blog 里面的,也会发个总结帖说明一下,还会发些分享帖说明如何进行 remote debug 来查看 app 使用 android api 时内部具体是怎么做的。现在在这里记录的只是原始资料,所以也只是跟帖而已。

解释的非常详细,太感谢 LZ 了。

您好,我按照您的帖子,导入了 android.jar 文件,红色感叹号还是没有消失,请问还要设置哪些地方?! 希望得到您的回复,同时我也给你QQ留言了,麻烦了

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册