閱讀345 返回首頁    go 技術社區[雲棲]


關於monkeyrunner源碼的一些探索

monkeyrunner工具提供一係列的API用於操控Android手機和模擬器,如向手機或模擬器發送模擬按鍵、截取用戶界麵的圖片並保存下來它主要可應用於多設備操控、功能測試,回歸測試,可擴展的自動化測試,並且可以定義專用monkeyrunner API,靈活性較強。

monkeyrunner工具使用Jython語言。Jython允許monkeyrunner API與Android框架輕鬆地進行交互,它可以使用Python語法來獲取API中的常量、類及方法。

MonkeyRunner流程圖:
a6d04bcb06e33d8cc13399af7768b92e6847fb94
MonkeyRunner的完整生命周期可以分為:初始化階段,運行階段,Log及結果輸出階段

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()中執行事件源
  • 生成測試日誌
前五項可以看做是在做初始化工作,下麵對它們進行詳細分析。


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。

在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係統。

根據腳本文件及監測端口,事件源分為三類:
  • 僅有一個腳本文件的情況,事件源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

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

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

這個過程是在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;
    }

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源碼剖析》 https://blog.csdn.net/column/details/monkeyrunner.html?&page=1

最後更新:2017-09-07 12:32:34

  上一篇:go  ApsaraDB for HBase性能/延時全麵領先社區版本
  下一篇:go  搭建spring cloud微服務架構