Appium uiautomatorviewer 二次开发之自动生成控件定位符

梦桥 · May 26, 2015 · Last by wyel2000 replied at August 29, 2017 · 8828 hits
本帖已被设为精华帖!

uiautomatorviewer二次开发之自动生成控件定位符

前言
我们在使用Appium进行移动自动化测试脚本编写的时候,经常出现控件无法定位,如ListView下面的item,控件基本属性一样的、某些控件没有id、name等,这个时候,如果单纯靠id、name、text可能无法完全唯一定位一个控件,这个时候就需要编写xpath了,可是xpath语法、写法对于没有接触过的测试人员来说,又是个门槛,接下来,我们就来讨论如何通过二次开发uiautomatorviewer自动生成xpath供用户直接copy使用。
原理
uiautomatorviewer是android SDK包中原生的开源工具,给用户提供一种查看当前终端布局、控件属性的一个辅佐工具,
该工具的GUI是使用RCP组件进行开发的,然后通过uiautomator dump把当前终端布局文件dump到本地,uiautomatorviewer
通过xml布局文件,构造一棵tree,放到Canvas SWT组件中,和当前png截图叠加在一起,同时监听鼠标move等事件,自动
获取该tree的node节点,并且把该node节点的所有属性获取显示出来。


代码结构

com.android.uiautomator:存放uiautomatorviewer工具的GUI界面代码,其中主入口UiAutomatorViewer.java文件里面有main函数入口,工具的窗口就在此创建。
com.android.uiautomator.actions:存放所有anction操作,如:Device screenshot 、open等。
com.android.uiautomator.tree:存放tree封装,dump出来的xml解析成一棵完整的tree,这个包是核心包。

二次开发
首先,dump出来的xml文件被uiautomationviewer解析成自定义的tree,每个节点代表一个控件,所以,如何添加Xpath属性呢?只需要在node节点中添加一个字段即可,其实很简单。通过阅读代码,在com.android.uiautomator.tree包下,有个node节点封装类,UiNode.java,看下以下代码片源:

