Uiautomator相关
一 uiautomatorviewer工具和Automator UI测试框架
UiAutomator是Google提供的用来做安卓自动化测试的一个Java库,基于Accessibility服务。功能很强,可以对第三方App进行测试,获取屏幕上任意一个APP的任意一个控件属性,并对其进行任意操作,但有两个缺点:1. 测试脚本只能使用Java语言 2. 测试脚本要打包成jar或者apk包上传到设备上才能运行。
uiautomatorviewer工具
- 工具目录:
Android\Sdk\tools\bin\uiautomatorviewer.bat
- 操作效果:
点击箭头按钮可以刷新界面
Automator框架使用
1>添加依赖:
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
// testImplementation 'junit:junit:4.12'
// androidTestImplementation 'com.android.support.test.uiautomator:uiautomator-v18:2.1.2'
// androidTestImplementation 'com.android.support.test:runner:1.0.2'
// androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
implementation 'junit:junit:4.12'
implementation 'com.android.support.test.uiautomator:uiautomator-v18:2.1.2'
implementation 'com.android.support.test:runner:1.0.2'
implementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}
注释中引用只能在debug环境使用
2>uiautomator中常用的API
- 设备对象:UiDevice
- 获取设备对象UiDevice:
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
UiDevice uiDevice = UiDevice.getInstance(instrumentation);
- UiDevice支持的操作:
以press开头的按压相关功能
public boolean pressMenu()//Simulates a short press on the MENU button.
public boolean pressBack()//Simulates a short press on the BACK button.
public boolean pressHome()//Simulates a short press on the HOME button.
public boolean pressSearch()//Simulates a short press on the SEARCH button.
- 点击某个点
public boolean click(int x, int y)
- 滑动和拖拽
public boolean swipe(int startX, int startY, int endX, int endY, int steps)
public boolean drag(int startX, int startY, int endX, int endY, int steps)
- 等待空闲
public void waitForIdle(long timeout)
- 唤起和休眠相关
public void wakeUp()
public void sleep()
public boolean isScreenOn()
- 查找UIObject
public UiObject findObject(UiSelector selector)
public boolean hasObject(BySelector selector)
public UiObject2 findObject(BySelector selector)
public List<UiObject2> findObjects(BySelector selector)
- 查找条件对象 UiSelector,BySelector
- 属性-文本匹配
返回值 | API | 说明 |
---|---|---|
UiSelector/BySelector | text(String text)/By.text(String substring) | 文本 |
UiSelector/BySelector | textContains(String text)/By.textContains(String substring) | 文本包含 |
UiSelector/BySelector | textMatches(String regex)/By.text(Pattern textValue) | 文本正则 |
UiSelector/BySelector | textStartsWith(String text)/By.textStartsWith(String substring) | 文本起始匹配 |
UiSelector/BySelector | 其他 | 其他文本起匹配 |
- 属性-资源ID匹配
返回值 | API | 说明 |
---|---|---|
UiSelector/BySelector | resourceId(String id)/res(String resourceName) | 资源ID |
UiSelector/BySelector | resourceIdMatches(String regex)/res(Pattern resourceName) | 资源ID正则 |
- 属性-类名
返回值 | API | 说明 |
---|---|---|
UiSelector/BySelector | className(String className)/clazz(String className) | 类名 |
UiSelector/BySelector | classNameMatches(String regex)/clazz(Pattern className) | 正则类名 |
- 其他匹配规则
- UI控件对象 UiObject,UiObject2
- 判断控件是否存在
boolean waitForExists(long timeout)
boolean exists()
- 获取属性
String getText()
Rect getVisibleBounds()
- 操作事件
boolean click()
boolean clickAndWaitForNewWindow()
boolean dragTo(int destX, int destY, int steps)
boolean longClick()
3>代码示例
在androidTest中ExampleInstrumentedTest类中实现
/**
* 步骤:输入 0.01金额, 人脸支付,等待刷脸失败,关闭按钮
* 目的:测试人脸支付中录屏功能是否稳定,占内存没有释放,卡顿等
*/
@Test
public void facePayAuto() throws Exception {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
LogUtil.startLogger(LogUtil.VERBOSE, true, InstrumentationRegistry.getContext());
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
UiDevice uiDevice = UiDevice.getInstance(instrumentation);
while (true) {
long start = System.currentTimeMillis();
LogUtil.d(TAG, "facePayAuto:start-->" + simpleDateFormat.format(start));
//寻找收款界面的0
UiObject input_0 = uiDevice.findObject(new UiSelector().text("0"));
UiObject input_dot = uiDevice.findObject(new UiSelector().text("•"));
UiObject input_1 = uiDevice.findObject(new UiSelector().text("1"));
if (input_0.waitForExists(2 * 1000)) {
input_0.click();
LogUtil.d(TAG, "input_0.click()");
}
if (input_dot.waitForExists(2 * 1000)) {
input_dot.click();
LogUtil.d(TAG, "input_dot.click()");
}
if (input_0.waitForExists(2 * 1000)) {
input_0.click();
LogUtil.d(TAG, "input_0.click()");
}
if (input_1.waitForExists(2 * 1000)) {
input_1.click();
LogUtil.d(TAG, "input_1.click()");
}
UiObject facePayBtn = uiDevice.findObject(new UiSelector().text("刷脸收款"));
if (facePayBtn.waitForExists(2 * 1000)) {
facePayBtn.click();
LogUtil.d(TAG, "facePayBtn.click()");
}
UiObject close = uiDevice.findObject(new UiSelector().text("关闭"));
if (close.waitForExists(50 * 1000)) {
close.click();
LogUtil.d(TAG, "close.click()");
}
long end = System.currentTimeMillis();
LogUtil.d(TAG, "facePayAuto:end-->" + simpleDateFormat.format(end));
LogUtil.d(TAG, "use:-->" + getGapTime(end - start));
}
}
二 AccessibilityService 服务
AccessibilityService设计初衷在于帮助残障用户使用android设备和应用,在后台运行,可以监听用户界面的一些状态转换,例如页面切换、焦点改变、通知、Toast等,并在触发AccessibilityEvents时由系统接收回调。后来被开发者另辟蹊径,用于一些插件开发,比如微信红包助手,应用静默安装
- 首先先写一个类去继承AccessibilityService
public class MyAccessibilityService extends AccessibilityService {
@Override
protected void onServiceConnected() {
super.onServiceConnected();
//服务断开连接
}
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
//获取触发事件的控件节点
AccessibilityNodeInfo sourceNodeInfo=event.getSource();
//获取事件类型
int eventType=event.getEventType();
}
private ActivityInfo tryGetActivity(ComponentName componentName) {
try {
return getPackageManager().getActivityInfo(componentName, 0);
} catch (PackageManager.NameNotFoundException e) {
return null;
}
}
@Override
public void onDestroy() {
super.onDestroy();
}
@Override
public void onInterrupt() {
}
}
- 服务配置
<service
android:name=".MyAccessibilityService"
android:label="@string/accessibility_service_name"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/config" />
</service>
- AccessibilityService相关配置
//config.xml
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:accessibilityEventTypes="typeAllMask"
android:accessibilityFeedbackType="feedbackGeneric"
android:accessibilityFlags="flagIncludeNotImportantViews"
android:canRetrieveWindowContent="true"
android:description="@string/accessibility_service_description"
tools:ignore="UnusedAttribute" />
- AccessibilityEvent事件分发流程
参考:Android 9.0源码学习-AccessibilityManager
三uiautomator dump 命令和底层实现
- 命令执行效果:
C:\Users\**>adb shell
T2:/ $ uiautomator dump
UI hierchary dumped to: /sdcard/window_dump.xml
T2:/ $
得到整个ui节点树打印
window_dump.xml
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<hierarchy rotation="0">
<node bounds="[0,0][1920,1011]" checkable="false" checked="false" class="android.widget.FrameLayout"
clickable="false" content-desc="" enabled="true" focusable="false"
focused="false" index="0" long-clickable="false" package="com.woyou.launcher" password="false"
resource-id="" scrollable="false" selected="false" text="">
<node bounds="[0,0][1920,1011]" checkable="false" checked="false" class="android.widget.FrameLayout"
clickable="false" content-desc="" enabled="true" focusable="false"
focused="false" index="0" long-clickable="false" package="com.woyou.launcher" password="false"
resource-id="" scrollable="false" selected="false" text="">
<node bounds="[0,0][1920,1011]" checkable="false" checked="false"
class="android.widget.FrameLayout" clickable="false" content-desc=""
enabled="true" focusable="false" focused="false" index="0" long-clickable="false"
package="com.woyou.launcher" password="false" resource-id="android:id/content" scrollable="false"
selected="false" text="">
……
</node>
</node>
</node>
</hierarchy>
window_dump.xml中顶层节点为hierarchy,控件对应一个个node.组成一个树
node中属性:
bounds="[0,0][540,888]" 控件边界
checkable="false"
checked="false"
class="android.widget.FrameLayout" 控件对应的view的完整类名
clickable="false"
content-desc=""
enabled="true"
focusable="false"
focused="false"
index="0"
long-clickable="false"
package="com.sunmi.payment.mobile" 控件所在的包名
password="false"
resource-id="com.sunmi.payment.mobile:id/input_money_view" 控件id
scrollable="false"
selected="false"
text="" 控件的显示文本
- 底层代码实现
- /frameworks/base/cmds/uiautomator/cmds/uiautomator/uiautomator
exec app_process ${base}/bin com.android.commands.uiautomator.Launcher ${args}
- /frameworks/base/cmds/uiautomator/cmds/uiautomator/src/com/android/commands/uiautomator/Launcher.java
129 private static Command[] COMMANDS = new Command[] {
130 HELP_COMMAND,
131 new RunTestCommand(),
132 new DumpCommand(),
133 new EventsCommand(),
134 };
- /frameworks/base/cmds/uiautomator/cmds/uiautomator/src/com/android/commands/uiautomator/DumpCommand.java
/**
34 * Implementation of the dump subcommand
35 *
36 * This creates an XML dump of current UI hierarchy
37 */
38public class DumpCommand extends Command {
39
40 private static final File DEFAULT_DUMP_FILE = new File(
41 Environment.getLegacyExternalStorageDirectory(), "window_dump.xml");
42
43 public DumpCommand() {
44 super("dump");
45 }
46
47 @Override
48 public String shortHelp() {
49 return "creates an XML dump of current UI hierarchy";
50 }
51
52 @Override
53 public String detailedOptions() {
54 return " dump [--verbose][file]\n"
55 + " [--compressed]: dumps compressed layout information.\n"
56 + " [file]: the location where the dumped XML should be stored, default is\n "
57 + DEFAULT_DUMP_FILE.getAbsolutePath() + "\n";
58 }
59
60 @Override
61 public void run(String[] args) {
62 File dumpFile = DEFAULT_DUMP_FILE;
63 boolean verboseMode = true;
64
65 for (String arg : args) {
66 if (arg.equals("--compressed"))
67 verboseMode = false;
68 else if (!arg.startsWith("-")) {
69 dumpFile = new File(arg);
70 }
71 }
72
73 UiAutomationShellWrapper automationWrapper = new UiAutomationShellWrapper();
74 automationWrapper.connect();
75 if (verboseMode) {
76 // default
77 automationWrapper.setCompressedLayoutHierarchy(false);
78 } else {
79 automationWrapper.setCompressedLayoutHierarchy(true);
80 }
81
82 // It appears that the bridge needs time to be ready. Making calls to the
83 // bridge immediately after connecting seems to cause exceptions. So let's also
84 // do a wait for idle in case the app is busy.
85 try {
86 UiAutomation uiAutomation = automationWrapper.getUiAutomation();
87 uiAutomation.waitForIdle(1000, 1000 * 10);
88 AccessibilityNodeInfo info = uiAutomation.getRootInActiveWindow();
89 if (info == null) {
90 System.err.println("ERROR: null root node returned by UiTestAutomationBridge.");
91 return;
92 }
93
94 Display display =
95 DisplayManagerGlobal.getInstance().getRealDisplay(Display.DEFAULT_DISPLAY);
96 int rotation = display.getRotation();
97 Point size = new Point();
98 display.getSize(size);
99 AccessibilityNodeInfoDumper.dumpWindowToFile(info, dumpFile, rotation, size.x, size.y);
100 } catch (TimeoutException re) {
101 System.err.println("ERROR: could not get idle state.");
102 return;
103 } finally {
104 automationWrapper.disconnect();
105 }
106 System.out.println(
107 String.format("UI hierchary dumped to: %s", dumpFile.getAbsolutePath()));
108 }
109}
- /frameworks/base/cmds/uiautomator/library/testrunner-src/com/android/uiautomator/core/UiAutomationShellWrapper.java
4/**
15 * @hide
16 */
17public class UiAutomationShellWrapper {
18
19 private static final String HANDLER_THREAD_NAME = "UiAutomatorHandlerThread";
20
21 private final HandlerThread mHandlerThread = new HandlerThread(HANDLER_THREAD_NAME);
22
23 private UiAutomation mUiAutomation;
24
25 public void connect() {
26 if (mHandlerThread.isAlive()) {
27 throw new IllegalStateException("Already connected!");
28 }
29 mHandlerThread.start();
30 mUiAutomation = new UiAutomation(mHandlerThread.getLooper(),
31 new UiAutomationConnection());
32 mUiAutomation.connect();
33 }
34
35 /**
36 * Enable or disable monkey test mode.
37 *
38 * Setting test as "monkey" indicates to some applications that a test framework is
39 * running as a "monkey" type. Such applications may choose not to perform actions that
40 * do submits so to avoid allowing monkey tests from doing harm or performing annoying
41 * actions such as dialing 911 or posting messages to public forums, etc.
42 *
43 * @param isSet True to set as monkey test. False to set as regular functional test (default).
44 * @see ActivityManager#isUserAMonkey()
45 */
46 public void setRunAsMonkey(boolean isSet) {
47 IActivityManager am = ActivityManagerNative.getDefault();
48 if (am == null) {
49 throw new RuntimeException("Can't manage monkey status; is the system running?");
50 }
51 try {
52 if (isSet) {
53 am.setActivityController(new DummyActivityController(), true);
54 } else {
55 am.setActivityController(null, true);
56 }
57 } catch (RemoteException e) {
58 throw new RuntimeException(e);
59 }
60 }
61
62 public void disconnect() {
63 if (!mHandlerThread.isAlive()) {
64 throw new IllegalStateException("Already disconnected!");
65 }
66 mUiAutomation.disconnect();
67 mHandlerThread.quit();
68 }
69
70 public UiAutomation getUiAutomation() {
71 return mUiAutomation;
72 }
73
74 public void setCompressedLayoutHierarchy(boolean compressed) {
75 AccessibilityServiceInfo info = mUiAutomation.getServiceInfo();
76 if (compressed)
77 info.flags &= ~AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
78 else
79 info.flags |= AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
80 mUiAutomation.setServiceInfo(info);
81 }
82
83 /**
84 * Dummy, no interference, activity controller.
85 */
86 private class DummyActivityController extends IActivityController.Stub {
87 @Override
88 public boolean activityStarting(Intent intent, String pkg) throws RemoteException {
89 /* do nothing and let activity proceed normally */
90 return true;
91 }
92
93 @Override
94 public boolean activityResuming(String pkg) throws RemoteException {
95 /* do nothing and let activity proceed normally */
96 return true;
97 }
98
99 @Override
100 public boolean appCrashed(String processName, int pid, String shortMsg, String longMsg,
101 long timeMillis, String stackTrace) throws RemoteException {
102 /* do nothing and let activity proceed normally */
103 return true;
104 }
105
106 @Override
107 public int appEarlyNotResponding(String processName, int pid, String annotation)
108 throws RemoteException {
109 /* do nothing and let activity proceed normally */
110 return 0;
111 }
112
113 @Override
114 public int appNotResponding(String processName, int pid, String processStats)
115 throws RemoteException {
116 /* do nothing and let activity proceed normally */
117 return 0;
118 }
119
120 @Override
121 public int systemNotResponding(String message)
122 throws RemoteException {
123 /* do nothing and let system proceed normally */
124 return 0;
125 }
126 }
127}
5.反射封装
public class UiAutomationShellWrapper {
private static final String TAG = "UiAutomationWrapper";
private static final String HANDLER_THREAD_NAME = "UiAutomatorHandlerThread";
private final HandlerThread mHandlerThread = new HandlerThread(HANDLER_THREAD_NAME);
private UiAutomation mUiAutomation;
private UiAutomationShellWrapper() {
}
static private UiAutomationShellWrapper INSTANCE = new UiAutomationShellWrapper();
static public UiAutomationShellWrapper getInstance() {
return INSTANCE;
}
public void init() throws Exception {
INSTANCE.connect();
// INSTANCE.setCompressedLayoutHierarchy(true);
mUiAutomation.waitForIdle(1000, 1000 * 10);
}
public void connect() throws Exception {
if (mHandlerThread.isAlive()) {
throw new IllegalStateException("Already connected!");
}
mHandlerThread.start();
// try {
Class<?> IUiAutomationConnection = Class.forName("android.app.IUiAutomationConnection");
Class<?> uiAutomationConnection = Class.forName("android.app.UiAutomationConnection");
Object uiAutomationShellWrapperObject = uiAutomationConnection.newInstance();
Constructor uiAutomationConstructor = UiAutomation.class.getConstructor(Looper.class, IUiAutomationConnection);
uiAutomationConstructor.setAccessible(true);
mUiAutomation = (UiAutomation) uiAutomationConstructor.newInstance(mHandlerThread.getLooper(), uiAutomationShellWrapperObject);
Method connectMethod = UiAutomation.class.getDeclaredMethod("connect");
connectMethod.setAccessible(true);
connectMethod.invoke(mUiAutomation);
// } catch (IllegalAccessException e) {
// e.printStackTrace();
// } catch (InstantiationException e) {
// e.printStackTrace();
// } catch (ClassNotFoundException e) {
// e.printStackTrace();
// } catch (InvocationTargetException e) {
// e.printStackTrace();
// } catch (NoSuchMethodException e) {
// e.printStackTrace();
// }
}
public void disconnect() {
if (!mHandlerThread.isAlive()) {
throw new IllegalStateException("Already disconnected!");
}
try {
Method disconnectMethod = UiAutomation.class.getDeclaredMethod("disconnect");
disconnectMethod.setAccessible(true);
disconnectMethod.invoke(mUiAutomation);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
mHandlerThread.quit();
}
public UiAutomation getUiAutomation() {
return mUiAutomation;
}
public void setCompressedLayoutHierarchy(boolean compressed) {
AccessibilityServiceInfo info = mUiAutomation.getServiceInfo();
if (compressed)
info.flags &= ~AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
else
info.flags |= AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
mUiAutomation.setServiceInfo(info);
}
}
- 使用须知
添加权限
<uses-permission android:name="android.permission.RETRIEVE_WINDOW_CONTENT" />
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
应用需要系统签名(我是使用公司的统一签名)
此文要是对你有帮助,如果方便麻烦点个赞,谢谢!!!