MonkeyRunner介绍
monkeyrunner工具提供一系列的API用于操控Android手机和模拟器,如向手机或模拟器发送模拟按键、截取用户界面的图片并保存下来。它主要可应用于多设备操控、功能测试,回归测试,可扩展的自动化测试,并且可以定义专用monkeyrunner API,灵活性较强。
monkeyrunner工具使用Jython语言。Jython允许monkeyrunner API与Android框架轻松地进行交互,它可以使用Python语法来获取API中的常量、类及方法。
MonkeyRunner源码分析
MonkeyRunner流程图:
MonkeyRunner的完整生命周期可以分为:初始化阶段,运行阶段,Log及结果输出阶段
1. 初始化阶段
MonkeyRunner的入口函数在Monkey.java的main()函数中,之后进入run()函数。
/**
* Command-line entry point.
*
* @param args The command-line arguments
*/
public static void main(String[] args) {
// Set the process name showing in "ps" or "top"
Process.setArgV0("com.android.commands.monkey");
Logger.err.println("args: " + Arrays.toString(args));
int resultCode = (new Monkey()).run(args);
System.exit(resultCode);
}
在run()中,主要做了以下几个工作:
- 在processOptions()中对命令行进行解析
- 在loadPackageLists()中加载白名单和黑名单包
- 在getSystemInterfaces()中获取系统级服务的引用
- 在getMainApps()中获取apk的入口activity
- 由命令行参数构建事件源
- 在runMonkeyCycles()中执行事件源
- 生成测试日志
前五项可以看做是在做初始化工作,下面对它们进行详细分析。
1.1 命令行解析
monkey支持的命令有:
usage.append("usage: monkey [-p ALLOWED_PACKAGE [-p ALLOWED_PACKAGE] ...]\n");
usage.append(" [-c MAIN_CATEGORY [-c MAIN_CATEGORY] ...]\n");
usage.append(" [--ignore-crashes] [--ignore-timeouts]\n");
usage.append(" [--ignore-security-exceptions]\n");
usage.append(" [--monitor-native-crashes] [--ignore-native-crashes]\n");
usage.append(" [--kill-process-after-error] [--hprof]\n");
usage.append(" [--match-description TEXT]\n");
usage.append(" [--pct-touch PERCENT] [--pct-motion PERCENT]\n");
usage.append(" [--pct-trackball PERCENT] [--pct-syskeys PERCENT]\n");
usage.append(" [--pct-nav PERCENT] [--pct-majornav PERCENT]\n");
usage.append(" [--pct-appswitch PERCENT] [--pct-flip PERCENT]\n");
usage.append(" [--pct-anyevent PERCENT] [--pct-pinchzoom PERCENT]\n");
usage.append(" [--pct-permission PERCENT]\n");
usage.append(" [--pkg-blacklist-file PACKAGE_BLACKLIST_FILE]\n");
usage.append(" [--pkg-whitelist-file PACKAGE_WHITELIST_FILE]\n");
usage.append(" [--wait-dbg] [--dbg-no-events]\n");
usage.append(" [--setup scriptfile] [-f scriptfile [-f scriptfile] ...]\n");
usage.append(" [--port port]\n");
usage.append(" [-s SEED] [-v [-v] ...]\n");
usage.append(" [--throttle MILLISEC] [--randomize-throttle]\n");
usage.append(" [--profile-wait MILLISEC]\n");
usage.append(" [--device-sleep-time MILLISEC]\n");
usage.append(" [--randomize-script]\n");
usage.append(" [--script-log]\n");
usage.append(" [--bugreport]\n");
usage.append(" [--periodic-bugreport]\n");
usage.append(" [--permission-target-system]\n");
usage.append(" COUNT\n");
其中,-s用于指定seed值;-port用于指定monkey服务需要监听的端口;-p用于指定被测试包,一个-p指定一个包,如果有多个被测包,需要使用多个-p;-v用于指定打印信息精度;--throttle用于指定相邻两个事件的间隔时间,单位为ms。
1.2 对系统级服务的引用
在getSystemInterfaces()中,获取ActivityManager/WindowManager/PackageManager的引用,并将ActivityManager的引用在MonkeyNetworkMonitor中进行注册。
private boolean getSystemInterfaces() {
mAm = ActivityManager.getService();
... ...
mWm = IWindowManager.Stub.asInterface(ServiceManager.getService("window"));
... ...
mPm = IPackageManager.Stub.asInterface(ServiceManager.getService("package"));
... ...
try {
mAm.setActivityController(new ActivityController(), true);
mNetworkMonitor.register(mAm);
} catch (RemoteException e) {
... ...
return false;
}
return true;
}
monkeyrunner使用这些系统引用实现将事件注入android系统。
1.3 构建事件源
根据脚本文件及监测端口,事件源分为三类:
- 仅有一个脚本文件的情况,事件源mEventSource为MonkeySourceScript的实例,它使用脚本来生成事件流;当有N(N>1)个脚本文件的情况下,事件源mEventSource为MonkeySourceRandomScript的实例,它有一个ArrayList数组,其中保存了N个MonkeySourceScript实例
- 通过--port设置了监听端口的话,事件源mEventSource为MonkeySourceNetwork的实例,它通过该端口来获取事件流
- 在既没有脚本文件,又没有监听端口的情况,事件源mEventSource为MonkeySourceRandom的实例,它将随机生成事件流
private int run(String[] args) {
... ...
if (mScriptFileNames != null && mScriptFileNames.size() == 1) {
mEventSource = new MonkeySourceScript(mRandom, mScriptFileNames.get(0), mThrottle,
mRandomizeThrottle, mProfileWaitTime, mDeviceSleepTime);
mEventSource.setVerbose(mVerbose);
mCountEvents = false;
} else if (mScriptFileNames != null && mScriptFileNames.size() > 1) {
if (mSetupFileName != null) {
mEventSource = new MonkeySourceRandomScript(mSetupFileName,
mScriptFileNames, mThrottle, mRandomizeThrottle, mRandom,
mProfileWaitTime, mDeviceSleepTime, mRandomizeScript);
mCount++;
} else {
mEventSource = new MonkeySourceRandomScript(mScriptFileNames,
mThrottle, mRandomizeThrottle, mRandom,
mProfileWaitTime, mDeviceSleepTime, mRandomizeScript);
}
mEventSource.setVerbose(mVerbose);
mCountEvents = false;
} else if (mServerPort != -1) {
try {
mEventSource = new MonkeySourceNetwork(mServerPort);
} catch (IOException e) {
return -5;
}
mCount = Integer.MAX_VALUE;
} else {
mEventSource = new MonkeySourceRandom(mRandom, mMainApps,
mThrottle, mRandomizeThrottle, mPermissionTargetSystem);
mEventSource.setVerbose(mVerbose);
}
... ...
}
类图为:
2. 运行阶段
monkeyrunner的运行阶段操作在runMonkeyCycles()函数中,调用EventSource的getNextEvent()方法获取MonkeyEvent事件流,再调用MonkeyEvent的子类的injectEvent()方法,使用InputManager将事件流发送到设备或模拟器中,成功的话返回MonkeyEvent.INJECT_SUCCESS,失败的话返回MonkeyEvent.INJECT_FAIL,当捕获到RemoteException时返回MonkeyEvent.INJECT_ERROR_REMOTE_EXCEPTION,当捕获到SecurityException时返回MonkeyEvent.INJECT_ERROR_SECURITY_EXCEPTION。
private int runMonkeyCycles() {
... ...
try {
// TO DO : The count should apply to each of the script file.
while (!systemCrashed && cycleCounter < mCount) {
MonkeyEvent ev = mEventSource.getNextEvent();
if (ev != null) {
int injectCode = ev.injectEvent(mWm, mAm, mVerbose);
... ...
}
... ...
}
}
2.1 获取MonkeyEvent事件流
monkeyrunner使用local host从用户获取事件流:
- 当用户命令为“done”时,停止监听
- 当用户命令为“quit”时,退出monkeyrunner
- 当用户命令以“#”开头时,忽略这个命令
- 在其他命令时,将它转换为MonkeyCommand,并生成相应的MonkeyEvent
public MonkeyEvent getNextEvent() {
if (!started) {
try {
startServer();
} catch (IOException e) {
return null;
}
started = true;
}
... ...
String command = input.readLine();
... ...
if (DONE.equals(command)) {
... ...
stopServer();
... ...
return new MonkeyNoopEvent();
}
if (QUIT.equals(command)) {
... ...
returnOk();
return null;
}
if (command.startsWith("#")) {
continue;
}
translateCommand(command);
... ...
}
MonkeySourceNetwork.java的translateCommand()方法将从用户处得到的事件与COMMAND_MAP进行匹配,进入到特定的XXXCommand内部类的translateCommand()方法中,获得MonkeyXXXEvent事件队列。
private static final Map<String, MonkeyCommand> COMMAND_MAP = new HashMap<String, MonkeyCommand>();
static {
// Add in all the commands we support
COMMAND_MAP.put("flip", new FlipCommand());
COMMAND_MAP.put("touch", new TouchCommand());
COMMAND_MAP.put("trackball", new TrackballCommand());
COMMAND_MAP.put("key", new KeyCommand());
COMMAND_MAP.put("sleep", new SleepCommand());
COMMAND_MAP.put("wake", new WakeCommand());
COMMAND_MAP.put("tap", new TapCommand());
COMMAND_MAP.put("press", new PressCommand());
COMMAND_MAP.put("type", new TypeCommand());
COMMAND_MAP.put("listvar", new MonkeySourceNetworkVars.ListVarCommand());
COMMAND_MAP.put("getvar", new MonkeySourceNetworkVars.GetVarCommand());
COMMAND_MAP.put("listviews", new MonkeySourceNetworkViews.ListViewsCommand());
COMMAND_MAP.put("queryview", new MonkeySourceNetworkViews.QueryViewCommand());
COMMAND_MAP.put("getrootview", new MonkeySourceNetworkViews.GetRootViewCommand());
COMMAND_MAP.put("getviewswithtext",
new MonkeySourceNetworkViews.GetViewsWithTextCommand());
COMMAND_MAP.put("deferreturn", new DeferReturnCommand());
}
可以看出接口MonkeyCommand的实现类有10种:
MonkeyCommand与MonkeyEvent的对应关系如下图:
2.2 发送事件给设备或模拟器
这个过程是在MonkeyEvent的injectEvent()方法中进行的,而它是一个抽象的方法,具体实现在MonkeyEvent的子类中。下面以MonkeyKeyEvent子类为例来进行分析,在KeyCommand的translateCommand()方法中,根据action和keyCode生成MonkeyKeyEvent实例,在injectEvent()方法中,根据这个action和keyCode生成Android源码中的KeyEvent实例,再调用源码InputManager的injectInputEvent()方法将事件发送给设备或模拟器。
@Override
public int injectEvent(IWindowManager iwm, IActivityManager iam, int verbose) {
KeyEvent keyEvent = mKeyEvent;
if (keyEvent == null) {
keyEvent = new KeyEvent(downTime, eventTime, mAction, mKeyCode,
mRepeatCount, mMetaState, mDeviceId, mScanCode,
KeyEvent.FLAG_FROM_SYSTEM, InputDevice.SOURCE_KEYBOARD);
}
if (!InputManager.getInstance().injectInputEvent(keyEvent,
InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT)) {
return MonkeyEvent.INJECT_FAIL;
}
return MonkeyEvent.INJECT_SUCCESS;
}
3. log及结果输出阶段
log输出是根据monkey命令行参数和monkey运行时发生的错误来设置的,如设置了--monitor-native-crashes,则当native发生crash时,在SD卡中记录下bugreport;如在monkey运行时app发生anr时,调用reportAnrTraces()记录下anr日志。
private void reportAnrTraces() {
try {
Thread.sleep(5 * 1000);
} catch (InterruptedException e) {
}
commandLineReport("anr traces", "cat /data/anr/traces.txt");
}
参考文章:《MonkeyRunner源码剖析》 http://blog.csdn.net/column/details/monkeyrunner.html?&page=1