public class UiNode extends BasicTreeNode {
private static final Pattern BOUNDS_PATTERN = Pattern
.compile("\\[-?(\\d+),-?(\\d+)\\]\\[-?(\\d+),-?(\\d+)\\]");

private final Map<String, String> mAttributes = new LinkedHashMap();
private String mDisplayName = "ShouldNotSeeMe";
private Object[] mCachedAttributesArray;

public void addAtrribute(String key, String value) {
this.mAttributes.put(key, value);
updateDisplayName();
if ("bounds".equals(key))
updateBounds(value);
}

public Map<String, String> getAttributes() {
return Collections.unmodifiableMap(this.mAttributes);
}

private void updateDisplayName() {
String className = (String) this.mAttributes.get("class");
if (className == null)
return;
String text = (String) this.mAttributes.get("text");
if (text == null)
return;
String contentDescription = (String) this.mAttributes
.get("content-desc");
if (contentDescription == null)
return;
String index = (String) this.mAttributes.get("index");
if (index == null)
return;
String bounds = (String) this.mAttributes.get("bounds");
if (bounds == null) {
return;
}

className = className.replace("android.widget.", "");
className = className.replace("android.view.", "");
StringBuilder builder = new StringBuilder();
builder.append('(');
builder.append(index);
builder.append(") ");
builder.append(className);
if (!text.isEmpty()) {
builder.append(':');
builder.append(text);
}
if (!contentDescription.isEmpty()) {
builder.append(" {");
builder.append(contentDescription);
builder.append('}');
}
builder.append(' ');
builder.append(bounds);
this.mDisplayName = builder.toString();

private final Map<String, String> mAttributes = new LinkedHashMap();UiNode 节点下定义一个mAttributes LinkedHashMap,用于存储节点所有key-value属性,如:className、text 、index等等,所以,只需要在这个UiNode 类下添加一个获取xpath方法,如下:

public String getXpath()
{
String className=getNodeClassAttribute();
String xpath="//"+className;
String text = getAttribute("text");
if(text !=null&& !text.equals(""))
{
xpath += "[@text='"+text+"']";
return xpath;
}else
{
return getAttribute("content-desc") !=""?
xpath+"[@content-desc='"+getAttribute("content-desc")+"']"
:xpath+"[@index='"+getAttribute("index")+"']";
}


}

根据约定的优先级,进行筛选(text>content-desc>index),方法定义完毕后,如何触发getXpath()代码呢?
同样,在此包下UiHierarchyXmlLoader.java中,该类是用于处理把dump xml转换为BasicTreeNode对象,UiHierarchyXmlLoader 引用org.xml.sax.helpers处理基本xml文件(Default base class for SAX2 event handlers.),实现了ContentHandler接口下的startElement、endElement接口,

    public void endElement(String uri, String localName, String qName)
throws SAXException {
if (this.mParentNode != null) {
this.mWorkingNode = this.mParentNode;
this.mParentNode = this.mParentNode.getParent();
`mTmpNode.addAtrribute("xpath",mTmpNode.getXpath());`
}
}
};

到这里,xpath就会自动出现在uiautomatorviewer界面上了,效果如下:

补充
其实uiautomatorviewer二次开发还不止这些,我们可以在uiautomatorviewer中加入录制自动生成java、python等appium脚本,还可以每次用户点击uiautomatorviewer界面,同步刷新(目前需要用户手动点击device screenshot 这个action)等等,如下:

第一次发分享贴,写的不好,希望大家多多鼓励,后续会继续分享

共收到 87 条回复 时间 点赞

顶,终于分享出来了

赞一个

赞 也就是说即便开发没有定义控件的text description或者device ID,我们依然可以对这个控件进行唯一性的自主命名对吗?

梦桥 #4 · May 26, 2015 作者

#3楼 @james88233 一开始,想做成你说这个思路的,完全通过布局如://FrameLayout/View/TextView类似这样的方式,后来发现这样做,如果app布局一旦做了调整,解析出来的xpath就不能复用,所以,后来就改成了一个控件,按照text>content-desc>index的优先级进行唯一定位,如:有text内容的,优先,其次是content-desc

最近也在想弄这种比较快速定位的东东,没想到你先弄出来了。

可以的。我支持你

#4楼 @cpfeng0124 首先我明白你关于不能复用的那个解释,然后我不太理解的是你接下来说的这个操作,如果uiautomatorviewer可以获取到控件的text desc等信息,我还要xpath的信息做什么呢?

梦桥 #8 · May 26, 2015 作者

#7楼 @james88233 我们这边遇到这种问题,2个控件,id、text都一样,只有desc或者index不一样,这个时候,就需要些xpath来区分这2个控件了

#8楼 @cpfeng0124 谢谢分享,如果能上传uiautomatorviewer未修改的源码更棒了

梦桥 #10 · May 26, 2015 作者

#9楼 @yuwuhen333 uiautomatorviewer本身就是开源的哦,可以通过反编译直接生成代码

#10楼 @cpfeng0124 反编译是直接解压缩jar包吗?还是需要工具进行反编译

梦桥 #12 · May 26, 2015 作者

#11楼 @yuwuhen333 z直接解压缩jar包哦

你好,请教下通过这个进行二次开发,是否也可以解决自定义控件无法定位的问题呢?谢谢~

赞!
有个疑问:如果遇到有两个控件 text 相同,那你生成的 xpath 是否就会一样?

梦桥 #15 · May 26, 2015 作者

#13楼 @mistyrain 基本可以的唯一定位的,目前通过xpath方式,还没发现定位不到的问题哦

梦桥 #16 · May 26, 2015 作者

#14楼 @chenhengjie123 吼吼,如果两个text相同的话,生成的xpath确实是一样的哦,有没有啥可以指点一下,如何改进啊?

@chenhengjie123 @cpfeng0124 看他这个代码逻辑只是判断了text是否为空,如果text相同,应该返回的xpath是一致的

梦桥 #18 · May 26, 2015 作者

#17楼 @app_testing 是的。

@cpfeng0124 ,如果是自定义控件的话,请问有没有思路呢,有人提议用HierachyViewer,但是感觉这个速度比较慢

#12楼 @cpfeng0124 压缩之后,文件夹中没有com.android.uiautomator.tree也没有UiNode.java,我的是Android19

#8楼 @cpfeng0124 我现在用的工具是黑盒的,所以我也不太清楚具体的架构,当你说的这种情况出现的时候,我们一般就用index=1 或者index=2来区分了 但是面对上图那种钟表式样的控件,就无能为力了,因为基本上text desc这些都没有,classname相同的一大堆,如果可以用xpath编译的话还是挺方便的,不过可能需要增加一些封装,让这个操作傻瓜化,比如tester点击控件可以直接对其命名

梦桥 #22 · May 26, 2015 作者

#19楼 @app_testing HierachyViewer我之前也做过,思路基本差不多,当时的需求是,需要测试手机整机全系统测试:如打电话、发短型、甚至并发,打电话的时候,上网啥的,当时就是基于HierachyViewer二次开发,全系统自动化测试的

#16楼 @cpfeng0124 大致思路:
在 dump 出来的 xml 文件中使用生成的 xpath 来查找节点,如果找到不止一个节点,就加上更多属性匹配条件(同样按照 text->content-desc->index,例如 android.view.view[@text="a" and @content-desc="b" and @index="1"])。如果都加上后还是不止一个元素,直接给个绝对路径吧。

可以录制脚本这个太赞了!能分享一下思路不?

@cpfeng0124 我的意思是HierachyViewer这个速度比较慢,你对这种情况有没有别的解决思路

梦桥 #25 · May 26, 2015 作者

#23楼 @chenhengjie123 录制脚本这个,我打算下周再做一次移动录制生成代码,因为第一次写这些分享,效率有点低啊,吼吼,谢谢你的指点!

梦桥 #26 · May 26, 2015 作者

#24楼 @app_testing 速度比较慢,主要卡在dump这个动作需要一定时间,这个我也在研究哦,吼吼!有啥想法,记得分享一下哈

#25楼 @cpfeng0124 期待你的分享!

#12楼 @cpfeng0124 能不能具体讲下怎么进行反编译,我的uiautomator.jar解压缩后,目录结构如下图
一直没看到你说的修改的文件

#28楼 @yuwuhen333 你找错包啦,是这个uiautomatorviewer.jar包,在sdk\tools\lib\uiautomatorviewer.jar 这个目录下哦

#29楼 @cpfeng0124 太感谢了,顺便再问一句解压缩之后,按照文章中的说明进行修改后,怎么重新打包成jar?

不好意思,楼主,您的源码能share一下么

梦桥 #32 · May 26, 2015 作者

#30楼 @yuwuhen333 正常打包就ok啦,要么导出来jar来,要么用maven打包

梦桥 #33 · May 26, 2015 作者

#31楼 @xxfcxx 额,这个源码暂时不能开放哦,因为里面还有其他功能,不止这个简单的xpath哦

#32楼 @cpfeng0124 将uinode变为java类型添加你的代码后,在转换为uinode.class报错了UiNode.java:123: 错误: 非法的 Unicode 转义,继续调整。能不能先把你修改成生成的uiautomatorviewer.jar分享让我们感受下

35Floor has been deleted

#32楼 @cpfeng0124 在配置UINode.java.中一直报String className=getNodeClassAttribute();找不到这个方法,这个方法是从哪里来的啊

梦桥 #37 · May 26, 2015 作者

#36楼 @yuwuhen333 getNodeClassAttribute(),这个方法,需要你自己添加一下哦,骚瑞啊,没说清楚,getNodeClassAttribute这个方法很简单,就返回private final Map<String, String> mAttributes = new LinkedHashMap();这里定义的mAttributes即可

#37楼 @cpfeng0124 能不能贴出来下?mTmpNode.addAtrribute("xpath",mTmpNode.getXpath());这个为什么在两边还有符号呢

#37楼 @cpfeng0124 不是返回这个map吧,String className=getNodeClassAttribute(), 这是String类型啊

#37楼 @cpfeng0124 是不是 要private String getNodeClassAttribute() {
// TODO Auto-generated method stub
return mAttributes.get("className");
}

