Android之UI Automator框架源码分析(第六篇:UiDevice查找控件之一)

2022-08-08,,,,

(注意:本文基于UI Automator测试框架版本为2.2.0)

UI Automator测试框架中,UiDevice对象表示当前设备(手机、平板、电视、手表等等),在UI功能自动化测试中,查找控件是测试三元素中的第一个Action,它的重要性不言而喻,UiDevice定义了查找控件的方法,3个重载的findObject方法+1个hasObject方法,共计4个方法,我们需要经常使用这些API,那么这些方法是如何定位到Window中的控件的呢?这是学习本篇文章的目的!限于篇幅,本篇分析其中一个方法findObject(BySelector),以下截图红圈处是唯一接受UiSelector对象的方法!

findObject(BySelector)

0、我们最常用的一个方法,它接受一个BySelector对象(表示条件选择器),通常情况我们使用只需要使用By类的一系列静态方法就可以获得BySelector对象,findObject方法会使用传递进来的BySelector对象(也称选择器对象)进行控件匹配,如果匹配控件成功就会返回一个UiObject2对象,匹配控件失败则会返回一个null。接下来我们就详细分析一下findObject(BySelector)方法的实现。(备注:UiObject2对象表示当前Window(单个或多个Window,因Android支持多窗口)View树中的一个控件,UiObject2是UI Automator测试框架使用率极高的类之一,后续文章会单独总结)

    public UiObject2 findObject(BySelector selector) {
        AccessibilityNodeInfo node = ByMatcher.findMatch(this, selector, getWindowRoots());
        return node != null ? new UiObject2(this, selector, node) : null;
    }

UiDevice实现了Searchable接口,Searchable接口规范了具备搜索能力的方法,findObject(BySelector)方法就是声明在Searchable接口中的抽象方法之一,UiDevice进行对此进行了具体的实现

a、在该方法内部,首先调用getWindowRoots()方法(见本文1号知识点)得到一个 AccessibilityNodeInfo[]数组对象,接下来调用ByMatcher的静态方法findMatch(见本文2号知识点),findMatch方法需要传入三个参数,一个UiDevce对象、一个BySelector对象,最后一个则是getWindowRoots()方法返回的AccessibilityNodeInfo[]数组对象,定义的局部变量node则负责存储ByMatcher的静态方法findMatch返回的AccessibilityNodeInfo对象,这个AccessibilityNodeInfo对象表示的正是当前Window中的View树中的一个控件

b、接下来对局部变量node进行判断,这里会出现两种情况,第一种情况是匹配控件成功,另一种是匹配控件失败。当匹配控件成功,局部变量node保存着AccessibilityNodeInfo对象的引用,此时会执行创建一个UiObject2对象,该UiObject2对象创建时需要持有当前的UiDevice对象this、传入的参数BySelector对象selector,以及局部变量node持有的AccessibilityNodeInfo对象,最后整个findObject方法会返回创建的这个UiObject2对象;当匹配控件失败时,此时局部变量node为null,说明在当前Window中并没有找到与BySelector对象相匹配的控件,这时候整个findObject方法就会返回一个null值,null表示未找到匹配的控件

在该方法内部调用的getWindowRoots方法、以及ByMatcher.findMatch方法共同合作完成了在Window中的View树中查找控件的工作,我们继续学习一下它们的具体实现

 

1、定义在UiDevice类中的getWindowRoots方法,返回值是AccessibilityNodeInfo[]数组对象

    AccessibilityNodeInfo[] getWindowRoots() {
        waitForIdle();

        Set<AccessibilityNodeInfo> roots = new HashSet();

        // Start with the active window, which seems to sometimes be missing from the list returned
        // by the UiAutomation.
        AccessibilityNodeInfo activeRoot = getUiAutomation().getRootInActiveWindow();
        if (activeRoot != null) {
            roots.add(activeRoot);
        }

        // Support multi-window searches for API level 21 and up.
        if (UiDevice.API_LEVEL_ACTUAL >= Build.VERSION_CODES.LOLLIPOP) {
            for (AccessibilityWindowInfo window : getUiAutomation().getWindows()) {
                AccessibilityNodeInfo root = window.getRoot();
                if (root == null) {
                    Log.w(LOG_TAG, String.format("Skipping null root node for window: %s",
                            window.toString()));
                    continue;
                }
                roots.add(root);
            }
        }
        return roots.toArray(new AccessibilityNodeInfo[roots.size()]);
    }

a、首先是waitForIdle()方法的调用,作用是等待主线程空闲后,该方法才会再继续,这样就确保不影响App的运行,后面单独开篇文章分析waitForIdle方法

b、接下来创建的一个作为容器的HashSet对象,该集合对象指定持有的元素类型为AccessibilityNodeInfo本身及其子类,并由局部变量roots负责持有该对象

c、代码继续进行,先是通过一个getUiAutomation()方法获得UiAutomation对象,再调用它的getRootInActiveWindow方法,经过层层调用(需要单独总结),会得到一个AccessibilityNodeInfo对象,这个对象表示的是在Window中的View树的根节点对象,可以理解为代表根结点的DecorView对象,由局部变量activeRoot先负责保存该对象,后续会再将已获得的activeRoot对象由HashSet对象roots负责保存

