Android之UiAutomator测试框架源码分析(第五篇:UiDevice查找控件功能深度分析)

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

前言

    在UI Automator测试框架中,每个UiDevice对象代表当前Android设备(手机、平板、电视、手表等等一切以Android作为操作系统的设备),通过设备查找控件,作者的思想非常清晰,UiDevice封装了查找控件的方法,这也是我们学习的重点,一个控件是如何被找到的呢?

    UiDevice提供查找控件的方法共计4个:

    2个重载的findObject()方法

    1个hasObject()方法

    1个findObjects()方法

     4个查找控件的方法中,1个findObject()方法需要传入UiSelector对象,另外3个方法则传入的是BySelector对象。4个方法中,2个方法用于查找单个控件、1个方法用于检查单个控件是否存在、还有1个方法可以用于查找多个控件(一组符合要求的控件)

    简单讲讲UiSelector与BySelector的历史?UiSelector是UI Automator早期版本中设计的条件对象,BySelector对象则是新版本表示条件的需,两个表示条件的对象都可以使用,区别在于使用UiSelector对象获取到的是UiObject对象、而使用BySelector对象得到的是UiObject2对象,UiObject与UiObject2都表示控件,用哪个都可以!

     要想操作控件,必须得找到控件,查找控件是Ui自动化程序的第一个动作,UiDevice查找控件的API一定会经常使用的…

     这些查找控件的方法是如何定位到Android设备屏幕中的某一个具体的控件的呢? 通过本篇源码的学习,我们将得到答案!!

    本文将着重分析UiDevice的4个查找控件方法中一个,findObject(BySelector)

    注意:下方截图中的红圈,代表需传入UiSelector对象的findObject()方法,不是本篇分析的findObject(BySelector)方法,话不多说,我们继续对findObject(BySelector)方法进行分析……

findObject(BySelector)方法分析

    位于UiDevice中的findObject()方法是查找控件的方法,它是个重载方法,期中一个方法需传入BySelector对象作为查找控件的条件,每个BySelector对象表示查找控件时条件选择器对象(简称条件对象)

    findObject()方法中会使用传入的BySelector对象持有的1个或者多个关于控件的属性,通过这些控件的属性再去在屏幕中查找到一个具体的控件!

    去哪获取BySelector对象呢? 通过BySelector的构造方法即可创建,但是官方推荐使用By类的静态工厂方法创建,每个静态工厂方法会返回一个表示持有不同属性的BySelector对象,By类的所有静态工厂方法返回的全部是BySelector对象

    当findObject()方法通过传入的BySelector对象找到某个具体的控件时,该方法会返回一个UiObject2对象,这个UiObject2对象就代表找到的具体控件,如果在window中没有找到符合条件的控件,findObject()方法会返回一个null,表示没有根据BySelector对象找到具体的控件!

    继续详细分析一下findObject()方法的实现……

 (备注1:每个UiObject2对象表示任意一个Window持有的View树(View层次结构)中的一个控件,该控件可以是一个View,也可以是一个ViewGroup)

 (备注2:Android支持多窗口,所以一个屏幕可以同时存在多个Window,在多窗口下查找某个控件,可能会出现多个控件被同样的条件对象所匹配)

    findObject(BySelector)源码……

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

    findObject()方法本身是Searchable接口(标准)中定义的抽象方法,Searchable接口定义和规范了具备搜索控件能力的方法,哪个类实现Searchable接口,则代表具备查找控件的能力,Searchable接口是个标准!

    UiDevice实现了Searchable接口,代表UiDevice具备查找控件的功能,且也具体实现了findObject()方法

    开始按照源码的书写顺序对findObject()方法分析……

1、获取屏幕中每个Window的根节点对象

首先调用getWindowRoots()方法,getWindowRoots()方法将会返回一个AccessibilityNodeInfo[]数组对象,AccessibilityNodeInfo[]数组对象持有的每个元素表示的是当前屏幕中每个Window持有的View树的根节点对象。举个例子:你的手机目前打开两个窗口,一个窗口是微信、一个窗口是微博,在此场景下,这个AccessibilityNodeInfo数组对象会持有2个元素,每个元素都是AccessibilityNodeInfo对象,每个AccessibilityNodeInfo对象一个窗口中表示一个控件,而在此处则表示的是每个窗口View树的根节点控件

