其他测试框架 Android 性能测试之 Monkey

myersguo · 2017年02月03日 · 最后由 zhangzhao_lenovo 回复于 2018年02月05日 · 4170 次阅读
本帖已被设为精华帖!

这里简单说一下 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) 来实现;
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 9 条回复 时间 点赞

好帖 (✪▽✪),赞赞👍

—— 来自 TesterHome 官方 安卓客户端

学习了,原理理解更加透彻

思寒_seveniruby 将本帖设为了精华贴 02月04日 15:08

加精理由: 测试技术好文, 从原理到工具应用都有深度的剖析.

记得更新自己的微信号, 或者打赏二维码. 精华帖社区会先行打赏.

这个文章不错,可以在基础上进行调整了

可以可以 相当不错

moncky 是如何获取 log 的呢,我看了 logcat 拿到的 log,跟 moneky 打印出来的相差好大。看了源码也没有发现

笑笑 回复

你说的获取 log 指的是什么 log? 如果是 monkey 的执行 log ,参数 -v ,然后重定向 log 到文件即可。

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册