345
技術社區[雲棲]
關於monkeyrunner源碼的一些探索
monkeyrunner工具提供一係列的API用於操控Android手機和模擬器,如向手機或模擬器發送模擬按鍵、截取用戶界麵的圖片並保存下來它主要可應用於多設備操控、功能測試,回歸測試,可擴展的自動化測試,並且可以定義專用monkeyrunner API,靈活性較強。
monkeyrunner工具使用Jython語言。Jython允許monkeyrunner API與Android框架輕鬆地進行交互,它可以使用Python語法來獲取API中的常量、類及方法。
MonkeyRunner流程圖:

MonkeyRunner的完整生命周期可以分為:初始化階段,運行階段,Log及結果輸出階段
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()中執行事件源
- 生成測試日誌
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。
在getSystemInterfaces()中,獲取ActivityManager/WindowManager/PackageManager的引用,並將ActivityManager的引用在MonkeyNetworkMonitor中進行注冊。
monkeyrunner使用這些係統引用實現將事件注入android係統。
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係統。
根據腳本文件及監測端口,事件源分為三類:
- 僅有一個腳本文件的情況,事件源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);
}
... ...
}
類圖為:

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);
... ...
}
... ...
}
}
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的對應關係如下圖:

這個過程是在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;
}
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源碼剖析》 https://blog.csdn.net/column/details/monkeyrunner.html?&page=1
最後更新:2017-09-07 12:32:34