备注:Android支持多窗口,每个窗口都有一个View树,而每个View树,一定会有一个根节点,这几乎是所有界面的数据结构

备注:View树的根结点对象,也被称为顶级View,它往往是DecorView对象

2、从每个Window的根节点对象出发,根据BySelector对象持有的属性查找对应的节点对象(查找控件)

接下来调用ByMatcher的静态方法findMatch(),findMatch方法需传入3个参数,第一个参数是UiDevce对象、第二个参数是BySelector对象,最后一个参数则是getWindowRoots()方法返回的AccessibilityNodeInfo[]数组对象。定义局部变量node负责存储ByMatcher的静态方法findMatch()返回的AccessibilityNodeInfo对象,此处返回的AccessibilityNodeInfo对象表示在当前屏幕中某一个Window持有的View树中的根据BySelector对象作为条件的,第一个符合匹配条件BySelector要求的控件!!!!(注意:这里查找控件时,如果你指定的属性不是控件的唯一属性,可能会出现查找控件的bug,即本来你是想查找A控件,但找到的是B控件,因为A和B控件的属性一样,且B控件在View树的上方……想想就坑……谁距离根节点越近,谁就会被找到)

备注:此处说明是某一个Window,而不是一个Window,这是因为Android支持在一个屏幕中开启多个Window

3、处理匹配控件后的结果,根据条件找到对应的控件时,则会封装为一个UiObject2对象并返回,没有找到对应的控件则只会返回一个null

定义的局部变量node进行null的判断,并针对node的两种情况分别进行处理,第一种情况是匹配控件成功,此时node指向一个AccessibilityNodeInfo对象;另一种情况则是匹配控件失败,此时的node指向null。当匹配控件成功,就会继续创建一个UiObject2对象,创建的UiObject2对象持有了当前UiDevice对象this、以及传递进来的BySelector对象selector,还有局部变量node持有的AccessibilityNodeInfo对象,findObject()方法最后会返回这个新创建的UiObject2对象;当匹配控件失败,说明在当前屏幕的所有Window持有的View树中均没有找到BySelector对象作为条件的控件,此时的findObject()方法会向调用者返回一个null值!!(备注:UiObject2对象持有了表示控件的AccessibilityNodeInfo对象,说明UiObject2进一步对AccessibilityNodeInfo对象进行了更多功能的封装)

说明:在findObject()方法内部调用的getWindowRoots()方法、以及ByMatcher的静态方法findMatch()共同完成在屏幕中的每个Window持有的View树中查找控件的工作,我们继续学习一下这些方法,才能知道控件具体的定位又是如何做到的?

getWindowRoots()方法分析

    定义在UiDevice类中的getWindowRoots()方法,无参数,它的返回值是AccessibilityNodeInfo[]数组对象,这个数组对象持有的每一个元素,都表示某一个Window持有View树的根结点对象!

    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()]);
    }

    getWindowRoots()方法在方法体中做了以下工作,我们一步一步的学习一下……

1、等待被测应用的主线程处于空闲状态

首先是waitForIdle()方法的调用,waitForIdle()方法的作用是等待被测应用的主线程空闲后,该方法才会再继续执行,而插装测试的线程此时会处于等待的状态,这样可以确保插装测试的线程不会影响到被测App的实际运行,我想需要单独的文章分析该waitForIdle()方法(等待主线程空闲的专题文章)

2、创建集合对象,用于存储每个Window中的根节点对象

接下来创建一个作为容器的HashSet对象,该HashSet对象指定持有的元素为AccessibilityNodeInfo对象本身及其子类对象,由局部变量roots负责持有该HashSet对象的引用,后面我们会知道这个HashSet容器对象用来干什么!

3、获取屏幕中第一个活动窗口的根节点

