Android Monkey测试及源码分析

一、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做二次改造进而实现更复杂、定制化的功能。需要进一步的探究和实践。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 197,814评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,124评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 144,814评论 0 327
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,924评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,815评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,562评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,944评论 3 388
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,582评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,859评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,881评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,700评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,493评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,943评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,115评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,413评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,978评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,182评论 2 339

推荐阅读更多精彩内容