在Appium基础学习之 | Bootstrap源码分析留的尾巴,然后通过Appium基础学习之 | UiAutomator使用过渡简单介绍了一下UiAutomator、由于在Android设备中是由UiAutomator工具接管,所以接下来看看Appium代码是如何转化为UiAutomator代码的。
在Appium基础学习之 | Bootstrap源码分析这篇文章的最后部分是map初始化的代码,这个map包含了全部对Android应用的操作,代码再重复贴一下
static {
map.put("waitForIdle", new WaitForIdle());
map.put("clear", new Clear());
map.put("orientation", new Orientation());
map.put("swipe", new Swipe());
map.put("flick", new Flick());
map.put("drag", new Drag());
map.put("pinch", new Pinch());
map.put("click", new Click());
map.put("touchLongClick", new TouchLongClick());
map.put("touchDown", new TouchDown());
map.put("touchUp", new TouchUp());
map.put("touchMove", new TouchMove());
map.put("getText", new GetText());
map.put("setText", new SetText());
map.put("getName", new GetName());
map.put("getAttribute", new GetAttribute());
map.put("getDeviceSize", new GetDeviceSize());
map.put("scrollTo", new ScrollTo());
map.put("find", new Find());
map.put("getLocation", new GetLocation());
map.put("getSize", new GetSize());
map.put("getRect", new GetRect());
map.put("wake", new Wake());
map.put("pressBack", new PressBack());
map.put("pressKeyCode", new PressKeyCode());
map.put("longPressKeyCode", new LongPressKeyCode());
map.put("takeScreenshot", new TakeScreenshot());
map.put("updateStrings", new UpdateStrings());
map.put("getDataDir", new GetDataDir());
map.put("performMultiPointerGesture", new MultiPointerGesture());
map.put("openNotification", new OpenNotification());
map.put("source", new Source());
map.put("compressedLayoutHierarchy", new CompressedLayoutHierarchy());
map.put("configurator", new ConfiguratorHandler());
}/**
* Gets the handler out of the map, and executes the command.
*
* @param command
* The {@link AndroidCommand}
* @return {@link AndroidCommandResult}
*/
public AndroidCommandResult execute(final AndroidCommand command) {
try {
Logger.debug("Got command action: " + command.action());if (map.containsKey(command.action())) {
return map.get(command.action()).execute(command);
} else {
return new AndroidCommandResult(WDStatus.UNKNOWN_COMMAND,
"Unknown command: " + command.action());
}
} catch (final JSONException e) {
Logger.error("Could not decode action/params of command");
return new AndroidCommandResult(WDStatus.JSON_DECODER_ERROR,
"Could not decode action/params of command, please check format!");
}
}
可以看到包含了很多比较常见的find、clear、click、setText(sendKeys)等等,不可能全部的操作都讲一遍,所以根据前面的示例中使用的find、click方法讲解。取出map中对应的实例化对象执行execute()方法。
一、Find
先讲解Find,顾名思义这个方法就是找元素,从上一篇文章对UiAutomator的了解,很容易就能理解实际Appium中使用Find方法去查找得到的对象,被转化的相对于UiAutomator来说就是UiSelector对象。
1.Bootstrap的Find类
下面看源代码中execute()方法代码如下
@Override
public AndroidCommandResult execute(final AndroidCommand command)
throws JSONException {
return execute(command, false);
}扫描二维码关注公众号,回复: 5037997 查看本文章/**
* execute implementation.
*
* @see io.appium.android.bootstrap.handler.Find#execute(io.appium.android.
* bootstrap.AndroidCommand)
*
* @param command
* The {@link AndroidCommand} used for this handler.
*
* @param isRetry
* Is this invocation a second attempt?
*
* @return {@link AndroidCommandResult}
* @throws JSONException
*/
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, contextId);
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) {
}
}
} else {
List<AndroidElement> foundElements = new ArrayList<AndroidElement>();
for (final UiSelector sel : selectors) {
// With multiple selectors, we expect that some elements may not
// exist.
try {
Logger.debug("Using: " + sel.toString());
final List<AndroidElement> elementsFromSelector = fetchElements(
sel, contextId);
foundElements.addAll(elementsFromSelector);
} catch (final UiObjectNotFoundException ignored) {
}
}
if (strategy == Strategy.ANDROID_UIAUTOMATOR) {
foundElements = ElementHelpers.dedupe(foundElements);
}
found = foundElements.size() > 0;
result = elementsToJSONArray(foundElements);
}if (!found) {
if (!isRetry) {
Logger
.debug("Failed to locate element. Clearing Accessibility cache and retrying.");
// some control updates fail to trigger AccessibilityEvents, resulting
// in stale AccessibilityNodeInfo instances. In these cases, UIAutomator
// will fail to locate visible elements. As a work-around, force clear
// the AccessibilityInteractionClient's cache and search again. This
// technique also appears to make Appium's searches conclude more quickly.
// See Appium issue #4200 https://github.com/appium/appium/issues/4200
if (ReflectionUtils.clearAccessibilityCache()) {
return execute(command, true);
}
}
// JSONWP spec does not return NoSuchElement
if (!multiple) {
// If there are no results and we've already retried, return an error.
return new AndroidCommandResult(WDStatus.NO_SUCH_ELEMENT,
"No element found");
}
}return getSuccessResult(result);
} catch (final InvalidStrategyException e) {
return getErrorResult(e.getMessage());
} catch (final UiSelectorSyntaxException e) {
return new AndroidCommandResult(WDStatus.UNKNOWN_COMMAND, e.getMessage());
} catch (final ElementNotFoundException e) {
return new AndroidCommandResult(WDStatus.NO_SUCH_ELEMENT, e.getMessage());
} catch (final ParserConfigurationException e) {
return getErrorResult("Error parsing xml hierarchy dump: "
+ e.getMessage());
} catch (final InvalidSelectorException e) {
return new AndroidCommandResult(WDStatus.INVALID_SELECTOR, e.getMessage());
}
}
可以看到AndroidCommandExecutor类中调用的是第一个execute()方法,然后自身再调用第二个execute()方法,所以重点是第二个execute()方法。
(1)首先看第一行代码
final Hashtable<String, Object> params = command.params();
得到一个Hashtable,从command.params()方法中返回,command是调用execute()方法入参的AndroidCommand对象,所以进入到AndroidCommand类的params()方法。
2.Bootstrap的AndroidCommand类
public AndroidCommand(final String jsonStr) throws JSONException,
CommandTypeException {
json = new JSONObject(jsonStr);
setType(json.getString("cmd"));
}......
/**
* Return a hash table of name, value pairs as arguments to the handlers
* executing this command.
*
* @return Hashtable<String, Object>
* @throws JSONException
*/
public Hashtable<String, Object> params() throws JSONException {
final JSONObject paramsObj = json.getJSONObject("params");
final Hashtable<String, Object> newParams = new Hashtable<String, Object>();
final Iterator<?> keys = paramsObj.keys();while (keys.hasNext()) {
final String param = (String) keys.next();
newParams.put(param, paramsObj.get(param));
}
return newParams;
}
上面代码主要列出了AndroidCommand构造函数以及params()方法,主要是因为需要用到AndroidCommand构造函数中初始化的JSON对象的数据。可以回看Appium基础学习之 | Bootstrap源码分析这篇文章中在SocketServer类中handleClientData()方法中调用getCommand()方法中入参了一个定位需要的String字符串。params()方法是取出其中的params的值,下面看看Appium的log中params是一些什么内容,如下图:
可以看到params是一个json串,里面是4个参数strategy、selector、context、multiple,前面3个都很好猜到,分别用于定位策略、定位位置、定位内容,multiple字段目前来说还不知道做什么的,在方法注释中暂时也没看到,后面用到的时候再看看。回到上面params()方法,把params的值取出加入到了Hashtable中,现在可以很清楚的知道Hashtable是什么内容了。
3.回到Bootstrap的Find类
从execute()方法第二行继续往下,分别是从Hashtable取出strategy、selector、context、multiple4个值。其中strategy还是Bootstrap中定义的对象,它具体是什么内容暂时不贴代码看了,猜也知道它应该是一些定位方法的内容。得到值后继续往下定义了一个布尔值found,一个Object对象;然后调用了getSelectors()方法。
/**
* Create and return a UiSelector based on the strategy, text, and how many
* you want returned.
*
* @param strategy
* The {@link Strategy} used to search for the element.
* @param text
* Any text used in the search (i.e. match, regex, etc.)
* @param many
* Boolean that is either only one element (false), or many (true)
* @return UiSelector
* @throws InvalidStrategyException
* @throws ElementNotFoundException
*/
private List<UiSelector> getSelectors(final Strategy strategy,
final String text, final boolean many, final String contextId) throws InvalidStrategyException,
ElementNotFoundException, UiSelectorSyntaxException, ParserConfigurationException, InvalidSelectorException {
final List<UiSelector> selectors = new ArrayList<UiSelector>();
UiSelector sel = new UiSelector();switch (strategy) {
case XPATH:
try {
selectors.addAll(getXPathSelectors(text, many, contextId));
} catch (final ElementNotFoundException ignore) {
}
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);// webview element ids do not have a package prefix
sel = sel.resourceId(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;
}
上面的getSelectors()方法代码的代码中,可以看看注释,注释中的many对应的就是上面不知道爹妈的multiple,这里有介绍Boolean that is either only one element (false), or many (true)它是用来区分要查找的元素是一个还是多个,这是由Appium协议中来规定的,如果使用的findElements()方法来查找元素,协议中multiple的值是true,如果用findElement()方法则是false。
回到在getSelectors()方法往下看,通过strategy来决定使用什么定位方式,从这里也可以看到Appium的定位方式有XPATH、CLASS_NAME、ID、ACCESSIBILITY_ID、NAME、ANDROID_UIAUTOMATOR、LINK_TEXT、PARTIAL_LINK_TEXT、CSS_SELECTOR共9种定位方法,先不急着看这么多种定位方法后面会专门开博文学习。从示例代码的id定位先了解源代码,先判断一下API大于等于18的时候才能使用ID定位,这里是做了很严谨的判断,实际上在Appium代码中如果引入的不是API18及以上的jar使用ID定位会报错。
通过正则表达式过滤下selector的值,具体怎么过滤或许不符合过滤规则执行else的代码简单看看,反正到最后都是通过UiSelector的resourceId方法得到UiSelector对象。这样就得到了UiAutomator可以识别的UiSelector对象啦,完成转换成功。而其它如XPATH、CLASS_NAME、ACCESSIBILITY_ID、NAME、ANDROID_UIAUTOMATOR、LINK_TEXT、PARTIAL_LINK_TEXT、CSS_SELECTOR等其他方法都有或者经过处理后对应UiSelector的方法完成转换。
然后再回到Find类的execute()方法,得到UiSelector对象往下就是判断multiple的值来执行单个或多个元素的操作,然后执行fetchElement方法
private JSONObject fetchElement(final UiSelector sel, final String contextId)
throws JSONException, ElementNotFoundException {
final JSONObject res = new JSONObject();
final AndroidElement el = elements.getElement(sel, contextId);
return res.put("ELEMENT", el.getId());
}
这个方法也很简单,显示调用了AndroidElementsHash的getElement()方法
4.Bootstrap的AndroidElementsHash类
AndroidElementsHash类的代码如下,只贴出了相关代码,其他省略号......表示
public class AndroidElementsHash {
private static final Pattern endsWithInstancePattern = Pattern.compile(".*INSTANCE=\\d+]$");
public static AndroidElementsHash getInstance() {
if (AndroidElementsHash.instance == null) {
AndroidElementsHash.instance = new AndroidElementsHash();
}
return AndroidElementsHash.instance;
}private final Hashtable<String, AndroidElement> elements;
private Integer counter;private static AndroidElementsHash instance;
public AndroidElementsHash() {
counter = 0;
elements = new Hashtable<String, AndroidElement>();
}
public AndroidElement addElement(final UiObject element) {
counter++;
final String key = counter.toString();
final AndroidElement el = new AndroidElement(key, element);
elements.put(key, el);
return el;
}......
public AndroidElement getElement(final UiSelector sel, final String key)
throws ElementNotFoundException {
AndroidElement baseEl;
baseEl = elements.get(key);
UiObject el;if (baseEl == null) {
el = new UiObject(sel);
} else {
try {
el = baseEl.getChild(sel);
} catch (final UiObjectNotFoundException e) {
throw new ElementNotFoundException();
}
}if (el.exists()) {
// there are times when UiAutomator returns an element from another parent
// so we need to see if it is within the bounds of the parent
try {
if (baseEl != null && !Rect.intersects(baseEl.getBounds(), el.getBounds())) {
Logger.debug("UiAutomator returned a child element but it is " +
"outside the bounds of the parent. Assuming no " +
"child element found");
throw new ElementNotFoundException();
}
} catch (final UiObjectNotFoundException e) {
throw new ElementNotFoundException();
}
return addElement(el);
} else {
throw new ElementNotFoundException();
}
}}
先判断了Hashtable取出key值,还没有加入任何元素,所以肯定是null;进入if语句块先初始化UiObject对象,往下接着走,exists()判断元素是否存在,元素存在最后调用了addElement加入到了Hashtable中并返回。如果根据UiSelector的信息找不到UIObject对象,则直接保存元素不存在。
5.回到Find类
fetchElement方法把数据转换JSON返回,完工