android自动化新人一个,目前研究PC端获取android设备的控件信息有哪些方式。多多指教!
android的目录tools下有2个自带的工具hierarchyviewer和uiautomatorviewer,打开后,如下所示:
分别来介绍它们怎么使用的:
点击左上角的手机图样的小图标,出现弹出框,此时正在获取信息:
完成获取后得到了当前的手机界面:
然后你可以点击主面板中的图片,右面的属性面板会显示你所点击的区域的控件属性。(需要注意到是,你的手机设备或模拟器的api要在16以上,也就是android版本得是4.1以上,因为这个工具是google在4.以后推出来的,只适用于4.1以后的版本)。
到这一步,我了解了如何使用这个工具,但是并不是我要研究的东西。我要知道他是如何和手机设备通信,然后获取控件信息的,所以我反编译了uiautomatorviewer.jar,以便从源码了解它的原理。
在tools/lib下找到uiautomatorviewer.jar。反编译后项目结构如下所示:
首先查看UiAutomatorViewer.class,打开后明显看出这是一个java.swt的主界面类,那我们只需要找到工具栏中的按钮的定义。我们就能追踪到点击它是怎么得到设备控件信息的。
找到这个按钮的定义后,我们按ctrl键 点击鼠标跟踪进去,发现该类中有一个进度条对话框,这正是我们先前点击按钮出现的对话框,这里面肯定定义信息获取的方式。
查看上面的代码分析得到,UiAutoMatorHelper的子类UiAutoMatorResult和对象result,它得到了UiAutomatorHelper方法和takeSnapShot返回的结果,那么我就去这个方法一探究竟。进入UiAutomatorHelper中,找到takeSnapshot方法。
public static UiAutomatorResult takeSnapshot(IDevice device, IProgressMonitor monitor) throws com.android.uiautomator.UiAutomatorHelper.UiAutomatorException { UiAutomatorModel model; String msg; RawImage rawImage; if (monitor == null) { monitor = new NullProgressMonitor(); } monitor.subTask("Checking if device support UI Automator"); if (!(supportsUiAutomator(device))) { String msg = "UI Automator requires a device with API Level 16"; throw new com.android.uiautomator.UiAutomatorHelper.UiAutomatorException(msg, null); } monitor.subTask("Creating temporary files for uiautomator results."); File tmpDir = null; File xmlDumpFile = null; File screenshotFile = null; try { tmpDir = File.createTempFile("uiautomatorviewer_", ""); tmpDir.delete(); if (!(tmpDir.mkdirs())) throw new IOException("Failed to mkdir"); xmlDumpFile = File.createTempFile("dump_", ".uix", tmpDir); screenshotFile = File.createTempFile("screenshot_", ".png", tmpDir); } catch (Exception e) { msg = "Error while creating temporary file to save snapshot: " + e.getMessage(); throw new com.android.uiautomator.UiAutomatorHelper.UiAutomatorException(msg, e); } tmpDir.deleteOnExit(); xmlDumpFile.deleteOnExit(); screenshotFile.deleteOnExit(); monitor.subTask("Obtaining UI hierarchy"); try { <span style="color:#ff0000;">getUiHierarchyFile(device, xmlDumpFile, monitor); </span> } catch (Exception e) { msg = "Error while obtaining UI hierarchy XML file: " + e.getMessage(); throw new com.android.uiautomator.UiAutomatorHelper.UiAutomatorException(msg, e); }
刚开始的时候没明白为什么有好多定义文件的代码,然后就没管,往下看的时候发现了一个getUiHierarchyFile方法,既然在这个方法里没找到获取控件信息的方式,那么我就试着去getUiherarchyFile方法中看看(其实我之前研究hierarchyviewer的时候,这个工具也用过这个getHierarchy方法,只不过叫parseHierarchy,它就是获取控件树的,所以我心中一喜,有戏) 。让我们进入getUiHerarchyFile中看看,看能否发现我们真正需要的。
private static void getUiHierarchyFile(IDevice device, File dst, IProgressMonitor monitor) { if (monitor == null) { monitor = new NullProgressMonitor(); } monitor.subTask("Deleting old UI XML snapshot ..."); String command = "rm /data/local/tmp/uidump.xml"; try { commandCompleteLatch = new CountDownLatch(1); device.executeShellCommand(command, new CollectingOutputReceiver(commandCompleteLatch)); commandCompleteLatch.await(5L, TimeUnit.SECONDS); } catch (Exception e1) { } monitor.subTask("Taking UI XML snapshot..."); command = String.format("%s %s %s", new Object[] { "/system/bin/uiautomator", "dump", "/data/local/tmp/uidump.xml" }); CountDownLatch commandCompleteLatch = new CountDownLatch(1); try { device.executeShellCommand(command, new CollectingOutputReceiver(commandCompleteLatch), 40000); commandCompleteLatch.await(40L, TimeUnit.SECONDS); monitor.subTask("Pull UI XML snapshot from device..."); device.getSyncService().pullFile("/data/local/tmp/uidump.xml", dst.getAbsolutePath(), SyncService.getNullProgressMonitor()); } catch (Exception e) { throw new RuntimeException(e); } }
</pre><pre code_snippet_id="250249" snippet_file_name="blog_20140322_3_2829642" class="html" name="code">
我原本以为这里面也应该有个socke啥的,跟手机端通信获取数据的。进来这里面没发现socket,但是仔细一分析,原来UiAutomatorviewer并不是通过socket来获取信息的。它是发送dump命令,让存放在手机设备中/system/bin/uiautomator下的脚本执行,获得一个uidump.xml的文件,然后将这个文件抓到本地。本地读取xml文件就可以了。这才恍然大悟为什么之前takeSnapshot方法中有定义文件的操作,原来它是通过获取xml文件存放在本地临时文件里,太鬼了。
那剩下的就是读取xml文件喽。到这,我的uiautomatorviewer的了解就结束了,还算有点收获。下面接着hierarchyviewer的使用。
说到hierarchyviewer都是眼泪,花了3个礼拜研究,由于自己的死心眼,非要通过它实现自动化,非要用java写。然后一直研究到它可以遍历settings的所有界面;然后才发现被坑了,代码太长了;if/else写了一大堆,又是dumpsys命令获取window信息和activity信息,又是图片比对确定点击跳转的图片,然后满心欢喜的拿给经理看,经理直接给否决了---效率太低;hierarchyviewer获取数据确实慢,但是总算有的基础版本的嘛;其实我发现经理心里算盘打着好着呢,她只是让我们一步一步的了解,她知道哪种方式最适合,就是还不告诉我们,就让我们自己研究,而我呢,刚来又着急展示一下,根基没打稳就像往大的方向走,都开始实现开啦;然后被经理给拽回来, 开始研究哪些实现获取控件的方式,以及优缺点,这才有了上面的uiautomator的研究;哎,不着急,一步一步来吧。
说到hierarchyviewer,研究起来真的是小孩学步啊。经理只丢了一个命题:多语自动化测试,你们研究吧。我ca类,我还傻不垃圾的问了一句:什么是多语。旁边的测试人员给我回答了,多国语言测试。汗!好吧,我低端。。。。。。开始吧。
首先选择是手机端直接测试,还是连上PC端测试?然后发现了monkey,再到monkeyrunner,然后在monkeyrunner里有touch方法点击,然后却不知道一个按钮的坐标怎么确定,在一个犄角旮旯的地方发现有人说通过hierarchyviewer可以获取坐标。然后就开始一个坐标一个坐标的找啊,编写脚本啊。总算实现了一点:唤醒--解锁--点--点--点;然后开讨论会的时候,让经理否决了,说是这么多控件,要一个一个找,得多长时间啊。要做到连上手机,不管哪个画面,它自己获得,然后点击。确实高端大气上档次!but你倒是告诉我从那块搞起啊。哎,在百般纠结于无奈中,柳暗花明啦,hierarchyviewer上的东西不就是人家从客户端获取的么。得到这个讯息后,我找到了一个知平软件写的,然后根据他的研究,一步一步了解了hierarchyviewer,在此感谢这个前辈,放出连接,新手同学可以研究下。
第一篇:http://www.cnblogs.com/vowei/archive/2012/07/30/2614353.html
第二篇:http://www.cnblogs.com/vowei/archive/2012/08/03/2618753.html
第三篇:http://www.cnblogs.com/vowei/archive/2012/08/08/2627614.html
第四篇:http://www.cnblogs.com/vowei/archive/2012/08/22/2650722.html
通过读这四篇文章,我对hierarchyviewer有了一定了解,对于这位前辈没给出的一些疑惑我做了一些深入的研究和总结。
总结:
1.hierarchyviewer是通过socket连接android设备的ViewServer,通过4939端口建立通信。
2.通过adb -s <device> forward tcp:localpott tcp:4939将端口映射到本地端口上
3.通过该本地端口,客户端启动socket连接ViewServer,发送"dump -1"命令获取控件信息。这些信息一行代表一个控件,然后存放在ViewNode中。
以上通过上面四篇文章你都能了解到。下面是自己的疑问:
1.信息是如何从一行一行的字符串转变为viewnode对象的。
2.是怎么深度遍历树的。
带着这些疑问我又暴力了,反编译hierarchyviewer2lib.jar文件,寻找到了DeviceBridge的parseHierarchy方法:
public static ViewNode parseViewHierarchy(BufferedReader in, Window window) { ViewNode currentNode = null; int currentDepth = -1; try { while ((line = in.readLine()) != null) { String line; if ("DONE.".equalsIgnoreCase(line)) break; int depth = 0; while (line.charAt(depth) == ' ') ++depth; while (depth <= currentDepth) { if (currentNode != null) currentNode = currentNode.parent; --currentDepth; } <span style="color:#ff6666;">currentNode = new ViewNode(window, currentNode, line.substring(depth)); </span> currentDepth = depth; } } catch (IOException e) { Log.e("hierarchyviewer", "Error reading view hierarchy stream: " + e.getMessage()); return null; } if (currentNode == null) return null; while (currentNode.parent != null) { currentNode = currentNode.parent; } return currentNode; }
通过标红的代码可知,创建ViewNode对象的时候,传入三个参数:window,currentNode,读取的行数据去掉空格后的line.找到ViewNode类。
package com.android.hierarchyviewerlib.models; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; import org.eclipse.swt.graphics.Image; public class ViewNode { private static final double RED_THRESHOLD = 0.80000000000000004D; private static final double YELLOW_THRESHOLD = 0.5D; public static final String MISCELLANIOUS = "miscellaneous"; public String id; public String name; public String hashCode; public List<Property> properties = new ArrayList(); public Map<String, Property> namedProperties = new HashMap(); public ViewNode parent; public List<ViewNode> children = new ArrayList(); public int left; public int top; public int width; public int height; public int scrollX; public int scrollY; public int paddingLeft; public int paddingRight; public int paddingTop; public int paddingBottom; public int marginLeft; public int marginRight; public int marginTop; public int marginBottom; public int baseline; public boolean willNotDraw; public boolean hasMargins; public boolean hasFocus; public int index; public double measureTime; public double layoutTime; public double drawTime; public ProfileRating measureRating = ProfileRating.NONE; public ProfileRating layoutRating = ProfileRating.NONE; public ProfileRating drawRating = ProfileRating.NONE; public Set<String> categories = new TreeSet(); public Window window; public Image image; public int imageReferences = 1; public int viewCount; public boolean filtered; public int protocolVersion; public ViewNode(Window window, ViewNode parent, String data) { <span style="color:#ff6666;">this.window = window; this.parent = parent; this.index = ((this.parent == null) ? 0 : this.parent.children.size()); if (this.parent != null) this.parent.children.add(this); int delimIndex = data.indexOf(64); if (delimIndex < 0) throw new IllegalArgumentException("Invalid format for ViewNode, missing @: " + data); this.name = data.substring(0, delimIndex); data = data.substring(delimIndex + 1); delimIndex = data.indexOf(32); this.hashCode = data.substring(0, delimIndex); if (data.length() > delimIndex + 1) { loadProperties(data.substring(delimIndex + 1).trim()); } else { this.id = "unknown"; this.width = (this.height = 10); } </span> this.measureTime = -1.0D; this.layoutTime = -1.0D; this.drawTime = -1.0D; } public void dispose() { int N = this.children.size(); for (int i = 0; i < N; ++i) ((ViewNode)this.children.get(i)).dispose(); dereferenceImage(); } public void referenceImage() { this.imageReferences += 1; } public void dereferenceImage() { this.imageReferences -= 1; if ((this.image != null) && (this.imageReferences == 0)) this.image.dispose(); } private void loadProperties(String data) { boolean stop; int start = 0; do { int index = data.indexOf(61, start); Property property = new Property(); property.name = data.substring(start, index); int index2 = data.indexOf(44, index + 1); int length = Integer.parseInt(data.substring(index + 1, index2)); start = index2 + 1 + length; property.value = data.substring(index2 + 1, index2 + 1 + length); this.properties.add(property); this.namedProperties.put(property.name, property); stop = start >= data.length(); if (!(stop)) ++start; } while (!(stop)); Collections.sort(this.properties, new Comparator(this) { public int compare(, ViewNode.Property destination) { return source.name.compareTo(destination.name); } }); this.id = ((Property)this.namedProperties.get("mID")).value; this.left = ((this.namedProperties.containsKey("mLeft")) ? getInt("mLeft", 0) : getInt("layout:mLeft", 0)); this.top = ((this.namedProperties.containsKey("mTop")) ? getInt("mTop", 0) : getInt("layout:mTop", 0)); this.width = ((this.namedProperties.containsKey("getWidth()")) ? getInt("getWidth()", 0) : getInt("layout:getWidth()", 0)); this.height = ((this.namedProperties.containsKey("getHeight()")) ? getInt("getHeight()", 0) : getInt("layout:getHeight()", 0)); this.scrollX = ((this.namedProperties.containsKey("mScrollX")) ? getInt("mScrollX", 0) : getInt("scrolling:mScrollX", 0)); this.scrollY = ((this.namedProperties.containsKey("mScrollY")) ? getInt("mScrollY", 0) : getInt("scrolling:mScrollY", 0)); this.paddingLeft = ((this.namedProperties.containsKey("mPaddingLeft")) ? getInt("mPaddingLeft", 0) : getInt("padding:mPaddingLeft", 0)); this.paddingRight = ((this.namedProperties.containsKey("mPaddingRight")) ? getInt("mPaddingRight", 0) : getInt("padding:mPaddingRight", 0)); this.paddingTop = ((this.namedProperties.containsKey("mPaddingTop")) ? getInt("mPaddingTop", 0) : getInt("padding:mPaddingTop", 0)); this.paddingBottom = ((this.namedProperties.containsKey("mPaddingBottom")) ? getInt("mPaddingBottom", 0) : getInt("padding:mPaddingBottom", 0)); this.marginLeft = ((this.namedProperties.containsKey("layout_leftMargin")) ? getInt("layout_leftMargin", -2147483648) : getInt("layout:layout_leftMargin", -2147483648)); this.marginRight = ((this.namedProperties.containsKey("layout_rightMargin")) ? getInt("layout_rightMargin", -2147483648) : getInt("layout:layout_rightMargin", -2147483648)); this.marginTop = ((this.namedProperties.containsKey("layout_topMargin")) ? getInt("layout_topMargin", -2147483648) : getInt("layout:layout_topMargin", -2147483648)); this.marginBottom = ((this.namedProperties.containsKey("layout_bottomMargin")) ? getInt("layout_bottomMargin", -2147483648) : getInt("layout:layout_bottomMargin", -2147483648)); this.baseline = ((this.namedProperties.containsKey("getBaseline()")) ? getInt("getBaseline()", 0) : getInt("layout:getBaseline()", 0)); this.willNotDraw = ((this.namedProperties.containsKey("willNotDraw()")) ? getBoolean("willNotDraw()", false) : getBoolean("drawing:willNotDraw()", false)); this.hasFocus = ((this.namedProperties.containsKey("hasFocus()")) ? getBoolean("hasFocus()", false) : getBoolean("focus:hasFocus()", false)); this.hasMargins = ((this.marginLeft != -2147483648) && (this.marginRight != -2147483648) && (this.marginTop != -2147483648) && (this.marginBottom != -2147483648)); for (Iterator i$ = this.namedProperties.keySet().iterator(); i$.hasNext(); ) { String name = (String)i$.next(); int index = name.indexOf(58); if (index != -1) this.categories.add(name.substring(0, index)); } if (this.categories.size() != 0) this.categories.add("miscellaneous"); } public void setProfileRatings() { int N = this.children.size(); if (N > 1) { ViewNode child; double totalMeasure = 0D; double totalLayout = 0D; double totalDraw = 0D; for (int i = 0; i < N; ++i) { child = (ViewNode)this.children.get(i); totalMeasure += child.measureTime; totalLayout += child.layoutTime; totalDraw += child.drawTime; } for (i = 0; i < N; ++i) { child = (ViewNode)this.children.get(i); if (child.measureTime / totalMeasure >= 0.80000000000000004D) child.measureRating = ProfileRating.RED; else if (child.measureTime / totalMeasure >= 0.5D) child.measureRating = ProfileRating.YELLOW; else child.measureRating = ProfileRating.GREEN; if (child.layoutTime / totalLayout >= 0.80000000000000004D) child.layoutRating = ProfileRating.RED; else if (child.layoutTime / totalLayout >= 0.5D) child.layoutRating = ProfileRating.YELLOW; else child.layoutRating = ProfileRating.GREEN; if (child.drawTime / totalDraw >= 0.80000000000000004D) child.drawRating = ProfileRating.RED; else if (child.drawTime / totalDraw >= 0.5D) child.drawRating = ProfileRating.YELLOW; else child.drawRating = ProfileRating.GREEN; } } for (int i = 0; i < N; ++i) ((ViewNode)this.children.get(i)).setProfileRatings(); } public void setViewCount() { this.viewCount = 1; int N = this.children.size(); for (int i = 0; i < N; ++i) { ViewNode child = (ViewNode)this.children.get(i); child.setViewCount(); this.viewCount += child.viewCount; } } public void filter(String text) { int dotIndex = this.name.lastIndexOf(46); String shortName = (dotIndex == -1) ? this.name : this.name.substring(dotIndex + 1); this.filtered = ((!(text.equals(""))) && (((shortName.toLowerCase().contains(text.toLowerCase())) || ((!(this.id.equals("NO_ID"))) && (this.id.toLowerCase().contains(text.toLowerCase())))))); int N = this.children.size(); for (int i = 0; i < N; ++i) ((ViewNode)this.children.get(i)).filter(text); } private boolean getBoolean(String name, boolean defaultValue) { Property p = (Property)this.namedProperties.get(name); if (p != null) try { return Boolean.parseBoolean(p.value); } catch (NumberFormatException e) { return defaultValue; } return defaultValue; } private int getInt(String name, int defaultValue) { Property p = (Property)this.namedProperties.get(name); if (p != null) try { return Integer.parseInt(p.value); } catch (NumberFormatException e) { return defaultValue; } return defaultValue; } public String toString() { return this.name + "@" + this.hashCode; } public static class Property { public String name; public String value; public String toString() { return this.name + '=' + this.value; } } public static enum ProfileRating { RED, YELLOW, GREEN, NONE; } }
在构造方法中,我了解到了它把传递过来的ViewNode对象作为当前对象的父对象,又把当前对象作为父节点的子节点。形成一个链表结构,这样我通过最上层的根节点就可以获得所有节点(佩服佩服,后悔自己数据结构和算法没学好啊!)。一行currentNode = new ViewNode(window, currentNode, line.substring(depth));解决了所有烦恼,就是理解的时候要花费一点时间。
通过源码的学习。我了解hierarchyviewer的工作方式,然后我动手用java自己来实现。下一篇再续讲,该吃饭了。
下一篇:
android自动化测试中hierarchyviewer和uiautomatorviewer获取控件信息的方式比对(2)
uiautomatorviewer在5.0中的改进