Appium appium Bootstrap UiSelectorParser 源码分析

saii · 2015年08月05日 · 最后由 米阳MeYoung 回复于 2015年08月17日 · 205 次阅读
本帖已被设为精华帖!

由于是从 UiAutomator 阵营转到 appium 上来的,所以留下了个不太好的习惯就是习惯性的使用 appium 的 find_element_by_android_uiautomator 进行控件元素的定位。那疑问就来了 appium 又是如何通过我传入的字符串进行解析的。这里说来惭愧,实际上只是看了 UiSelectorParser 但是 appium 到底什么时候调用它我还没去详细看过。
进入到 appium 源代码目录的

这里我们关注 UiSelectorParser.java 从字面就能够明白它是用来解析你的 UiSelector 语句的,还有一个 UIScrollableParser 就不用我解释了是干什么的了。
那么就进去看看 UiSelectorParser 到底做了些什么吧
首先进来先看到这句

private final static Method[] methods = UiSelector.class.getDeclaredMethods();

一个静态的 methods 数组,这里是获取了 UiSelector 这个类中的所有方法(不包括继承),后面会拿你传入参数中的方法与 methods 数组进行比较。

public UiSelector parse(String textToParse) throws UiSelectorSyntaxException {
    selector = new UiSelector();
    text = cleanseText(textToParse);

    while (text.length() > 0) {
      consumePeriod();
      consumeFunctionCall();
    }
    return selector;
  }

这里我们先举个例子,假设现在传入的参数内容是

"new UiSelector().resourceId(\"com.seewo.teachercare:id/pass_msg_head_name_textView\").checked(true)"

解析函数就这么几句话,首先是 new 出一个 UiSelector 对象,在来进入 cleansetText 中 看看这个方法做了什么

private String cleanseText(String dirtyText) {
    String cleanText = dirtyText.trim();

    if (cleanText.startsWith("new UiSelector()")) {
      cleanText = cleanText.substring(16);
    }
    else if (cleanText.startsWith("UiSelector()")) {
      cleanText = cleanText.substring(12);
    }
    else if (!cleanText.startsWith(".")){
      cleanText = "." + cleanText;
    }

    return cleanText;
  }

哦 原来如此直接是将 UiSelector 给过滤了,那么刚才我传入的字符串现在就变成了

".resourceId(\"com.seewo.teachercare:id/pass_msg_head_name_textView\").checked(true)"

不过这里就要注意一点很坑的地方,因为 appium 传 uiautomator 的方法都是字符串的形式,所以你一定要保证空格以及大小写正确,要是你在 new 与 UiSelector 之间再多一个空格的话,估计你找半天都不知道啥原因。
继续往下走

while (text.length() > 0) {
      consumePeriod();
      consumeFunctionCall();
    }

这里循环判断文本的长度,如果文本内容没有解析完就继续解析,consumePeriod 这个方法的作用只是判断字符串开头有没有带'.'如果有就去掉,所以文本的解析以及 UiSelector 方法的调用都是在 consumeFunctionCall 方法里面。
那么就来看看 consumeFunctionCall 方法吧。

