UiautomatorViewer是谷歌提供给Uiautomator脚本开发时查看和dump移动端页面数据的一个工具。这个工具可以对当前连接到PC上的手机屏幕进行一个快照,我们可以轻松的从dump出来的信息当前页面的层级关系和每个控件的属性。利用这些信息,我们可以轻松编写测试脚本。
但是,有时候,这个工具有些地方不尽人意。比如在一些Android版本(9.0)上并不稳定(null root node returned by UiTestAutomationBridge.)。又比如界面没有提供控件的instance序号,没有提供x-path,响应速度较慢等等。这些都是可以通过我们了解其原理后进行二次开发来解决的。
OK,扯得有点多,开始上菜。
1、去官网载个源码下来,然后在eclipse中创建一个工程,把源码拉进去,需要的一些引用包,记得也要导入。
2、打开主界面的类,
package com.android.uiautomator;
import java.io.File;
import org.eclipse.jface.action.ToolBarManager;
import org.eclipse.jface.window.ApplicationWindow;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.ToolBar;
import com.android.uiautomator.actions.EasyScreenshotAction;
import com.android.uiautomator.actions.OpenFilesAction;
import com.android.uiautomator.actions.ScreenshotAction;
public class UiAutomatorViewer extends ApplicationWindow {
private UiAutomatorView mUiAutomatorView;
public UiAutomatorViewer() {
super((Shell)null);
}
protected Control createContents(Composite parent) {//基本布局
Composite c = new Composite(parent, 2048);
GridLayout gridLayout = new GridLayout(1, false);
gridLayout.marginWidth = 0;
gridLayout.marginHeight = 0;
gridLayout.horizontalSpacing = 0;
gridLayout.verticalSpacing = 0;
c.setLayout(gridLayout);
GridData gd = new GridData(768);
c.setLayoutData(gd);
ToolBarManager toolBarManager = new ToolBarManager(8388608);
toolBarManager.add(new OpenFilesAction(this));
toolBarManager.add(new ScreenshotAction(this));
ToolBar tb = toolBarManager.createControl(c);
tb.setLayoutData(new GridData(768));
this.mUiAutomatorView = new UiAutomatorView(c, 2048);
this.mUiAutomatorView.setLayoutData(new GridData(1808));
return parent;
}
public static void main(String[] args) {
DebugBridge.init();
try {
UiAutomatorViewer e = new UiAutomatorViewer();
e.setBlockOnOpen(true);
e.open();
} catch (Exception var5) {
var5.printStackTrace();
} finally {
DebugBridge.terminate();
}
}
protected void configureShell(Shell newShell) {
super.configureShell(newShell);
newShell.setText("UI Automator Viewer By Zekyll");//application title
}
protected Point getInitialSize() {
return new Point(800, 600);
}
public void setModel(final UiAutomatorModel model, final File modelFile, final Image screenshot) {
if(Display.getDefault().getThread() != Thread.currentThread()) {
Display.getDefault().syncExec(new Runnable() {
public void run() {
UiAutomatorViewer.this.mUiAutomatorView.setModel(model, modelFile, screenshot);
}
});
} else {
this.mUiAutomatorView.setModel(model, modelFile, screenshot);
}
}
}
3、接下来,考虑程序是如何将手机与PC连接起来的。看下这个类:DebugBridge中有下面一段话:
public static void init() {
String adbLocation = getAdbLocation();
if(adbLocation != null) {
AndroidDebugBridge.init(false);
sDebugBridge = AndroidDebugBridge.createBridge(adbLocation, false);
}
}
public static void terminate() {
if(sDebugBridge != null) {
sDebugBridge = null;
AndroidDebugBridge.terminate();
}
}
从这里可以看出,程序是使用ddmlib这个包来建立AndroidDebugBridge。
4、打开com.android.uiautomator.actions包下的ScreenshotAction这个类。我们可以看到,点击屏幕快照按钮后触发的事件代码。
public class ScreenshotAction extends Action {
UiAutomatorViewer mViewer;
public ScreenshotAction(UiAutomatorViewer viewer) {
super("&Device Screenshot");
this.mViewer = viewer;
}
public ImageDescriptor getImageDescriptor() {
return ImageHelper.loadImageDescriptorFromResource("images/screenshot.png");//加载按钮图标
}
//点击快照按钮的事件
public void run() {
if(!DebugBridge.isInitialized()) {// 判断adb是否已经连接
MessageDialog.openError(this.mViewer.getShell(), "Error obtaining Device Screenshot", "Unable to connect to adb. Check if adb is installed correctly.");
} else {
final IDevice device = this.pickDevice();
if(device != null) {
ProgressMonitorDialog dialog = new ProgressMonitorDialog(this.mViewer.getShell());
try {
dialog.run(true, false, new IRunnableWithProgress() {
public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException {
UiAutomatorHelper.UiAutomatorResult result = null;
try {
result = UiAutomatorHelper.takeSnapshot(device, monitor);//截屏
} catch (UiAutomatorHelper.UiAutomatorException var4) {
monitor.done();
ScreenshotAction.this.showError(var4.getMessage(), var4);
var4.printStackTrace();
return;
}
ScreenshotAction.this.mViewer.setModel(result.model, result.uiHierarchy, result.screenshot);//树形结构处理
monitor.done();
}
});
} catch (Exception var4) {
this.showError("Unexpected error while obtaining UI hierarchy", var4);
var4.printStackTrace();
}
}
}
}
private void showError(final String msg, final Throwable t) {
this.mViewer.getShell().getDisplay().syncExec(new Runnable() {
public void run() {
Status s = new Status(4, "Screenshot", msg, t);
ErrorDialog.openError(ScreenshotAction.this.mViewer.getShell(), "Error", "Error obtaining UI hierarchy", s);
}
});
}
private IDevice pickDevice() {
List<?> devices = DebugBridge.getDevices();
if(devices.size() == 0) {
MessageDialog.openError(this.mViewer.getShell(), "Error obtaining Device Screenshot", "No Android devices were detected by adb.");
return null;
} else if(devices.size() == 1) {
return (IDevice)devices.get(0);
} else {
ScreenshotAction.DevicePickerDialog dlg = new ScreenshotAction.DevicePickerDialog(this.mViewer.getShell(), devices);
return dlg.open() != 0?null:dlg.getSelectedDevice();
}
}
跟踪到UiAutomatorHelper.takeSnapshot(device, monitor);这个方法,可以看到,这个方法主要做了两件事情,一个是截屏,另外一个就是dump .uix文件(当前界面数据),并且将这两个进行处理,返回一个 UiAutomatorHelper.UiAutomatorResult实例。
public static UiAutomatorResult takeSnapshot(IDevice device, IProgressMonitor monitor) throws UiAutomatorHelper.UiAutomatorException
{
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 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) {
String msg = "Error while creating temporary file to save snapshot: " + e.getMessage();
throw new UiAutomatorException(msg, e);
}
tmpDir.deleteOnExit();
xmlDumpFile.deleteOnExit();
screenshotFile.deleteOnExit();
monitor.subTask("Obtaining UI hierarchy");
try {
getUiHierarchyFile(device, xmlDumpFile, monitor);
} catch (Exception e) {
String msg = "Error while obtaining UI hierarchy XML file: " + e.getMessage();
throw new UiAutomatorException(msg, e);
}
UiAutomatorModel model;
try
{
model = new UiAutomatorModel(xmlDumpFile);
} catch (Exception e) {
String msg = "Error while parsing UI hierarchy XML file: " + e.getMessage();
throw new UiAutomatorException(msg, e);
}
monitor.subTask("Obtaining device screenshot");
RawImage rawImage;
try {
rawImage = device.getScreenshot();
} catch (Exception e) {
String msg = "Error taking device screenshot: " + e.getMessage();
throw new UiAutomatorException(msg, e);
}
BasicTreeNode root = model.getXmlRootNode();
if ((root instanceof RootWindowNode)) {
for (int i = 0; i < ((RootWindowNode)root).getRotation(); i++) {
rawImage = rawImage.getRotated();
}
}
PaletteData palette = new PaletteData(rawImage.getRedMask(), rawImage.getGreenMask(), rawImage.getBlueMask());
ImageData imageData = new ImageData(rawImage.width, rawImage.height, rawImage.bpp, palette, 1, rawImage.data);
ImageLoader loader = new ImageLoader();
loader.data = new ImageData[] { imageData };
loader.save(screenshotFile.getAbsolutePath(), 5);
Image screenshot = new Image(Display.getDefault(), imageData);
return new UiAutomatorResult(xmlDumpFile, model, screenshot);
}
5、通过dump出来的层级结构数据文件,来生成树并显示到程序界面中:UiAutomatorView类
package com.android.uiautomator;
import java.io.File;
import java.util.Iterator;
import org.eclipse.jface.action.ToolBarManager;
import org.eclipse.jface.layout.TableColumnLayout;
import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.CellEditor;
import org.eclipse.jface.viewers.ColumnLabelProvider;
import org.eclipse.jface.viewers.ColumnWeightData;
import org.eclipse.jface.viewers.EditingSupport;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.TableViewerColumn;
import org.eclipse.jface.viewers.TextCellEditor;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.swt.custom.SashForm;
import org.eclipse.swt.custom.StackLayout;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.ImageLoader;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.graphics.Transform;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.FileDialog;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;
import org.eclipse.swt.widgets.Tree;
import com.android.uiautomator.actions.ExpandAllAction;
import com.android.uiautomator.actions.ToggleNafAction;
import com.android.uiautomator.tree.AttributePair;
import com.android.uiautomator.tree.BasicTreeNode;
import com.android.uiautomator.tree.BasicTreeNodeContentProvider;
public class UiAutomatorView extends Composite {
@SuppressWarnings("unused")
private static final int IMG_BORDER = 2;
private Composite mScreenshotComposite;
private StackLayout mStackLayout;
private Composite mSetScreenshotComposite;
private Canvas mScreenshotCanvas;
private TreeViewer mTreeViewer;
private TableViewer mTableViewer;
private float mScale = 1.0F;
private int mDx;
private int mDy;
private UiAutomatorModel mModel;
private File mModelFile;
private Image mScreenshot;
public UiAutomatorView(Composite parent, int style) {
super(parent, 0);
this.setLayout(new FillLayout());
SashForm baseSash = new SashForm(this, 256);
this.mScreenshotComposite = new Composite(baseSash, 2048);
this.mStackLayout = new StackLayout();
this.mScreenshotComposite.setLayout(this.mStackLayout);
this.mScreenshotCanvas = new Canvas(this.mScreenshotComposite, 2048);
this.mStackLayout.topControl = this.mScreenshotCanvas;
this.mScreenshotComposite.layout();
this.mScreenshotCanvas.addMouseListener(new MouseAdapter() {
public void mouseUp(MouseEvent e) {
if(UiAutomatorView.this.mModel != null) {
UiAutomatorView.this.mModel.toggleExploreMode();
UiAutomatorView.this.redrawScreenshot();
}
}
});
this.mScreenshotCanvas.setBackground(this.getShell().getDisplay().getSystemColor(22));
this.mScreenshotCanvas.addPaintListener(new PaintListener() {
public void paintControl(PaintEvent e) {
if(UiAutomatorView.this.mScreenshot != null) {
UiAutomatorView.this.updateScreenshotTransformation();
Transform t = new Transform(e.gc.getDevice());
t.translate((float)UiAutomatorView.this.mDx, (float)UiAutomatorView.this.mDy);
t.scale(UiAutomatorView.this.mScale, UiAutomatorView.this.mScale);
e.gc.setTransform(t);
e.gc.drawImage(UiAutomatorView.this.mScreenshot, 0, 0);
e.gc.setTransform((Transform)null);
if(UiAutomatorView.this.mModel.shouldShowNafNodes()) {
e.gc.setForeground(e.gc.getDevice().getSystemColor(7));
e.gc.setBackground(e.gc.getDevice().getSystemColor(7));
Iterator<?> rect = UiAutomatorView.this.mModel.getNafNodes().iterator();
while(rect.hasNext()) {
Rectangle r = (Rectangle)rect.next();
e.gc.setAlpha(50);
e.gc.fillRectangle(UiAutomatorView.this.mDx + UiAutomatorView.this.getScaledSize(r.x), UiAutomatorView.this.mDy + UiAutomatorView.this.getScaledSize(r.y), UiAutomatorView.this.getScaledSize(r.width), UiAutomatorView.this.getScaledSize(r.height));
e.gc.setAlpha(255);
e.gc.setLineStyle(1);
e.gc.setLineWidth(2);
e.gc.drawRectangle(UiAutomatorView.this.mDx + UiAutomatorView.this.getScaledSize(r.x), UiAutomatorView.this.mDy + UiAutomatorView.this.getScaledSize(r.y), UiAutomatorView.this.getScaledSize(r.width), UiAutomatorView.this.getScaledSize(r.height));
}
}
Rectangle rect1 = UiAutomatorView.this.mModel.getCurrentDrawingRect();
if(rect1 != null) {
e.gc.setForeground(e.gc.getDevice().getSystemColor(3));
if(UiAutomatorView.this.mModel.isExploreMode()) {
e.gc.setLineStyle(2);
e.gc.setLineWidth(1);
} else {
e.gc.setLineStyle(1);
e.gc.setLineWidth(2);
}
e.gc.drawRectangle(UiAutomatorView.this.mDx + UiAutomatorView.this.getScaledSize(rect1.x), UiAutomatorView.this.mDy + UiAutomatorView.this.getScaledSize(rect1.y), UiAutomatorView.this.getScaledSize(rect1.width), UiAutomatorView.this.getScaledSize(rect1.height));
}
}
}
});
this.mScreenshotCanvas.addMouseMoveListener(new MouseMoveListener() {
public void mouseMove(MouseEvent e) {
if(UiAutomatorView.this.mModel != null && UiAutomatorView.this.mModel.isExploreMode()) {
BasicTreeNode node = UiAutomatorView.this.mModel.updateSelectionForCoordinates(UiAutomatorView.this.getInverseScaledSize(e.x - UiAutomatorView.this.mDx), UiAutomatorView.this.getInverseScaledSize(e.y - UiAutomatorView.this.mDy));
if(node != null) {
UiAutomatorView.this.updateTreeSelection(node);
}
}
}
});
this.mSetScreenshotComposite = new Composite(this.mScreenshotComposite, 0);
this.mSetScreenshotComposite.setLayout(new GridLayout());
final Button setScreenshotButton = new Button(this.mSetScreenshotComposite, 8);
setScreenshotButton.setText("Specify Screenshot...");
setScreenshotButton.addSelectionListener(new SelectionAdapter() {
public void widgetSelected(SelectionEvent arg0) {
FileDialog fd = new FileDialog(setScreenshotButton.getShell());
fd.setFilterExtensions(new String[]{"*.png"});
if(UiAutomatorView.this.mModelFile != null) {
fd.setFilterPath(UiAutomatorView.this.mModelFile.getParent());
}
String screenshotPath = fd.open();
if(screenshotPath != null) {
ImageData[] data;
try {
data = (new ImageLoader()).load(screenshotPath);
} catch (Exception var6) {
return;
}
if(data.length >= 1) {
UiAutomatorView.this.mScreenshot = new Image(Display.getDefault(), data[0]);
UiAutomatorView.this.redrawScreenshot();
}
}
}
});
SashForm rightSash = new SashForm(baseSash, 512);
Composite upperRightBase = new Composite(rightSash, 2048);
upperRightBase.setLayout(new GridLayout(1, false));
ToolBarManager toolBarManager = new ToolBarManager(8388608);
toolBarManager.add(new ExpandAllAction(this));
toolBarManager.add(new ToggleNafAction(this));
toolBarManager.createControl(upperRightBase);
this.mTreeViewer = new TreeViewer(upperRightBase, 0);
this.mTreeViewer.setContentProvider(new BasicTreeNodeContentProvider());
this.mTreeViewer.setLabelProvider(new LabelProvider());
this.mTreeViewer.addSelectionChangedListener(new ISelectionChangedListener() {
public void selectionChanged(SelectionChangedEvent event) {
BasicTreeNode selectedNode = null;
if(event.getSelection() instanceof IStructuredSelection) {
IStructuredSelection selection = (IStructuredSelection)event.getSelection();
Object o = selection.getFirstElement();
if(o instanceof BasicTreeNode) {
selectedNode = (BasicTreeNode)o;
}
}
UiAutomatorView.this.mModel.setSelectedNode(selectedNode);
UiAutomatorView.this.redrawScreenshot();
if(selectedNode != null) {
UiAutomatorView.this.loadAttributeTable();
}
}
});
Tree tree = this.mTreeViewer.getTree();
tree.setLayoutData(new GridData(4, 4, true, true, 1, 1));
tree.setFocus();
Composite lowerRightBase = new Composite(rightSash, 2048);
lowerRightBase.setLayout(new FillLayout());
Group grpNodeDetail = new Group(lowerRightBase, 0);
grpNodeDetail.setLayout(new FillLayout(256));
grpNodeDetail.setText("Node Detail");
Composite tableContainer = new Composite(grpNodeDetail, 0);
TableColumnLayout columnLayout = new TableColumnLayout();
tableContainer.setLayout(columnLayout);
this.mTableViewer = new TableViewer(tableContainer, 65536);
Table table = this.mTableViewer.getTable();
table.setLinesVisible(true);
this.mTableViewer.setContentProvider(new ArrayContentProvider());
TableViewerColumn tableViewerColumnKey = new TableViewerColumn(this.mTableViewer, 0);
TableColumn tblclmnKey = tableViewerColumnKey.getColumn();
tableViewerColumnKey.setLabelProvider(new ColumnLabelProvider() {
public String getText(Object element) {
return element instanceof AttributePair?((AttributePair)element).key:super.getText(element);
}
});
columnLayout.setColumnData(tblclmnKey, new ColumnWeightData(1, 20, true));
TableViewerColumn tableViewerColumnValue = new TableViewerColumn(this.mTableViewer, 0);
tableViewerColumnValue.setEditingSupport(new UiAutomatorView.AttributeTableEditingSupport(this.mTableViewer));
TableColumn tblclmnValue = tableViewerColumnValue.getColumn();
columnLayout.setColumnData(tblclmnValue, new ColumnWeightData(2, 20, true));
tableViewerColumnValue.setLabelProvider(new ColumnLabelProvider() {
public String getText(Object element) {
return element instanceof AttributePair?((AttributePair)element).value:super.getText(element);
}
});
baseSash.setWeights(new int[]{5, 3});
}
private int getScaledSize(int size) {
return this.mScale == 1.0F?size:(new Double(Math.floor((double)((float)size * this.mScale)))).intValue();
}
private int getInverseScaledSize(int size) {
return this.mScale == 1.0F?size:(new Double(Math.floor((double)((float)size / this.mScale)))).intValue();
}
private void updateScreenshotTransformation() {
Rectangle canvas = this.mScreenshotCanvas.getBounds();
Rectangle image = this.mScreenshot.getBounds();
float scaleX = (float)(canvas.width - 4 - 1) / (float)image.width;
float scaleY = (float)(canvas.height - 4 - 1) / (float)image.height;
this.mScale = Math.min(scaleX, scaleY);
this.mDx = (canvas.width - this.getScaledSize(image.width) - 4) / 2 + 2;
this.mDy = (canvas.height - this.getScaledSize(image.height) - 4) / 2 + 2;
}
public void redrawScreenshot() {
if(this.mScreenshot == null) {
this.mStackLayout.topControl = this.mSetScreenshotComposite;
} else {
this.mStackLayout.topControl = this.mScreenshotCanvas;
}
this.mScreenshotComposite.layout();
this.mScreenshotCanvas.redraw();
}
public void setInputHierarchy(Object input) {
this.mTreeViewer.setInput(input);
}
public void loadAttributeTable() {
this.mTableViewer.setInput(this.mModel.getSelectedNode().getAttributesArray());
}
public void expandAll() {
this.mTreeViewer.expandAll();
}
public void updateTreeSelection(BasicTreeNode node) {
this.mTreeViewer.setSelection(new StructuredSelection(node), true);
}
public void setModel(UiAutomatorModel model, File modelBackingFile, Image screenshot) {
this.mModel = model;
this.mModelFile = modelBackingFile;
if(this.mScreenshot != null) {
this.mScreenshot.dispose();
}
this.mScreenshot = screenshot;
this.redrawScreenshot();
BasicTreeNode wrapper = new BasicTreeNode();
wrapper.addChild(this.mModel.getXmlRootNode());
this.setInputHierarchy(wrapper);
this.mTreeViewer.getTree().setFocus();
}
public boolean shouldShowNafNodes() {
return this.mModel != null?this.mModel.shouldShowNafNodes():false;
}
public void toggleShowNaf() {
if(this.mModel != null) {
this.mModel.toggleShowNaf();
}
}
private class AttributeTableEditingSupport extends EditingSupport {
private TableViewer mViewer;
public AttributeTableEditingSupport(TableViewer viewer) {
super(viewer);
this.mViewer = viewer;
}
protected boolean canEdit(Object arg0) {
return true;
}
protected CellEditor getCellEditor(Object arg0) {
return new TextCellEditor(this.mViewer.getTable());
}
protected Object getValue(Object o) {
return ((AttributePair)o).value;
}
protected void setValue(Object arg0, Object arg1) {
}
}
}