梦桥 #41 · May 26, 2015 作者

#40楼 @xxfcxx 是的

#40楼 @xxfcxx 我写的是return this.mAttributes.get("class");

#41楼 @cpfeng0124 public void endElement(String uri, String localName, String qName)
throws SAXException {
if (this.mParentNode != null) {
this.mWorkingNode = this.mParentNode;
this.mParentNode = this.mParentNode.getParent();
mTmpNode.addAtrribute("xpath",mTmpNode.getXpath());
}
}
};这个代码中你加的那行为什么多了``呢

我按照你这里改完之后,重新打成uiautomator.jar,然后启动,提示 uiautomatorviewer.jar中没有主清单属性

#44楼 @xxfcxx UiHierarchyXmlLoader.java你修改好了吗

#45楼 @yuwuhen333 我修改了,但是在eclipse里一直报错,我是改完之后export jar,然后从jar包里copy这2个class文件,然后复制到uiautomator.jar解压后的文件夹,然后jar cvf打包。。。

#46楼 @xxfcxx 相同的提示,我也是修改UiHierarchyXmlLoader.java的时候各种错误
#41楼 @cpfeng0124 能不能提供个你只是添加了xpath功能的jar包出来?

mTmpNode 这个变量是哪来的,源码没有啊,是你加的一个成员变量么,有没有初始化啊

