一、Monkey测试简介
monkey测试是Android压力测试的一种手段,通过monkey程序随机模拟用户触摸屏幕、滑动Trackball、按键等操作来对设备上的程序进行压力测试,检测程序会发生的各种异常。
我们可以通过执行:adb shell monkey { +命令参数 } 进行Monkey测试。
例如执行一次最简单的monkey测试:
adb shell monkey -p yourPackageName -v optionTime
其中-p参数后跟着想要测试的apk包名,-v参数后跟着要执行的操作次数。
下面我执行的就是网易云阅读随机操作200次的monkey测试。
monkey测试遇到crash会停止,但是通过一些参数的设置可以设置遇到crash继续,也可以设置多个测试包、延时时间等。具体的参数设置可参考Android monkey官网介绍:
https://developer.android.com/studio/test/monkey.html
二、Monkey源码分析
monkey源码下载地址:
https://code.google.com/archive/p/androidmonkey/downloads
其中AndroidMonkey.tgz就是Android monkey的源代码。
1. 当执行adb shell monkey的时候发生了什么
我们通过adb shell进入模拟器或者真机的Android系统,在/system/bin/目录下可以看到monkey执行脚本,也就是执行adb shell monkey的时候执行到的文件。我们看下这个文件:
这个普通的shell文件,关键执行了app_process这条命令。app_process是Android的系统启动进程,用于启动zygote和其他java进程(使用adb的诸多java命令许多都是app_process启动的)。monkey.jar包的位置位于/system/framework/目录下,而进程的入口就是monkey.jar包的com.android.commands.monkey.Monkey.java文件。
2.monkey命令入口
在Monkey.java文件中,我们可以找到monkey命令的入口main方法。
public static void main(String[] args) {
int resultCode = (new Monkey()).run(args);
System.exit(resultCode);
}
这里可以看到入口Monkey.java的run(args)方法。
3.monkey执行流程
下面将monkey执行流程里的关键代码摘出来了,从中可以较为清晰地看出monkey的一般执行流程。
private int run(String[] args) {
//初始化各个参数
processOptions();//处理从命令行接收到的参数,并赋给相应的变量
loadPackageLists();//加载设置的白名单或者黑名单
getSystemInterfaces();//获得系统接口
getMainApps();//获取要执行的activity
if (mScriptFileName != null) {
mEventSource = new MonkeySourceScript(mScriptFileName);//如果有脚本文件输入的话,就new一个MonkeySourceScript对象。
mEventSource.setVerbose(mVerbose);
} else {
mEventSource = new MonkeySourceRandom(mSeed, mMainApps, mThrottle, mThrottleTouch, mThrottleKey);//如果没有脚本文件输入的话,就new一个MonkeySourceRandom对象。也是我们常用的简单操作。
mEventSource.setVerbose(mVerbose);
}
((MonkeySourceRandom) mEventSource).setFactors(i, mFactors[i]);//设置事件的操作比例。
mEventSource.validate();//调整事件比例。
((MonkeySourceRandom) mEventSource).generateActivity();//随机事件下,产生一个随机的Activity。
int crashedAtCycle = runMonkeyCycles();//monkey的核心事件注入逻辑
}
在runMonkeyCycles()方法中,就是不断地执行注入事件操作。在介绍这部分代码之前,有必要先宏观上了解下monkey的类结构。
4.monkey类结构
monkey源代码主要有若干个java文件,除了入口Monkey.java文件及工具文件外,另外还有两组类文件,分别是以MonkeyEventSource为接口类的事件流处理类(MonkeySourceRandom、MonkeySourceScript等类,分别处理随机事件流和monkey脚本事件流,类图如下图一所示),以及以MonkeyEvent为接口类的事件操作类(MonkeyActivityEvent、MonkeyKeyEvent、MonkeyMotionEvent、MonkeyFlipEvent、MonkeyThrottleEvent等类,类图如下图二所示)。因事件操作类较多,只列出来常用的几个。
事件流处理:我们一般默认的monkey命令都是使用random伪事件流(MonkeySourceRandom),因而一般用不到monkey脚本和网络事件流。
4.monkey核心事件注入逻辑
我们继续回到之前Monkey.java入口方法处,runMonkeyCycles()方法里处理了monkey的核心事件注入逻辑,该方法的核心代码整理如下:
while (!systemCrashed && i < mCount) {
if (mRequestAnrBugreport){ //如果页面发生了ANR,上报ANR
getBugreport("anr_" + mReportProcessName + "_");
mRequestAnrBugreport = false;
}
if (mRequestAppCrashBugreport){ //如果页面发生了crash,上报crash
getBugreport("app_crash" + mReportProcessName + "_");
mRequestAppCrashBugreport = false;
}
if (mRequestDumpsysMemInfo) { //如果设置了发生crash后打印memento,则在发生crash后输出。
mRequestDumpsysMemInfo = false;
shouldReportDumpsysMemInfo = true;
}
if (mMonitorNativeCrashes) { //如果在命令行设置开了监控crash开关,会打印crash
if (checkNativeCrashes() && (i > 0)) {
mAbort = mAbort || mKillProcessAfterError;
}
}
MonkeyEvent ev = mEventSource.getNextEvent(); //获得下一个注入事件
int injectCode = ev.injectEvent(mWm, mAm, mVerbose); //注入事件
}
获得下一个注入事件,通常我们选择的事件流方法是随机事件流,因而我们主要看下MonkeySourceRandom中的getNextEvent()关键方法:
public MonkeyEvent getNextEvent() {
//从MonkeyEvent队列mQ里取出MonkeyEvent事件,如果mQ当前是空的话,就生成一个事件。否则就取出队列的第一个事件。
if (mQ.isEmpty()) {
generateEvents();
}
mEventCount++;
MonkeyEvent e = mQ.getFirst();
mQ.removeFirst();
return e;
}
接下来看下如果MonkeyEvent队列mQ为空的时候,如果产生一个事件,也就是generateEvents()方法:
private void generateEvents() {
float cls = mRandom.nextFloat(); //取一个随机数
int lastKey = 0;
//依据随机数的大小,执行生成不同手势的MotionEvent事件(generatePointerEvent实质上是生成MotionEvent事件)。
if (cls < mFactors[FACTOR_TOUCH]) {
generatePointerEvent(mRandom, GESTURE_TAP);
return;
} else if (cls < mFactors[FACTOR_MOTION]) {
generatePointerEvent(mRandom, GESTURE_DRAG);
return;
} else if (cls < mFactors[FACTOR_PINCHZOOM]) {
generatePointerEvent(mRandom, GESTURE_PINCH_OR_ZOOM);
return;
} else if (cls < mFactors[FACTOR_TRACKBALL]) {
generateTrackballEvent(mRandom);
return;
}
// The remaining event categories are injected as key events
for (;;) {
if (cls < mFactors[FACTOR_NAV]) {
lastKey = NAV_KEYS[mRandom.nextInt(NAV_KEYS.length)];
} else if (cls < mFactors[FACTOR_MAJORNAV]) {
lastKey = MAJOR_NAV_KEYS[mRandom.nextInt(MAJOR_NAV_KEYS.length)];
} else if (cls < mFactors[FACTOR_SYSOPS]) {
lastKey = SYS_KEYS[mRandom.nextInt(SYS_KEYS.length)];
} else if (cls < mFactors[FACTOR_APPSWITCH]) {
MonkeyActivityEvent e = new MonkeyActivityEvent(mMainApps.get(
mRandom.nextInt(mMainApps.size())));
mQ.addLast(e);
return;
} else if (cls < mFactors[FACTOR_FLIP]) {
MonkeyFlipEvent e = new MonkeyFlipEvent(mKeyboardOpen);
mKeyboardOpen = !mKeyboardOpen;
mQ.addLast(e);
return;
} else {
lastKey = 1 + mRandom.nextInt(KeyEvent.getMaxKeyCode() - 1);
}
if (lastKey != KeyEvent.KEYCODE_POWER
&& lastKey != KeyEvent.KEYCODE_ENDCALL
&& PHYSICAL_KEY_EXISTS[lastKey]) {
break;
}
}
MonkeyKeyEvent e = new MonkeyKeyEvent(KeyEvent.ACTION_DOWN, lastKey);
mQ.addLast(e);
e = new MonkeyKeyEvent(KeyEvent.ACTION_UP, lastKey);
mQ.addLast(e);
}
我们拿MonkeyMotionEvent(点击操作)为例,看下关键的注入事件方法是如何操作的:
public int injectEvent(IWindowManager iwm, IActivityManager iam, int verbose) {
MotionEvent me = getEvent();
if ((verbose > 0 && !mIntermediateNote) || verbose > 1) {
StringBuilder msg = new StringBuilder(":Sending ");
msg.append(getTypeLabel()).append(" (");
switch (me.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
msg.append("ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
msg.append("ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
msg.append("ACTION_UP");
break;
case MotionEvent.ACTION_CANCEL:
msg.append("ACTION_CANCEL");
break;
case MotionEvent.ACTION_POINTER_DOWN:
msg.append("ACTION_POINTER_DOWN ").append(me.getPointerId(me.getActionIndex()));
break;
case MotionEvent.ACTION_POINTER_UP:
msg.append("ACTION_POINTER_UP ").append(me.getPointerId(me.getActionIndex()));
break;
default:
msg.append(me.getAction());
break;
}
msg.append("):");
int pointerCount = me.getPointerCount();
for (int i = 0; i < pointerCount; i++) {
msg.append(" ").append(me.getPointerId(i));
msg.append(":(").append(me.getX(i)).append(",").append(me.getY(i)).append(")");
}
System.out.println(msg.toString());
}
try {
//
if (!injectMotionEvent(iwm, me)) {
return MonkeyEvent.INJECT_FAIL;
}
} catch (RemoteException ex) {
return MonkeyEvent.INJECT_ERROR_REMOTE_EXCEPTION;
} finally {
me.recycle();
}
return MonkeyEvent.INJECT_SUCCESS;
}
}
可见MonkeyMotionEvent事件注入最后还是利用了Android的MotionEvent和injectMotionEvent方法进行了事件注入。
以上,就是Android monkey源码的基本分析,核心就是通过随机算法选择操作行为和操作位置,从而实现完全的随机压力测试。我们可以通过对monkey做二次改造进而实现更复杂、定制化的功能。需要进一步的探究和实践。