关于monkeyrunner源码的一些探索

MonkeyRunner介绍

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

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

MonkeyRunner源码分析

MonkeyRunner流程图:

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

 类图为:

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种:

MonkeyCommand与MonkeyEvent的对应关系如下图:

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

时间: 2024-08-31 03:15:11

关于monkeyrunner源码的一些探索的相关文章

关于Junit源码的一些探索

Junit介绍 Junit 是由 Erich Gamma 和 Kent Beck 编写的一个开源的单元测试框架,主要用于白盒测试.Junit 的设计精简,易学易用,但是功能却非常强大. 在Junit3中,使用时只需继承TestCase类,并且 1. 测试方法名以test开头,返回void,方法中没有参数 2. 使用方法setUp()做初始化工作,使用方法tearDown()做清理工作 3. 使用Assert类的方法来判断测试用例结果 Junit源码分析 Junit的流程图: Junit的完整生命

Appium Server源码分析之作为Bootstrap客户端

Appium Server拥有两个主要的功能: 它是个http服务器,它专门接收从客户端通过基于http的REST协议发送过来的命令 他是bootstrap客户端:它接收到客户端的命令后,需要想办法把这些命令发送给目标安卓机器的bootstrap来驱动uiatuomator来做事情 通过上一篇文章<Appium Server 源码分析之启动运行Express http服务器>我们分析了Appium Server是如何作为一个http服务器进行工作的.那么今天我们就要分析第二点,Appium S

android源码探索之定制android关机界面的方法_Android

本文实例讲述了android源码探索之定制android关机界面的方法.分享给大家供大家参考.具体如下: 在Android系统中,长按Power键默认会弹出对话框让你选择"飞行模式","静音","关机"等功能.如下图所示: 但这些功能都对Android-x86和其他终端产品就没什么必要了.本文就简单介绍下如何定制关机界面. 我的目标是长按Power键,将会关机,弹出"设备将要关机"选择对话框.如果可以选择"是&quo

Hadoop2源码分析-RPC探索实战

1.概述 在<Hadoop2源码分析-RPC机制初识>博客中,我们对RPC机制有了初步的认识和了解,下面我们对Hadoop V2的RPC机制做进一步探索,在研究Hadoop V2的RPC机制,我们需要掌握相关的Java基础知识,如:Java NIO.动态代理与反射等.本篇博客介绍的内容目录如下所示: Java NIO简述 Java NIO实例演示 动态代理与反射简述 动态代理与反射实例演示 Hadoop V2 RPC框架使用实例 下面开始今天的博客介绍. 2.Java NIO简述 Java N

android源码探索之定制android关机界面的方法

本文实例讲述了android源码探索之定制android关机界面的方法.分享给大家供大家参考.具体如下: 在Android系统中,长按Power键默认会弹出对话框让你选择"飞行模式","静音","关机"等功能.如下图所示: 但这些功能都对Android-x86和其他终端产品就没什么必要了.本文就简单介绍下如何定制关机界面. 我的目标是长按Power键,将会关机,弹出"设备将要关机"选择对话框.如果可以选择"是&quo

Orchard 源码探索:Module,Theme,Core扩展加载概述

1. host.Initialize(); private static IOrchardHost HostInitialization(HttpApplication application) { var host = OrchardStarter.CreateHost(MvcSingletons); host.Initialize(); // initialize shells to speed up the first dynamic query host.BeginRequest();

Silverlight 5 beta新特性探索系列:6.Silverlight 5新增低延迟声音效果类SoundEffect.支持wav音乐格式【附带源码实例】

在Silverlight 5中新增了一个SoundEffect类和SoundEffectInstance类用以加载wav格式的音乐,这样可以很及时的为动画添加音效. 现在我们看如何使用这两个类来控制播放wav音乐,首先引入mav音乐格式的文件(音频采样大小必须为16或者8位不能是24位,采用PCM编码,22.5, 44.1 or 48khz的采样率)如下图属性: 其次将引入的wav格式音乐文件在项目中右键点击其属性设置其"生成操作"为"内容",如下图所示: 现在我们

Silverlight 5 beta新特性探索系列:3.Silverlight5中的文字增进控制【附带实例源码】

在Silverlight 5中新增了CharacterSpacing属性对文字间距进行控制,增加了RichTextBoxOverflow控件以灵活的对大量文字进行合理的排版显示. 一.CharacterSpacing属性 在Silverlight原来的版本中文字和文字之间并没有一个间距控制属性,这让某一些特殊的文字显示场合排版不易(比如杂志,电子报),在Silverlight 5中引入的CharacterSpacing属性就可以很好处理文字间距,它可用做TextBlock,RichTextBox

竞价网站源码与SEO

导读:受产品利益的驱使,互联网上遍布各种各样的竞价网站.既是竞价网站,所以,较少有人谈到竞价网站的SEO问题.那么,竞价网站可以SEO吗?如果可以,又该如何来处理竞价网站源码才算SEO呢?龄毅网在这儿浅谈一下竞价网站源码与SEO的问题. 龄毅网截至目前为止,已经发布过两三百个竞价网站源码了,后面还将陆续发布更多的竞价网站源码,敬请需要竞价网站源码的朋友注意本站的更新啦! 受产品利益的驱使,互联网上遍布各种各样的竞价网站.既是竞价网站,所以,较少有人谈到竞价网站的SEO问题.那么,竞价网站可以SE