关于monkeyrunner源码的一些探索

简介: monkeyrunner源码分析

MonkeyRunner介绍

monkeyrunner工具提供一系列的API用于操控Android手机和模拟器,如向手机或模拟器发送模拟按键、截取用户界面的图片并保存下来它主要可应用于多设备操控、功能测试,回归测试,可扩展的自动化测试,并且可以定义专用monkeyrunner API,灵活性较强。

monkeyrunner工具使用Jython语言。Jython允许monkeyrunner API与Android框架轻松地进行交互,它可以使用Python语法来获取API中的常量、类及方法。

MonkeyRunner源码分析

MonkeyRunner流程图:
a6d04bcb06e33d8cc13399af7768b92e6847fb94
MonkeyRunner的完整生命周期可以分为:初始化阶段,运行阶段,Log及结果输出阶段

1. 初始化阶段

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()中执行事件源
  • 生成测试日志
前五项可以看做是在做初始化工作,下面对它们进行详细分析。

1.1 命令行解析

monkey支持的命令有:

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。

1.2 对系统级服务的引用

在getSystemInterfaces()中,获取ActivityManager/WindowManager/PackageManager的引用,并将ActivityManager的引用在MonkeyNetworkMonitor中进行注册。
    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系统。

1.3 构建事件源

根据脚本文件及监测端口,事件源分为三类:
  • 仅有一个脚本文件的情况,事件源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);
        }
    ... ...
}
 类图为:
40990d573cdc41d68a3ba70d92b8e567892ee073

2. 运行阶段

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);
                    ... ...
                 }
             ... ...
       }
}

2.1 获取MonkeyEvent事件流

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种:
347f973c611f0c52e51224c4ba7a7c5ae2e814fc
MonkeyCommand与MonkeyEvent的对应关系如下图:
7aeeb6ee94a7dff2f7edbb59edf29c0faeafe495

2.2 发送事件给设备或模拟器

这个过程是在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;
    }

3. log及结果输出阶段

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源码剖析》  http://blog.csdn.net/column/details/monkeyrunner.html?&page=1
目录
相关文章
|
安全 Android开发
反编译之利用AndroidStudio动态调试smali源码2
反编译之利用AndroidStudio动态调试smali源码
|
Java Android开发
反编译之利用AndroidStudio动态调试smali源码1
反编译之利用AndroidStudio动态调试smali源码
|
小程序 测试技术 开发工具
Airtest使用详解
Airtest是网易开源的一个跨平台的UI自动化测试框架。 该项目分为AirtestIDE、Airtest、Poco、Testlab四个部分,基于python脚本的方式,用于web、windows程序、app自动化测试。 AirtestIDE:跨平台的UI自动化测试编辑器,内置了Airtest和Poco的相关插件功能,能够使用它快速简单地编写脚本;
450 0
|
IDE 开发工具
Airtest日常使用总结(一)
Airtest日常使用总结(一)
|
Java 测试技术 API
Android测试工具 UIAutomator入门与介绍(上)
  UI Automator 测试工具定义以及用途
Android测试工具 UIAutomator入门与介绍(上)
|
Java Android开发
【Android Gradle】安卓应用构建流程 ( Java 源码编译 和 AIDL 文件编译 )(三)
【Android Gradle】安卓应用构建流程 ( Java 源码编译 和 AIDL 文件编译 )(三)
224 0
【Android Gradle】安卓应用构建流程 ( Java 源码编译 和 AIDL 文件编译 )(三)
|
缓存 Java 开发工具
【Android Gradle】安卓应用构建流程 ( Java 源码编译 和 AIDL 文件编译 )(一)
【Android Gradle】安卓应用构建流程 ( Java 源码编译 和 AIDL 文件编译 )(一)
234 0
【Android Gradle】安卓应用构建流程 ( Java 源码编译 和 AIDL 文件编译 )(一)
|
存储 Java 开发工具
【Android Gradle】安卓应用构建流程 ( Java 源码编译 和 AIDL 文件编译 )(二)
【Android Gradle】安卓应用构建流程 ( Java 源码编译 和 AIDL 文件编译 )(二)
177 0
【Android Gradle】安卓应用构建流程 ( Java 源码编译 和 AIDL 文件编译 )(二)
|
测试技术 API Android开发
MonkeyRunner简介
monkeyrunner工具提供了编写控制Android设备或仿真器从Android的代码之外程序的API。随着monkeyrunner,您可以编写安装一个Android应用程序或测试包,运行它,发送击键它,需要它的用户界面截图,并将截图工作站上的Python程序。
1429 0
|
Android开发 Python
MonkeyRunner 模块
用python编写脚本   1.导入模块: MonkeyRunner MonkeyDevice MonkeyImage ps:如果给导入模块起别名,就应该使用别名,而不能使用原名,否则会出现错误。 from com.
1148 0