#37楼 @cpfeng0124 能不能提供个xpath的uiautomatorviewer.jar?

同问有木有提供jar?谢谢

很棒的分享啊,大赞

#37楼 @cpfeng0124
这样的元素,该如何定位?类似你文章中的这个元素,见下图红色框

这个元素的xpath能给一下吗?

梦桥 #53 · May 29, 2015 作者

#52楼 @yuwuhen333 老实说,这样的元素,比较苦恼啊,唯一可以是别的就是index,但是这个index又不是控件的属性,所以,可以结合layout控件进行定位吧,从layout找起,//linearlayout//android.view.View[7]这种类似的语法,自动生成xpath有待改进哈

#53楼 @cpfeng0124 self.driver.find_element_by_xpath("//linearlayout//android.view.View[7]").click()是类似这样的写法吗?//linearlayout//android.view.View[7]根据实际几层linearlayout就写几个?如图这样的该怎么写呢

梦桥 #55 · May 29, 2015 作者

#54楼 @yuwuhen333 driver.findElementByAndroidUIAutomator("new UiSelector().text('返回').fromParent(new UiSelector()......)");链式调用,不过,你这个是查找WebView里面的元素,可能就难定位了,WebView里面可以用WebElement来查找

梦桥 #56 · May 29, 2015 作者

new UiSelector().className("android.view.View").getChild(new UiSelector().index(1)),试试吧

梦桥 #57 · May 29, 2015 作者

#54楼 @yuwuhen333 new UiSelector().className("android.view.View").getChild(new UiSelector().index(1)),试试吧

#57楼 @cpfeng0124 好的,我稍后试试,到时候反馈个结果给大家。执行报错:
Traceback (most recent call last):
File "zb_rules.py", line 57, in test_rules
self.driver.find_elements_by_android_uiautomator("new UiSelector().className('android.view.View').getChild(new UiSelector().index(1))").click()
File "D:\Python27\lib\site-packages\appium\webdriver\webdriver.py", line 123, in find_elements_by_android_uiautomator
return self.find_elements(by=By.ANDROID_UIAUTOMATOR, value=uia_string)
File "D:\Python27\lib\site-packages\selenium\webdriver\remote\webdriver.py", line 679, in find_elements
{'using': by, 'value': value})['value']
File "D:\Python27\lib\site-packages\selenium\webdriver\remote\webdriver.py", line 175, in execute
self.error_handler.check_response(response)
File "D:\Python27\lib\site-packages\appium\webdriver\errorhandler.py", line 29, in check_response
raise wde
WebDriverException: Message: The requested resource could not be found, or a request was received using an HTTP method that is not supported by the mapped resource.