private void consumeFunctionCall() throws UiSelectorSyntaxException {
    String methodName;
    StringBuilder argument = new StringBuilder();

    int parenIndex = text.indexOf('(');
    //这里获取到方法名,通过截取‘(’ 刚好就得到了 resourceId
    methodName = text.substring(0, parenIndex);
    //下面的一大串则是为了获取'('以及')'中的内容,结果 argument的内容就是     //"com.seewo.teachercare:id/pass_msg_head_name_textView"
    int index = parenIndex+1;
    int parenCount = 1;
    while (parenCount > 0) {
      try {
        switch (text.charAt(index)) {
          case ')':
            parenCount--;
            if (parenCount > 0) {
              argument.append(text.charAt(index));
            }
            break;
          case '(':
            parenCount++;
            argument.append(text.charAt(index));
            break;
          default:
            argument.append(text.charAt(index));
        }
      } catch (StringIndexOutOfBoundsException e) {
        throw new UiSelectorSyntaxException("unclosed paren in expression");
      }
      index++;
    }
    if (argument.length() < 1) {
      throw new UiSelectorSyntaxException(methodName + " method expects an argument");
    }

    //设置剩余的文本内容+2的意思是之前被过滤掉的左右括号 那么这里的结果就是.checked(true)
    // add two for parentheses surrounding arg
    text = text.substring(methodName.length() + argument.length() + 2);
    //这里是将最开始获取的method的数组的函数名与传入的methodName做比较 将相同的已ArrayList进行返回
      //得到 public com.android.uiautomator.core.UiSelector com.android.uiautomator.core.UiSelector.resourceId(java.lang.String)
    ArrayList<Method> overloadedMethods = getSelectorMethods(methodName);
    if (overloadedMethods.size() < 1) {
      throw new UiSelectorSyntaxException("UiSelector has no " + methodName + " method");
    }

    selector = applyArgToMethods(overloadedMethods, argument.toString());
  }

好了剩下 applyArgToMethods 了看到它的返回值是 UiSelector 就知道实际上执行 UiAutomator 的代码是在这里了。

private UiSelector applyArgToMethods(ArrayList<Method> methods, String argument) throws UiSelectorSyntaxException {

    Object arg = null;
    Method ourMethod = null;
    UiSelectorSyntaxException exThrown = null;
    //这里遍历methods ,并且同时比较参数 argument与method方法的参数类型是否一致
    for (Method method : methods) {
      try {
        //获取参数类型
        Type parameterType = method.getGenericParameterTypes()[0];
       //比较参数类型,不一致的话就抛出异常
        arg = coerceArgToType(parameterType, argument);
        ourMethod = method;
      } catch (UiSelectorSyntaxException e) {
        exThrown = e;
      }
    }

    if (ourMethod == null || arg == null) {
      if (exThrown != null) {
        throw exThrown;
      } else {
        throw new UiSelectorSyntaxException("Could not apply argument " + argument + " to UiSelector method");
      }
    }
    //到这里一切OK的话 那么ourMethod就为public com.android.uiautomator.core.UiSelector com.android.uiautomator.core.UiSelector.resourceId(java.lang.String)
    //arg 为   com.seewo.teachercare:id/pass_msg_head_name_textView
    //下来看到invoke就知道了通过反射 通过对应的函数名反射UiSelector中的函数。返回selector.
    try {
      return (UiSelector)ourMethod.invoke(selector, arg);
    } catch (IllegalAccessException e) {
      e.printStackTrace();
      throw new UiSelectorSyntaxException("problem using reflection to call this method");
    } catch (InvocationTargetException e) {
      e.printStackTrace();
      throw new UiSelectorSyntaxException("problem using reflection to call this method");
    }

  }

重新回到 while 循环处,还记得刚才我们的 text 还剩下 .checked(true),那么就继续
下来大家肯定就明白了,method 为 checked 参数为 true,调用 UiSelector 中的 checked 方法。
以上的解释不知道能清楚不

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

相当清晰了!赞!

  • 定位脚本

    WebElement el = driver.findElementsByAndroidUIAutomator("new UiSelector().className(android.widget.ListView).childSelector(new UiSelector().className(android.widget.LinearLayout).clickable(true))");
    
  • 打开 findElementByAndroidUIAutomator 方法

@SuppressWarnings("unchecked")
@Override
public RequiredElementType findElementByAndroidUIAutomator(String using) {
    return (RequiredElementType) findElement("-android uiautomator", using);
}
  • 再打开 findElement("-android uiautomator", using) // 是 webdriver 的方法