d、Android支持多窗口(一个屏幕多个Window),所以在前面定义的HashSet对象roots的作用就是负责持有每个Window的View树的根节点对象,在API21及其以上时,此处多窗口支持的代码会执行,先通过getUiAutomation()方法获得UiAutomation对象,接着调用UiAutomation对象的getWindows()方法,接下来是对getWindows()返回的List对象进行foreach循环遍历,该List对象持有的元素类型为AccessibilityWindowInfo,通过AccessibilityWindowInfo的getRoot()方法可以获得表示每个Window中View树的根节点对象(顶级View),表示顶级View的AccessibilityNodeInfo对象,并且会将每个Window的顶级View放到一个Set容器对象中,google的工程师对没有获取到Window顶级View的情况作了容错,不仅会通过Log类输出一段日志,还会暂停一轮,因为无需将null的元素添加到Set中!

e、在该方法的最后,该方法会将容器Set对象roots转换为AccessibilityNodeInfo数组对象并返回给调用者

该方法返回的AccessibilityNodeInfo数组对象,持有的每个元素表示的是每一个Window中View树的根节点对象,比如该数组对象一共持有2个元素,那么就说明此时当前的界面中存在两个Window(Android支持多窗口)!那么上文中提及的UiAutomation对象的getRootInActiveWindow是如何获得表示Window的根结点对象、getWindows方法又是如何定位到控件的呢?这里继续留个疑问,这需要在后续文章中单独总结!!我们继续先看下ByMatcher的findMatch方法!

 

2、定义在ByMatcher类中的findMatch静态方法,接受一个UiDevice对象,一个BySelector对象,一个可变参数的AccessibilityNodeInfo数组对象

    static AccessibilityNodeInfo findMatch(UiDevice device, BySelector selector,
            AccessibilityNodeInfo... roots) {

        // TODO: Don't short-circuit when debugging, and warn if more than one match.
        ByMatcher matcher = new ByMatcher(device, selector, true);
        for (AccessibilityNodeInfo root : roots) {
            List<AccessibilityNodeInfo> matches = matcher.findMatches(root);
            if (!matches.isEmpty()) {
                return matches.get(0);
            }
        }
        return null;
    }

该方法从持有多个Window的View树的根结点的AccessibilityNodeInfo数组对象(它持有每个Window的顶级View)中,找到一个匹配的控件对象……

a、首先创建一个ByMatcher对象,ByMatcher构造方法接受了传入的UiDevice对象、BySelector对象、一个boolean值,这个ByMatcher对象可以完成过滤匹配控件的功能

b、对传入的AccessibilityNodeInfo数组对象进行遍历,这是逐个对每一个Window进行一个处理,一旦在某个Window中匹配到满足条件的控件,对Window的遍历就会立刻停止;具体的匹配过程,则是将每一个AccessibilityNodeInfo对象传入到ByMatcher对象matcher的findMatchers方法中(见下方3号知识点),目前每个AccessibilityNodeInfo对象代表的是View树的根节点(一个Window对应一个View树),根据控件的条件对象,每个View树中可能会有多个控件满足匹配条件,所以在ByMatcher对象的findMatchers方法(见本文3号知识点)返回的是一个List对象,容器List对象持有的每个元素是AccessibilityNodeInfo对象,在当前Window的View树如果找到一个或多个匹配的控件(此时matchers不会是空的),整个方法就会返回表示该Window中所有匹配控件List对象的第一个元素……,此时其他窗口的Window就不会再参与匹配过程(注意这样的冲突bug),当该方法从始至终没有找到匹配条件的控件,就会返回一个null,那么我们接下来继续看看ByMatcher的findMatchers方法是如何根据View树的根结点找到匹配条件的控件?(注意:ByMatcher的findMatchers方法跟ByMatcher的findMatcher静态方法只相差一个字母‘s’)

 

3、位于ByMatcher类中的findMatchers方法,接受一个AccessibilityNodeInfo对象(该对象表示一个Window中的View树的根结点对象)

    private List<AccessibilityNodeInfo> findMatches(AccessibilityNodeInfo root) {
        List<AccessibilityNodeInfo> ret =
                findMatches(root, 0, 0, new SinglyLinkedList<PartialMatch>());

        // If no matches were found
        if (ret.isEmpty()) {
            // Run watchers and retry
            mDevice.runWatchers();
            ret = findMatches(root, 0, 0, new SinglyLinkedList<PartialMatch>());
        }

        return ret;
    }

a、开始先调用了一个重载的findMachers方法(见本文4号知识点),该方法返回的List对象由局部变量ret负责保存,这个List对象中持有的每个AccessibilityNodeInfo对象,表示的都是满足匹配条件的控件

b、当ret没有持有任何元素时,说明此时没有在View树中找到匹配的元素,此时会先执行容错机制,ByMatcher对象持有了UiDevice对象mDevice,此时就会调用UiDevice对象的runWatchers()方法,runWatchers()方法会执行在UiDevice中注册的所有UiWatch对象的checkCondition方法(我们在该方法执行一些具体的容错,比如弹窗处理)