Appium 报错如下
info: --> POST /wd/hub/session/f34d3f04-3964-4d6c-a0bf-777e4ae40a6e/elements {"using":"-android uiautomator","sessionId":"f34d3f04-3964-4d6c-a0bf-777e4ae40a6e","value":"new UiSelector().className('android.view.View').getChild(new UiSelector().index(1))"}

info: [debug] Waiting up to 0ms for condition
info: [debug] Pushing command to appium work queue: ["find",{"strategy":"-android uiautomator","selector":"new UiSelector().className('android.view.View').getChild(new UiSelector().index(1))","context":"","multiple":true}]
info: [debug] [BOOTSTRAP] [debug] Got data from client: {"cmd":"action","action":"find","params":{"strategy":"-android uiautomator","selector":"new UiSelector().className('android.view.View').getChild(new UiSelector().index(1))","context":"","multiple":true}}
info: [debug] [BOOTSTRAP] [debug] Got command of type ACTION
info: [debug] [BOOTSTRAP] [debug] Got command action: find
info: [debug] [BOOTSTRAP] [debug] Finding new UiSelector().className('android.view.View').getChild(new UiSelector().index(1)) using ANDROID_UIAUTOMATOR with the contextId: multiple: true
info: [debug] [BOOTSTRAP] [debug] Parsing selector: new UiSelector().className('android.view.View').getChild(new UiSelector().index(1))
info: [debug] [BOOTSTRAP] [debug] UiSelector coerce type: java.lang.Class arg: 'android.view.View'
info: [debug] [BOOTSTRAP] [debug] UiSelector coerce type: class java.lang.String arg: 'android.view.View'
info: [debug] Responding to client with error: {"status":9,"value":{"message":"The requested resource could not be found, or a request was received using an HTTP method that is not supported by the mapped resource.","origValue":"Could not parse UiSelector argument: 'android.view.View' is not a string"},"sessionId":"f34d3f04-3964-4d6c-a0bf-777e4ae40a6e"}
info: <-- POST /wd/hub/session/f34d3f04-3964-4d6c-a0bf-777e4ae40a6e/elements 500 11.186 ms - 308
info: [debug] [BOOTSTRAP] [debug] Returning result: {"value":"Could not parse UiSelector argument: 'android.view.View' is not a string","status":9}
info: --> DELETE /wd/hub/session/f34d3f04-3964-4d6c-a0bf-777e4ae40a6e {}
info: Shutting down appium session

您好,我想请问一下做这个uiautomatorviewer二次开发的时候怎么进行调试呢,运行后,点击Device Screenshot提示不能与adb进行通讯,只能把打包好的jar放到lib下去运行。另外我给您发了一封邮件,还望不吝赐教。

谢谢分享!!!!

真是希望谷歌用 标准的java swing来开发 android sdk的相关UI, Eclipse的rcp真是不熟悉.

#59楼 @adfghzhang https://plus.google.com/108487870030743970488/posts/2TrMqs1ZGQv 这个链接网上说可以解决问题,但是我修改失败了 所以我直接修改 DebugBridge 中

f (toolsDir == null) {
toolsDir ="D:\\AndroidSDK\\android-sdk-windows\\android-sdk-windows\\tools";
// return null;;
}

就可以了

#62楼 @zsx10110 我自己解决了,多谢

#63楼 @adfghzhang 其实我想知道你是怎么解决的。。

#64楼 @zsx10110 就是用的替换DebugBridge


这个是直接解压出来的, 还是有加密的,后来用工具反编译出来的