protected WebElement findElement(String by, String using) {
    if(using == null) {
        throw new IllegalArgumentException("Cannot find elements when the selector is null.");
    } else {
        Response response = this.execute("findElement", ImmutableMap.of("using", by, "value", using));
        Object value = response.getValue();

        WebElement element;
        try {
            element = (WebElement)value;
        } catch (ClassCastException var7) {
            throw new WebDriverException("Returned value cannot be converted to WebElement: " + value, var7);
        }

        this.setFoundBy(this, element, by, using);
        return element;
    }
}

大概有点了解了 不过再底层一点的 就没看了

btw 楼主解析代码 一步步走 真好 很清晰呢 赞赞赞~

saii #3 · 2015年08月06日 Author

#2 楼 @jennyhui
重新又看了下源码,调试一步步走 大概知道了一些,这里只是说 Bootstrap 是如何执行到 UiSelectorParser
首先看 appium windows 的 log

可以知道的一点是 appium 发送给移动端的消息是

["find",{"strategy":"-android uiautomator","selector":"new UiSelector().resourceId(\"com.seewo.teachercare:id/pass_vote_body\")","context":"","multiple":false}]

看 doctorq 关于 Bootstrap 的源码分析就可以的出来 这里的 ACTION 是 find,
那就简单了 进到 Find.java 中看看就知道了。