代码继续,先通过一个getUiAutomation()方法获得一个UiAutomation对象,然后再调用UiAutomation对象的getRootInActiveWindow()方法,通过getRootInActiveWindow()方法内部的层层调用(UiAutomation的方法都单独总结)并返回一个AccessibilityNodeInfo对象,这个AccessibilityNodeInfo对象表示的是一个处于活动状态的Window持有的View树的根节点对象(实际为DecorView对象),该对象由临时定义的局部变量activeRoot负责保存,后续再将已获得的activeRoot对象添加到之前创建的HashSet对象roots中,现在你知道这个HashSet容器对象roots是干嘛的了吧?这个HashSet容器对象专门负责保存表示每个Winidow持有的View树的顶级View对象

4、屏幕中有多个窗口时,获取其它非活动窗口的根节点,并添加到集合中

Android支持多窗口(当前屏幕多个Window),之前创建的HashSet对象roots专门负责持有屏幕中出现的每个Window持有的View树的根节点对象(每个都使用AccessibilityNodeInfo对象来表示),在API 21及其以上时,多窗口支持的代码才会执行,此代码块的内部会首先通过getUiAutomation()方法获得一个UiAutomation对象,接着调用该UiAutomation对象的getWindows()方法,接下来对getWindows()方法返回的List对象进行foreach循环遍历,该List对象持有的元素类型为AccessibilityWindowInfo,在遍历的代码块中会通过每个AccessibilityWindowInfo对象的getRoot()方法获得一个表示每个Window持有的View树的根节点对象,即表示每个Window持有的View树的根节点对象(此处为AccessibilityNodeInfo对象),再将表示每个Window持有的View树的顶级View(AccessibilityNodeInfo对象)保存到之前创建的HashSet容器对象roots中。Google的工程师对没有获取到Window持有的View树的顶级View的特殊情况作了容错处理,处理方式则是使用Log类输出一段日志,再使用continue关键字暂停后续代码的执行,因为不需要将未获取到View树根节点的情况存储到HashSet中(一个null元素没必要添加到HashSet中)!

5、将集合HashSet对象转换为AccessibilityNodeInfo[]数组对象,并返回

在getWindowRoots()方法的最后,该方法会将容器HashSet对象roots转换为一个AccessibilityNodeInfo[]数组对象并返回给调用者。getWindowRoots()方法返回的是AccessibilityNodeInfo[]数组对象,该数组对象持有的每个元素为AccessibilityNodeInfo对象,此处的每个AccessibilityNodeInfo对象表示屏幕中任意一个Window持有的View树的根节点对象,假设该数组对象一共持有2个元素,此时则说明当前屏幕一定存在两个Window。

说明:上面提及的UiAutomation对象的getRootInActiveWindow()方法是如何获得表示某一个Window持有的View树的根结点对象的呢?另外UiAutomation的getWindows()方法又是如何定位到一个View树中的具体控件的呢?这里继续留个疑问,后续文章中会单独总结UiAutmation的一系列方法!!我们还是先看下ByMatcher的findMatch方法是如何实现查找到一个控件的……

findMatch(UiDevice,BySelector,AccessibilityNodeInfo)静态方法分析

    定义在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;
    }

第一个参数UiDevice对象表示一台Android设备、第二个参数BySelector对象表示查找控件时使用的条件,第三个参数AccessiblityNodeInfo数组对象表示每个窗口持有的View树的根View的集合,每个元素AccessiblityNodeInfo表示屏幕中每个Window持有的View树的根结点对象。对屏幕上每个Window持有的View树进行遍历查找,查找符合BySelector条件的控件,而结果则是可能出现2种情况,第一种情况是在某个Window的View树中找到多个符合条件的控件,整个方法只会返回第一个根据条件匹配成功的控件,多余匹配的控件等于被忽略掉了……;第二种情况是根据条件没有找到匹配的控件,此时整个方法只会返回一个null,下面是详细的实现过程……

1、创建ByMatcher对象

首先创建一个ByMatcher对象,ByMatcher构造方法接受了从当前findMatch()方法传入的UiDevice对象、BySelector对象、一个boolean值,后面会使用此ByMatcher对象的查找控件功能

2、遍历屏幕中所有可见的Window,从每一个Window的根节点开始进行遍历查找控件,遍历过程中,只要在任意一个Window的View树的找到一个或者多个匹配的控件,直接返回给调用者查找控件的结果,后续的Window的View树不会再被遍历。(控件定位冲突)