#66楼 @darker50 其实不用反编译,google搜下直接就可以下到源代码的。

感谢分享,昨天被触发getXpath()代码的那一部分难住了,今天看代码跑了几遍,改了一下位置,成功完成,但是不知是作者故意留的坑还是什么原因,所以,暂时不贴出我的修改方法,不好意思。

另外,二次修改的这个想法太酷了!

努力向大神靠拢。

@cpfeng0124 请问各位大神,uiautomatorviewer怎么没有刷新功能呢,每次换一个界面都要重新打开一次吗

我反编译了这个包,在重新打包的时候有几个保报错,修改之后打包正常,但是有如下几个问题,请赐教!1.多个手机连接不管选哪个序列号都是默认第一个2.图片显示界面没有自动缩放,鼠标点在上面也没有方框显示。

—— 来自TesterHome官方 安卓客户端

弱弱的问一下,我怎么再打成jar包就不能运行了

#72楼 @everflier 请问反编译以后修好了那两个文件怎么重新打包

为什么我修改以后有错误

76Floor has been deleted

hi 你好,请问一下,这里的文本输入、校验元素,是基于XML文件的么,还是和手机

#48楼 @xxfcxx 请问你这个控件弄好了吗

@cpfeng0124 private String getNodeClassAttribute() {
// TODO Auto-generated method stub
return mAttributes.get("className");
}这个方法get里传的参数试过className和class,打印出来的xpath都是这样的://null[@content-desc='null']这种,感觉是class获取不到,并且在界面中没有新增xpath属性这一栏,全程无报错

提供一个思路参考下:使用目标元素的index和父节点直到根节点的index组合索引生成的xpath唯一定位一个元素

已经尝试,只能获取简单的xpath,如果是稍微复杂点的xpah完全不准。。。

已经尝试,我是修改源码后,生成class文件,替换uiautomatorviewer.jar中的那两个class文件实现的。刚接触自动化测试,这帖子给我很大启发,以后可以通过修改源码来实现自己想要的功能。谢谢楼主。

#58楼 @yuwuhen333 你好,请问这个问题你最后是如何解决的,元素如何定位?非常感谢~~

#59楼 @adfghzhang 您好,我也碰到连接不到adb设备的情况,你最后是怎么解决的,没有明白打个jar放在lib下去运行,我打成jar运行时报找不到主类方法,求赐教,谢谢

#84楼 @binger8296 直接修改DebugBridge.java中的部分代码即可。

//        String toolsDir = System.getProperty("com.android.uiautomator.bindir"); //$NON-NLS-1$
// if (toolsDir == null) {
// return null;
// }

// File sdk = new File(toolsDir).getParentFile();
String toolsDir = System.getenv("ANDROID_HOME");
if (toolsDir == null) {
toolsDir = new FileOp().GetConfigString("Android SDK Path");
if (toolsDir == null) {
return null;
}
}
File sdk = new File(toolsDir);
// check if adb is present in platform-tools
File platformTools = new File(sdk, "platform-tools");
File adb = new File(platformTools, SdkConstants.FN_ADB);
if (adb.exists()) {
return adb.getAbsolutePath();
}

#85楼 @adfghzhang 谢谢,还有个问题FileOp是自己写的类吗?我这个地方报错

#86楼 @binger8296 这是自己写的,可以把它注释掉,我是做了两手准备。保证没配环境变量也可以通过配置文件识别到路径

雪怪 [Topic was deleted] 中提及了此贴 19 Aug 16:31
雪怪 uiautomatorviewer 功能扩展实践 中提及了此贴 06 Dec 22:35

#10楼 @cpfeng0124 编译依赖的其他库怎么处理,根据.mk文件提到的信息,手动添加了这四个库,但是eclipse工程一直报错!

bauul 基于 uiautomator 与 shell 的自动化测试工具 中提及了此贴 02 May 10:12

楼主方便分享下源码吗,或者git的地址,感谢。

需要 Sign In 后方可回复, 如果你还没有账号请点击这里 Sign Up