private AndroidCommandResult execute(final AndroidCommand command,
      final boolean isRetry) throws JSONException {
    final Hashtable<String, Object> params = command.params();

    // only makes sense on a device
    final Strategy strategy;
    try {
      strategy = Strategy.fromString((String) params.get("strategy"));
    } catch (final InvalidStrategyException e) {
      return new AndroidCommandResult(WDStatus.UNKNOWN_COMMAND, e.getMessage());
    }

    final String contextId = (String) params.get("context");
    final String text = (String) params.get("selector");
    final boolean multiple = (Boolean) params.get("multiple");

    Logger.debug("Finding " + text + " using " + strategy.toString()
        + " with the contextId: " + contextId + " multiple: " + multiple);
    boolean found = false;
    try {
      Object result = null;
      final List<UiSelector> selectors = getSelectors(strategy, text, multiple);

我这里只是截取了部分,看到最后一句 getSelector 返回的是一个 List所以肯定是在 getSelectors 中进行查找的
在看看 getSelectors

private List<UiSelector> getSelectors(final Strategy strategy,
     final String text, final boolean many) throws InvalidStrategyException,
     ElementNotFoundException, UiSelectorSyntaxException,
     ParserConfigurationException, InvalidSelectorException {
   final List<UiSelector> selectors = new ArrayList<UiSelector>();
   UiSelector sel = new UiSelector();

   switch (strategy) {
     case XPATH:
       for (final UiSelector selector : getXPathSelectors(text, many)) {
         selectors.add(selector);
       }
       break;
     case CLASS_NAME:
       sel = sel.className(text);
       if (!many) {
         sel = sel.instance(0);
       }
       selectors.add(sel);
       break;
     case ID:
       // There are three types of ids on Android.
       // 1. resourceId (API >= 18)
       // 2. accessibility id (content description)
       // 3. strings.xml id
       //
       // If text is a resource id then only use the resource id selector.
       if (API_18) {
         if (resourceIdRegex.matcher(text).matches()) {
           sel = sel.resourceId(text);
           if (!many) {
             sel = sel.instance(0);
           }
           selectors.add(sel);
           break;
         } else {
           // not a fully qualified resource id
           // transform "textToBeChanged" into:
           // com.example.android.testing.espresso.BasicSample:id/textToBeChanged
           // android:id/textToBeChanged
           // either it's prefixed with the app package or the android system page.
           String pkg = (String) params.get("pkg");

           if (pkg != null) {
             sel = sel.resourceId(pkg + ":id/" + text);
             if (!many) {
               sel = sel.instance(0);
             }
             selectors.add(sel);
           }

           sel = sel.resourceId("android:id/" + text);
           if (!many) {
             sel = sel.instance(0);
           }
           selectors.add(sel);
         }
       }

       // must create a new selector or the selector from
       // the resourceId search will cause problems
       sel = new UiSelector().description(text);
       if (!many) {
         sel = sel.instance(0);
       }
       selectors.add(sel);

       // resource id and content description failed to match
       // so the strings.xml selector is used
       final UiSelector stringsXmlSelector = stringsXmlId(many, text);
       if (stringsXmlSelector != null) {
         selectors.add(stringsXmlSelector);
       }
       break;
     case ACCESSIBILITY_ID:
       sel = sel.description(text);
       if (!many) {
         sel = sel.instance(0);
       }
       selectors.add(sel);
       break;
     case NAME:
       sel = new UiSelector().description(text);
       if (!many) {
         sel = sel.instance(0);
       }
       selectors.add(sel);

       sel = new UiSelector().text(text);
       if (!many) {
         sel = sel.instance(0);
       }
       selectors.add(sel);
       break;
     case ANDROID_UIAUTOMATOR:
       List<UiSelector> parsedSelectors;
       try {
         parsedSelectors = uiAutomatorParser.parse(text);
       } catch (final UiSelectorSyntaxException e) {
         throw new UiSelectorSyntaxException(
             "Could not parse UiSelector argument: " + e.getMessage());
       }

       for (final UiSelector selector : parsedSelectors) {
         selectors.add(selector);
       }

       break;
     case LINK_TEXT:
     case PARTIAL_LINK_TEXT:
     case CSS_SELECTOR:
     default:
       throw new InvalidStrategyException("Sorry, we don't support the '"
           + strategy.getStrategyName() + "' locator strategy yet");
   }

   return selectors;
 }

内容很多,但是我们只看我们需要的 也就是 Strategy 为 ANDROID_UIAUTOMATOR 的
看到这里 就知道了 parsedSelectors = uiAutomatorParser.parse(text);
调用了 uiAutomatorParser 的解析,剩下的就简单了,uiAutomatorParser 的工作先对传进来的参数做些处理 类似于去掉空格之类的再来就判断你的字符串是 new UiSelecotor 还是 new UiScrollable 分别到对应的地方进行解析
不过这里还有个疑问,

while (index < text.length()) {
     if (text.charAt(index) == ';' && parenCount == 0) {
       break;
     }
     if (text.charAt(index) == '(') {
       if (parenCount < 0) {
         parenCount = 1;
       } else {
         parenCount++;
       }
     }
     if (text.charAt(index) == ')') {
       parenCount--;
     }
     index++;
   }

uiAutomatorParser 会判断文本内容中是否包含有';'这个又是干什么呢,

#3 楼 @zsx10110 卧槽! 就是喜欢你这种人! 👏 我先 mark 着~明天再来看你这一段研究啊哈哈哈 😄

#3 楼 @zsx10110 关于你的最后一个问题

private void consumeStatement() throws UiSelectorSyntaxException {
    String statement;
    int index = 0;
    int parenCount = -1; // semicolons could appear inside String arguments, so we make sure we only count occurrences outside of a parenthesis pair
    while (index < text.length()) {
      if (text.charAt(index) == ';' && parenCount == 0) {
        break;
      }
      if (text.charAt(index) == '(') {
        if (parenCount < 0) {
          parenCount = 1;
        } else {
          parenCount++;
        }
      }
      if (text.charAt(index) == ')') {
        parenCount--;
      }
      index++;
    }

driver.findElement(MobileBy.AndroidUIAutomator("new UiSelector().index(0)"));

你把定位脚本传入得字符串放进去就知道了。
定义了一个 parenCount=-1 做标示;
遇到"(" -> parenCount=1
遇到")" -> parentCount--
所以 parenCount 最后肯定等于 0。
而传入的字符串是允许有 “;” 的,只不过只能放在分号外 (semicolons could appear inside String arguments, so we make sure we only count occurrences outside of a parenthesis pair)
所以也就是在解析字符串时,到了最后一个字符如果是";"就直接 break;如果不是那就 index++,最后也是不符合 while 条件跳出来的~

saii #6 · 2015年08月07日 Author

#5 楼 @jennyhui 有点明白了,但是我更疑惑的点是在于
还是同一个段代码

private void consumeStatement() throws UiSelectorSyntaxException {
   String statement;
   int index = 0;
   int parenCount = -1; // semicolons could appear inside String arguments, so we make sure we only count occurrences outside of a parenthesis pair
   while (index < text.length()) {
     if (text.charAt(index) == ';' && parenCount == 0) {
       break;
     }
     if (text.charAt(index) == '(') {
       if (parenCount < 0) {
         parenCount = 1;
       } else {
         parenCount++;
       }
     }
     if (text.charAt(index) == ')') {
       parenCount--;
     }
     index++;
   }

   statement = text.substring(0, index);
   if (UiScrollableParser.isUiScrollable(statement)) {
     Logger.debug("Parsing scrollable: " + statement);
     selectors.add(scrollableParser.parse(statement));
   } else {
     Logger.debug("Parsing selector: " + statement);
     selectors.add(selectorParser.parse(statement));
   }

   text = text.substring(index);
 }

因为 selectors 是一个 List所以如果说存在有分号的话,那就应该会产生多个 selector 就是根据分号来进行划分的吧。
难道说还可以有这种用法?

driver.findElement(MobileBy.AndroidUIAutomator("new UiSelector().index(0);new UiSelector().checked(true)"));

#6 楼 @zsx10110

没有这种用法。
其实分号不加可以定位到,那就不用加了。定位多个元素 appium 有封装好的方法可以直接用。

saii #8 · 2015年08月07日 Author

#7 楼 @jennyhui 我想说实际上加分号是可以的 只是 appium 做了些处理。
我查找的代码如下

driver.find_element_by_android_uiautomator('new UiSelector().resourceId("'+Id+'");new UiSelector().clickable(false)')

接着看看 appium for windows 的 log

实际上两条 selector 语句都是会执行的,但是问题来了为什么 log 打印还是采用

Using: UiSelector[RESOURCE_ID=com.seewo.teachercare:id/pass_notice_list_add_notice_imageButton]

这个呢,而忽略了我的后面那个 clickable 的 selector 实际上看 FIND 函数就知道了

final List<UiSelector> selectors = getSelectors(strategy, text, multiple);
      if (!multiple) {
        for (int i = 0; i < selectors.size() && !found; i++) {
          try {
            Logger.debug("Using: " + selectors.get(i).toString());
            result = fetchElement(selectors.get(i), contextId);
            found = result != null;
          } catch (final ElementNotFoundException ignored) {
          }
        }
      }

这里实际上查找按顺序来,如果找到的话 就赋值给了 found,接着 for 循环中如果 found 为空的情况下才会继续往下走,所以前面只是查找到 resoureID 就跳出来了,我估计分号的用法是个或的关系 即如果这种情况下找不到 再换另外一种 selector 进行查找为了验证这个 我换下我的查找

driver.find_element_by_android_uiautomator('new UiSelector().className("hehe.hehe");new UiSelector().resourceId("'+Id+'")')

这个时候看下 log 就一目了然了。

结论:分号的用法是一个或的用法,前面成立的话后面就不执行了。

#8 楼 @zsx10110 赞赞赞!!! 不过这种方式我真是没用过 :plus1: 太厉害了 ~!!!

saii #10 · 2015年08月07日 Author

#9 楼 @jennyhui 说实话我也是没用过,今天中午研究了下,还真可以。我也学习了。

#10 楼 @zsx10110 嗯嗯很喜欢您的学习风格啊大赞~~学习学习了~thx

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