接着对传入的AccessibilityNodeInfo数组对象进行遍历,这是对设备屏幕可见的每一个Window进行遍历,一旦在某一个Window中根据匹配条件查找出来一个或者多个符合要求的控件,此时就不会再费劲继续在后续的Window中继续做查找控件的工作,遍历工作还会立刻停止,并返回给调用者查找控件的结果。具体的匹配过程是将AccessibilityNodeInfo数组对象的每一个元素,即AccessibilityNodeInfo对象传入到ByMatcher对象matcher的findMatchers()方法中,每个AccessibilityNodeInfo对象代表的是View树的根节点对象(一个Window对应且持有一个View树,View的根节点是DecorView)。根据控件的BySelector条件对象在View树中查找控件,每个Window持有的View树中可能会有多个控件符合BySelector条件对象的要求,所以在ByMatcher对象的findMatchers()方法返回的是一个List对象,这个容器List对象持有的每个元素是AccessibilityNodeInfo对象,不过在当前Window持有的View树中找到一个或多个匹配的控件(此时matchers不会是空的),整个方法直接返回第一个成功匹配的控件……,后续没有参与遍历的Window,也没有机会再被去查找控件。我相信这也是为何明明写的BySelector却找不到控件,因为在View树中有一个符合条件的控件被返回了,你预期的控件并不是第一个被匹配的控件!

3、在屏幕中所有Window持有的View树中都没有找到一个符合匹配条件的控件,方法返回null

该方法在所有Window持有的View树中,没有找到匹配条件的控件,就会返回null,表示没有找到匹配的控件

接下来继续学习ByMatcher的findMatchers()方法是如何根据View树的根结点找到匹配条件的控件?

(注意:ByMatcher的findMatchers()方法与ByMatcher的静态方法findMatcher()只差一个字母‘s’,很容易混淆……)

findMatchers(AccessibilityNodeInfo)方法分析

    位于ByMatcher类中的findMatchers方法,这个方法只接受1个AccessibilityNodeInfo对象,传入的AccessibilityNodeInfo对象表示任意一个Window持有的View树的根结点对象,一般具体到代码层面就是DecorView对象

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

1、使用重载的含有4个参数的findMatchers()进行查找控件

开始先调用了一个重载的需要4个参数的findMachers()方法,这个方法返回的List对象由局部变量ret负责保存。List对象持有的元素是AccessibilityNodeInfo对象,List对象持有的每一个元素表示匹配成功的控件,如果这个List持有的元素超过1个,则说明我们构建的BySelector对象的设计根本就不合理,最佳的BySelector对象应该只能匹配出一个唯一的控件才是。重载的findMachers()方法要求传入4个参数,这4个参数非常重要,第一个参数AccessibilityNodeInfo对象表示某个Window持有的View树的根节点对象,此处只要传入表示即将要遍历的View树的根节点对象即可,在当前方法中,它就是当前结点;第二个参数index表示当前节点在其父节点下的索引,此时我们传入的是0,因为第一个参数是作为View树的根节点,根结点没有父节点的,所以0就很准确(后面我看到第一个直接子元素的index为0,此处是不是为-1更合适?根结点没有父节点,可以说它是唯一的结点,0也算准确,我们就假设它有一个虚拟的父节点8吧);第三个参数depth表示当前节点与根节点之间的距离,此处也传入的也是0,因为第一个参数作为当前节点,它本身就是一个根节点,所以自己距离自己的距离肯定为0(我都没找见这个depth值在哪里可以获得……);index与depth这两个参数是非常重要的知识点。第四个参数需要传入一个SinglyLinkedList对象,这个对象是一个自定义的单链表对象,它专门用来存储每个匹配成功的控件对象!

2、没有找到匹配的控件,触发所有注册的UiWatcher对象

