Android 性能测试之 Monkey

本帖已被设为精华帖!,

这里简单说一下monkey的实现原理。

起步

当你执行adb shell monkey的时候,它到底干了什么。

monkey位于/system/bin目录下。内容为:

# Script to start "monkey" on the device, which has a very rudimentary
# shell.
#
base=/system
export CLASSPATH=$base/framework/monkey.jar
trap "" HUP
exec app_process $base/bin com.android.commands.monkey.Monkey $*

首先,这个app_process是什么呢?

app_process是Android的系统启动进程,用于启动zygote和其他java进程:

if (zygote) {
runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
} else if (className) {
runtime.start("com.android.internal.os.RuntimeInit", args, zygote);
}

更详细的内容,需要阅读android源码,这里不做详细扩展。

adb这里是runtime执行com.android.internal.os.RuntimeInit来启动,位置在:

/system/framework/下面。有很多系统的包,其中有一个/system/framework/monkey.jar为monkey的所在包。

com.android.commands.monkey.Monkey

Application that injects random key events and other actions into the system.

下面,我们一步一步讲解一下:

public static void main(String[] args) {
// Set the process name showing in "ps" or "top"
Process.setArgV0("com.android.commands.monkey");

int resultCode = (new Monkey()).run(args);
System.exit(resultCode);
}

看一下run具体方法:

active manager

monkey中注入系统事件是通过使用内部API来实现的(activemanger, windowmanger, packagemanger),其他方式(instrumentation)只能是二等公民。

private int run(String[] args) {
processOptions();//处理参数
loadPackageLists();//加载黑白名单,可测的有效包名
getSystemInterfaces();//获取系统接口,都是系统的隐藏接口。
//mAm = ActivityManagerNative.getDefault();
//这里返回了一个ActivityManagerProxy对象,用来执行mangerservice接口。
//mWm = IWindowManager.Stub.asInterface(ServiceManager.getService("window"));
//上面,获取了系统窗口服务
//mPm = IPackageManager.Stub.asInterface(ServiceManager.getService("package"));
getMainApps();//获取要执行的activity
mEventSource = new MonkeySourceRandom(mRandom, mMainApps,
mThrottle, mRandomizeThrottle, mPermissionTargetSystem);//产生一个随机事件
((MonkeySourceRandom) mEventSource).setFactors(i, mFactors[i]);
mEventSource.validate();//验证事件,并调整比例
mNetworkMonitor.start();//监听网络变化
crashedAtCycle = runMonkeyCycles();//monkey核心逻辑

}

我们看一下monkey的事件列表类:

public abstract class MonkeyEvent {
protected int eventType;
public static final int EVENT_TYPE_KEY = 0;
public static final int EVENT_TYPE_TOUCH = 1;
public static final int EVENT_TYPE_TRACKBALL = 2;
public static final int EVENT_TYPE_ROTATION = 3; // Screen rotation
public static final int EVENT_TYPE_ACTIVITY = 4;
public static final int EVENT_TYPE_FLIP = 5; // Keyboard flip
public static final int EVENT_TYPE_THROTTLE = 6;
public static final int EVENT_TYPE_PERMISSION = 7;
public static final int EVENT_TYPE_NOOP = 8;

public static final int INJECT_SUCCESS = 1;
public static final int INJECT_FAIL = 0;

// error code for remote exception during injection
public static final int INJECT_ERROR_REMOTE_EXCEPTION = -1;
// error code for security exception during injection
public static final int INJECT_ERROR_SECURITY_EXCEPTION = -2;

public MonkeyEvent(int type) {
eventType = type;
}
...

monkey有11种事件,在MonkeyEventSource中有事件的比例设置。

下面,我们来看monekey的核心执行逻辑;

while (!systemCrashed && cycleCounter < mCount) {
//检查是否发生了ANR
if (mRequestAnrBugreport){
getBugreport("anr_" + mReportProcessName + "_");
mRequestAnrBugreport = false;
}
//检查系统watchdog是否报告bug
if (mRequestWatchdogBugreport) {
System.out.println("Print the watchdog report");
getBugreport("anr_watchdog_");
mRequestWatchdogBugreport = false;
}
//检查是否发生了CRASH
if (mRequestAppCrashBugreport){
getBugreport("app_crash" + mReportProcessName + "_");
mRequestAppCrashBugreport = false;
}
//检查bugreport报告生成
if (mRequestPeriodicBugreport){
getBugreport("Bugreport_");
mRequestPeriodicBugreport = false;
}
//报告系统信息,ANR时出发
if (mRequestDumpsysMemInfo) {
mRequestDumpsysMemInfo = false;
shouldReportDumpsysMemInfo = true;
}
//获取下一个随机时间
MonkeyEvent ev = mEventSource.getNextEvent();
//注入事件
int injectCode = ev.injectEvent(mWm, mAm, mVerbose);
}

回到之前的代码逻辑,这个mEventSource有三种来源:

//脚本模式
mEventSource = new MonkeySourceScript(mRandom, mScriptFileNames.get(0), mThrottle,
mRandomizeThrottle, mProfileWaitTime, mDeviceSleepTime);
mEventSource = new MonkeySourceRandomScript(mSetupFileName,
mScriptFileNames, mThrottle, mRandomizeThrottle, mRandom,
mProfileWaitTime, mDeviceSleepTime, mRandomizeScript);
//网络模式,monkeyrunner的使用方式
mEventSource = new MonkeySourceNetwork(mServerPort);
//默认模式,一般都使用随机事件
mEventSource = new MonkeySourceRandom(mRandom, mMainApps,
mThrottle, mRandomizeThrottle, mPermissionTargetSystem);

好,我们这里展开说一下脚本模式怎么使用monkey.先写一个简单的monkey事件脚本文件:

/**
* monkey event queue. It takes a script to produce events sample script format:
*
* <pre>
* type= raw events
* count= 10
* speed= 1.0
* start data &gt;&gt;
* captureDispatchPointer(5109520,5109520,0,230.75429,458.1814,0.20784314,0.06666667,0,0.0,0.0,65539,0)
* captureDispatchKey(5113146,5113146,0,20,0,0,0,0)
* captureDispatchFlip(true)
* ...
* </pre>
*/


#我们以小米商城为例,进入商城,滑动到最下面
type= user
count= 49
speed= 1.0
start data >>
LaunchActivity(com.xiaomi.shop, com.xiaomi.shop.activity.MainTabActivity)
#wait for launch
UserWait(10000)
#drag to down
Drag(542,1326,542,560,15)
#wait for 500 milliseconds
UserWait(500)
#tap second tab
Tap(346,1868)

那这个脚本是怎么解析的呢?(这里不详细展开):

readHeader();//打开文件,读文件头,设置参数,文件头的结尾必须是:STARTING_DATA_LINE 
当然,脚本中也可以不写文件头的。
readLines();
readNextBatch();
processLine();//处理每一行命令,加入事件队列中。命令包括:


```java
// event key word in the capture log
private static final String EVENT_KEYWORD_POINTER = "DispatchPointer";
private static final String EVENT_KEYWORD_TRACKBALL = "DispatchTrackball";
private static final String EVENT_KEYWORD_ROTATION = "RotateScreen";
private static final String EVENT_KEYWORD_KEY = "DispatchKey";
private static final String EVENT_KEYWORD_FLIP = "DispatchFlip";
private static final String EVENT_KEYWORD_KEYPRESS = "DispatchPress";
private static final String EVENT_KEYWORD_ACTIVITY = "LaunchActivity";
private static final String EVENT_KEYWORD_INSTRUMENTATION = "LaunchInstrumentation";
private static final String EVENT_KEYWORD_WAIT = "UserWait";
private static final String EVENT_KEYWORD_LONGPRESS = "LongPress";
private static final String EVENT_KEYWORD_POWERLOG = "PowerLog";
private static final String EVENT_KEYWORD_WRITEPOWERLOG = "WriteLog";
private static final String EVENT_KEYWORD_RUNCMD = "RunCmd";
private static final String EVENT_KEYWORD_TAP = "Tap";//点击,轻触

private static final String EVENT_KEYWORD_PROFILE_WAIT = "ProfileWait";
private static final String EVENT_KEYWORD_DEVICE_WAKEUP = "DeviceWakeUp";
private static final String EVENT_KEYWORD_INPUT_STRING = "DispatchString";
private static final String EVENT_KEYWORD_PRESSANDHOLD = "PressAndHold"; //
private static final String EVENT_KEYWORD_DRAG = "Drag"; //拖动
private static final String EVENT_KEYWORD_PINCH_ZOOM = "PinchZoom";
private static final String EVENT_KEYWORD_START_FRAMERATE_CAPTURE = "StartCaptureFramerate";
private static final String EVENT_KEYWORD_END_FRAMERATE_CAPTURE = "EndCaptureFramerate";
private static final String EVENT_KEYWORD_START_APP_FRAMERATE_CAPTURE =
"StartCaptureAppFramerate";
private static final String EVENT_KEYWORD_END_APP_FRAMERATE_CAPTURE = "EndCaptureAppFramerate";

就是酱紫。执行一下我们的脚本(命令列表):

adb -s 8b52f091 push d:\script.txt /sdcard/data
monkey -f /sdcard/data/script.txt 1

你可以看到,我们滑动到了底部,然后打开了第二个TAB。当然,我们可以直接通过adb shell来执行上面的操作:

adb shell input swipe 542 1326 560 15
adb shell input swipe 542 1326 560 15
adb shell input tap 346 1868

这里使用的是input命令来执行。和monkey一样,input是一个脚本,执行的是/system/framework/input.jar:

$ cat /system/bin/input
# Script to start "input" on the device, which has a very rudimentary
# shell.
#
base=/system
export CLASSPATH=$base/framework/input.jar
exec app_process $base/bin com.android.commands.input.Input "$@"

回到monkey上去,上面说到

int injectCode = ev.injectEvent(mWm, mAm, mVerbose);

这个事件来源三类,我们现在看默认的随机事件(MonkeySourceRandom),它的getnextevent返回多种随机事件,这里以MonkeyMotionEvent为例进行说明

@Override
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 {
//InputManager.getInstance返回input manager的实例
//Injects an input event into the event system on behalf of an application
//注入事件
if (!InputManager.getInstance().injectInputEvent(me,
InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT)) {
return MonkeyEvent.INJECT_FAIL;
}
} finally {
me.recycle();
}
return MonkeyEvent.INJECT_SUCCESS;
}

其他如MonkeyRotationEvent,使用iwm.freezeRotation(mRotationDegree);来实现旋转屏幕。

总结

  • monkey事件来源三种:默认随机事件、脚本定义事件、network网络事件;
  • monkey事件根据类型比例生成事件队列,循环查找事件;
  • monkey事件的实现使用系统内部API(activemanager,inputmanager,windowmanager)来实现;

* 注:本文来自网络投稿,不代表本站立场,如若侵犯版权,请及时知会删除