c、运行完UiWatcher的方法后,再尝试一次查找控件的工作,同样再调用一次重载的findMatchers方法(见本文4号知识点)进行的,最后整个方法会返回表示匹配的多个控件的list对象,那么在这个方法内部调用的这个重载的findMatchers方法接受的四个参数,都表示什么?只能继续研究它的源码了!

 

4、位于ByMatcher类的findMatchers方法,它是一个重载方法,接受四个参数,第一个参数node是AccessibilityNodeInfo对象(表示要查找的View树的根节点,可以是子树的根节点),第二个参数index的类型是整型(表示此控件节点在其父节点下面的索引位置),第三个参数depth也是整型(表示当前第一个参数的node结点与根节点之间的距离),第四个参数partialMatches的类型是SinglyLInkedList对象(表示需要更新的持有PartialMatch的列表)

    private List<AccessibilityNodeInfo> findMatches(AccessibilityNodeInfo node,
            int index, int depth, SinglyLinkedList<PartialMatch> partialMatches) {

        List<AccessibilityNodeInfo> ret = new ArrayList<AccessibilityNodeInfo>();

        // Don't bother searching the subtree if it is not visible
        if (!node.isVisibleToUser()) {
            return ret;
        }

        // Update partial matches
        for (PartialMatch partialMatch : partialMatches) {
            partialMatches = partialMatch.update(node, index, depth, partialMatches);
        }

        // Create a new match, if necessary
        PartialMatch currentMatch = PartialMatch.accept(node, mSelector, index, depth);
        if (currentMatch != null) {
            partialMatches = SinglyLinkedList.prepend(currentMatch, partialMatches);
        }

        // For each child
        int numChildren = node.getChildCount();
        boolean hasNullChild = false;
        for (int i = 0; i < numChildren; i++) {
            AccessibilityNodeInfo child = node.getChild(i);
            if (child == null) {
                if (!hasNullChild) {
                    Log.w(TAG, String.format("Node returned null child: %s", node.toString()));
                }
                hasNullChild = true;
                Log.w(TAG, String.format("Skipping null child (%s of %s)", i, numChildren));
                continue;
            }

            // Add any matches found under the child subtree
            ret.addAll(findMatches(child, i, depth + 1, partialMatches));

            // We're done with the child
            child.recycle();

            // Return early if we sound a match and shortCircuit is true
            if (!ret.isEmpty() && mShortCircuit) {
                return ret;
            }
        }

        // Finalize match, if necessary
        if (currentMatch != null && currentMatch.finalizeMatch()) {
            ret.add(AccessibilityNodeInfo.obtain(node));
        }

        return ret;
    }

首先创建一个ArrayList对象,由局部变量ret保存,它负责持有多个AccessibilityNodeInfo对象,那么该List的作用是什么?我们继续看源码

a、判断传入的以node为根结点的View树是否对用户可见

若不可见,则不再进行搜索,整个方法直接返回局部变量ret,作者给的注释很有意思:Don't bother searching the subtree if it is not visible(如果子树不可见,就不必费心搜索)

b、接着更新部分匹配功能

遍历传入的SinglyLinkedList对象,每次执行它持有的每个元素对象PartialMatch的update方法,update方法会返回一个SinglyLinkedList对象,每次都会将返回的对象赋值给传入的局部变量partialMatches(第四个参数)进行保存

c、如果需要,创建一个新的匹配

通过调用PartialMatch的静态方法accept,获得一个PartialMatch对象并由局部变量currentMatch负责存储,静态方法accept需要四个参数,一个是当前传入的node对象,第二个是ByMatcher对象持有的BySelector对象,第三个是则是传入的局部变量index,第四个是depth,每当accept方法成功返回一个对象,就会调用SinglyLinkedList的静态方法prepend,prepend方法返回的又是一个SinglyLinkedList对象,这次返回的SinglyLinkedList对象又会交给局部变量partialMatches保存(这个局部变量真的好辛苦)

d、对传入的View树的根结点的每一个子View进行操作

通过AccessibilityNodeInfo的getChildCount方法获得当前根结点直接持有的子元素数量(直接子元素),创建一个临时标志位hasNullChild,用于表示是否有空的子元素,开始遍历持有的每一个直接子元素(牛逼)

e、坑爹的View树遍历啊(学好树是多么重要)……总之最后一个List,每个元素表示满足匹配条件的控件……

 

 

总结

1、Uiautomation对象起到很关键的作用,单独分析它的实现是如何找到表示Window中表示控件根结点的对象的?

2、index:表示此节点在其父节点下面的索引,怎么理解?

3、depth:表示此节点和根节点之间的距离,怎么理解?

4、findObject(BySelector),本文只分析了该方法,另外三个查找控件的方法仍需单独分析

本文地址:https://blog.csdn.net/cadi2011/article/details/106737758

《Android之UI Automator框架源码分析(第六篇:UiDevice查找控件之一).doc》

下载本文的Word格式文档,以方便收藏与打印。