当局部变量ret指向的List对象没有持有任何AccessibilityNodeInfo元素时,说明此时屏幕上所有可见的Window持有的View树中没有找到符合我们指定条件的元素,此时会先执行一个容错机制(查找控件功能自动触发一次容错机制,牛逼),ByMatcher对象持有一个UiDevice对象mDevice,调用UiDevice对象的runWatchers()方法,runWatchers()方法会触发在UiDevice中注册的所有作为观察者的UiWatch对象的checkCondition()方法被执行,我们可以在checkCondition()方法中执行具体的容错,在容错方式上有两种,一个是简单粗暴的方式:比如判处有系统弹窗,然后直接关闭系统弹窗,另一个则是比较婉转的方式:只是先判断系统弹窗的弹出状况,以后在其它的测试用例方法中再使用UiDevice的判断哪个UiWatcher被触发再进一步做具体的容错处理!

3、 未找到控件的情况下,使用findMatchers()方法再次尝试查找控件(这是第二次查找)

运行完所有已注册的UiWatcher对象的checkCondition()方法后,说明因为没有找到控件的容错机制已经全部完成,此时会再尝试查找控件一次,重试的工作仍然是由重载的4个参数的findMatchers()方法进行的,查找的所有控件还是赋值给局部变量ret进行保存

4、向调用者返回结果,所有匹配的控件由ret指向的List对象负责持有

返回持有匹配控件的List对象ret。

注意:此时返回的List对象,可能只持有了0个元素,0个元素表示并没有找到匹配的控件!

注意:这个查找控件的功能,一次没有找到控件的情况,作者又做了容错,然后再一次做了查找,也就是说,理想情况是查找控件一次,最差的情况则会查找控件两次,两次都没有查找到控件,查找工作才结束!这段代码好好学学真的没错!

说明:每次查找控件的工作,都是由4个参数的findMatchers()实际进行的,那么这个findMatchers()方法是如何查找到一个具体的控件的呢?继续学习这个4个参数的findMatchers()方法做了些什么!往下看!

findMatchers()方法分析

    位于ByMatcher类的findMatchers方法,它是一个重载方法,接受4个参数,第一个参数node是AccessibilityNodeInfo对象(此处的AccessibilityNodeInfo对象表示一个Window持有的View树的根节点,或者也可以传入View树中的任意一个表示控件的结点对象),第二个参数index表示当前node节点(第一个参数表示当前节点)在其父节点下面的索引位置,第三个参数depth表示当前node结点与根节点(DecorView)之间的距离,第四个参数partialMatches是SinglyLInkedList类的对象,它表示一个持有PartialMatch对象的列表,其实是一个单链表,而每个PartialMatch对象表示匹配成功的控件

说明:index与depth相对的结点对象,index是当前结点相对直接父节点的位置,而depth则表示当前结点相对View树根结点的位置

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

下面是对该方法拆解分析……

1、创建List对象,专门负责保存AccessibilityNodeInfo对象

最开始先创建一个ArrayList对象,赋值给局部变量ret保存,这个ArrayList对象负责持有多个AccessibilityNodeInfo对象,每个AccessibilityNodeInfo对象表示根据条件匹配成功的控件

2、使用传入的View树的根结点,进行可见性判断

若View树的根结点node对用户不可见,则说明以node为根结点的整个View树对用户都是不可见的状态,所以此时就不会在View树中进行控件查找工作,整个方法直接返回局部变量List对象ret,作者给的注释很有意思:Don't bother searching the subtree if it is not visible(如果子树不可见,就不必费心搜索)

注意:此处虽然说明某个子树的根结点,比如一个TextView是作为终端结点,它也算做一棵子树,只不过这棵子树只有它自己而已,这里充分说明如果一个控件对用户是不可见的,比如TextView没有文本信息,此时就算对用户不可见,尽管你有TextView的id,但仍然算作找不到该控件!(有待进一步验证)

private static final int BOOLEAN_PROPERTY_VISIBLE_TO_USER = 0x00000800; //位于AccessibilityNodeInfo类中,表示控件是否可见的一个标志位值……

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

遍历传入的SinglyLinkedList对象,每次执行此单链表中的每个元素PartialMatch对象的update方法,update方法会返回一个SinglyLinkedList对象,每次都会将返回的对象赋值给传入的局部变量partialMatches(第四个参数)进行保存,局部变量partialMatches将永远指向一个全新的单链表(一个新增加了元素单链表)

4、匹配成功,创建一个新的结点对象

通过调用PartialMatch的静态方法accept(),这个accept()方法中进行了控件的匹配工作,匹配成功就会得到一个PartialMatch对象并由局部变量currentMatch负责存储,静态方法accept()需要四个参数,第一个参数是当前传入的node对象,第二个参数是ByMatcher对象持有的BySelector对象,第三个是则是传入的局部变量index,第四个是depth,每当accept方法返回一个PartialMatch对象,就表示根据BySelector对象成功匹配到一个控件

5、将匹配成功的控件添加到自定义的单链表中,并更新局部变量partialMatches

再次调用SinglyLinkedList的静态方法prepend(),prepend()方法返回的又是一个SinglyLinkedList对象,这次返回的SinglyLinkedList对象又会交给局部变量partialMatches保存(这个局部变量真的好辛苦,指向的对象,一直在更新……),每次新返回的SinglyLinkedList对象,自身持有的元素数量也增加了, SinglyLinkedList是一个单链表容器……prepend()方法采用的是的头插法……

6、遍历每一个子View

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

7、View树遍历(学好树是多么重要)……匹配的控件,会被一个List持有,这个List每个元素表示满足匹配条件的控件……

accept(AccessibilityNodeInfo,BySelector)方法分析

        public static PartialMatch accept(AccessibilityNodeInfo node, BySelector selector,
                int index, int depth) {
            return accept(node, selector, index, depth, depth);
        }

实际负责根据条件与控件属性进行匹配的方法,内部会调用重载的5个参数的accept方法

accept()方法分析(查找控件的实际匹配过程)

        public static PartialMatch accept(AccessibilityNodeInfo node, BySelector selector,
                int index, int absoluteDepth, int relativeDepth) {

            if ((selector.mMinDepth != null && relativeDepth < selector.mMinDepth) ||
                    (selector.mMaxDepth != null && relativeDepth > selector.mMaxDepth)) {
                return null;
            }

            // NB: index is not checked, as it is not a BySelector criteria (yet). Keeping the
            // parameter in place in case matching on index is really needed.

            PartialMatch ret = null;
            if (checkCriteria(selector.mClazz, node.getClassName()) &&
                    checkCriteria(selector.mDesc, node.getContentDescription()) &&
                    checkCriteria(selector.mPkg, node.getPackageName()) &&
                    checkCriteria(selector.mRes, node.getViewIdResourceName()) &&
                    checkCriteria(selector.mText, node.getText()) &&
                    checkCriteria(selector.mChecked, node.isChecked()) &&
                    checkCriteria(selector.mCheckable, node.isCheckable()) &&
                    checkCriteria(selector.mClickable, node.isClickable()) &&
                    checkCriteria(selector.mEnabled, node.isEnabled()) &&
                    checkCriteria(selector.mFocused, node.isFocused()) &&
                    checkCriteria(selector.mFocusable, node.isFocusable()) &&
                    checkCriteria(selector.mLongClickable, node.isLongClickable()) &&
                    checkCriteria(selector.mScrollable, node.isScrollable()) &&
                    checkCriteria(selector.mSelected, node.isSelected())) {

                ret = new PartialMatch(selector, absoluteDepth);
            }
            return ret;
        }

执行过程分析(AccessibilityNodeInfo对象持有的属性与BySelector对象持有的属性进行比较)

1、检查传入值是否符合View树的最小深度与最大深度

不符合的情况,会直接返回null

2、正式开始做分配,你会发现一个彩蛋,没有设置的检查条件,都算作通过!

3、如果控件的条件匹配,会返回一个PartialMatch对象,表示成功,失败则返回一个null

两个重载checkCriteria()方法完成所有条件匹配的比较工作

1、控件相关的字符串类型属性比较

    static private boolean checkCriteria(Pattern criteria, CharSequence value) {
        if (criteria == null) {
            return true;
        }
        return criteria.matcher(value != null ? value : "").matches();
    }

第一个参数是我们自动化代码中传入的条件,第二个是表示控件的AccessibilityNodeInfo对象持有的值

2、控件相关的boolean值类型属性比较

总结

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

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

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

4、本文内容偏多,有点乱,我会再次总结

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值