Android穩定性測試-- Monkey原始碼分析

CharliChen發表於2017-01-03

背景

最近由於公司要求對移動端的app做各種專項測試,包括穩定性測試,效能測試(cpu,記憶體,流暢度,電量,流量,啟動時間)。基於以上的測試項,我的初期想法是開發一套移動端專項測試平臺(包括資料輸入,效能指標採集,結果報告的生成與展示,bug單自動提交,持續整合),專門針對app的穩定性和效能進行測試。

本系列教程主要針對Android端的穩定性測試,後續會增加Android端的效能測試。想到穩定性,當然首選Google自帶的Monkey,但是Monkey最大的短板是,它不是基於控制元件的,導致很多事件是無效的,且覆蓋率不可控,並且沒有截圖功能導致問題很難定位;

基於上述所以決定對Monkey進行造輪子,彌補以上的不足!!


Monkey的程式碼框架

圖片來自網路:

圖片來自網路


Monkey的程式碼框架中,大概包括如下模組:

  • 主控模組:主控模組即Monkey類,是入口函式所在類,主要負責引數解析和賦值、初始化執行環境,執行runMonkeyCycles()方法,針對不同的事件源開始獲取並執行不同的事件。

  • 事件源模組:事件源代表不同的事件來源。以MonkeyEventSource為基類,它是一個介面,主要的實現類是MonkeySourceRandom,也就是預設的隨機事件源,當然也還有網路事件源 MonkeySourceNetwork,指令碼事件源 MonkeySourceScript。

    MonkeySourceRandom類首先是定義了10種事件,分別是TOUCH, MOTION, PINCHZOOM, TRACKBALL, NAV, MAJORNAV, SYSOPS, APPSWITCH, FLIP和ANYTHING,這些事件和命令列中的可設定10種引數對應。在構造方法中設定了各個事件的初始佔比,並定了一個用於儲存事件的佇列。

  • 事件模組:事件代表了各種使用者操作型別。以MonkeyEvent為基類,衍生出各種Event類,每一個Event類代表一種使用者操作型別,如常見的點選、輸入、滑動事件等。MonkeyEvent抽象類中提供了intinjectEvent()方法,用於執行對應的事件。

    這裡寫圖片描述

  • 監控模組:監控部分包括異常監控和網路監控兩部分。異常監控通過ActivityWatch類來實現,主要監控Activity的Crash和ANR事件。網路監控通過MonkeyNetworkMonitor類來實現,主要用於統計執行期間行動網路和Wi-Fi網路的連結時長


常見方法

processOptions(),用來解析命令列傳入的引數,設定相應的變數。
runMonkeyCycles() 執行具體事件

validate() 將傳入的引數結合預設的引數進行調整和校驗。
generateEvents() 生成隨機事件
getNextEvent() 獲取到事件 MonkeyEvent

injectEvent() 對於具體的事件,觸發真正的執行


常見事件

0:–pct-touch//touch
events percentage觸控事件百分比(觸控事件是一個在螢幕單一位置的按下-抬起事件)
1:–pct-motion//motion
events percentage手勢事件百分比(手勢事件是由一個在螢幕某處的按下事件、一系列的偽隨機移動、一個抬起事件組成)即一個滑動操作,但是是直線的,不能拐彎
2:–pct-pinchzoom//pinch
zoom events percentage二指縮放百分比,即智慧機上的放大縮小手勢操作
3:–pct-trackball//trackball
events percentage軌跡球事件百分比(軌跡球事件包括一個或多個隨機移動,有時還伴有點選。軌跡球現在智慧手機上已經沒有了,就是類似手柄的方向鍵一樣)
4:–pct-rotation//screen
rotation events percentage螢幕旋轉百分比,橫屏豎屏
5:–pct-nav//nav
events percentage”基本”導航事件百分比(導航事件包括上下左右,如方向輸入裝置的輸入)老手機的上下左右鍵,智慧機上沒有
6:–pct-majornav//major
nav events percentage”主要”導航事件百分比(這些導航事件通常會引發UI的事件,例如5-way pad的中間鍵、回退鍵、選單鍵)
7:–pct-syskeys//system(key)
operations percentage”系統”按鈕事件百分比(這些按鈕一般專供系統使用,如Home, Back, Start Call, End Call,音量控制)
8:–pct-appswitch//app
switch events percentage啟動activity事件百分比。在隨機的間隔裡,Monkey會執行一個startActivity()呼叫,作為最大程度覆蓋包中全部Activity的一種方法
9:–pct-flip//keyboard
flip percentage鍵盤輕彈百分比,如點選輸入框,鍵盤彈起,點選輸入框以外區域,鍵盤收回
10:–pct-anyevent//anyevents
percentage其他型別事件百分比。包括了其他所有的型別事件,如按鍵、其他不常用的裝置上的按鈕等等。


主控模組程式碼分析

1. main方法:

/**
     * 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");

        int resultCode = (new Monkey()).run(args);
        System.exit(resultCode);
    }

  第一句的意思就是在 shell 命令列下 使用 ps | grep com.**.monkey 就找到正在執行的monkey程式
  第二句是後續的內容,我們繼續看後續幹了什麼。
  

2. run方法

/**
     * Run the command!
     *
     * @param args The command-line arguments
     * @return Returns a posix-style result code. 0 for no error.
     */
    private int run(String[] args) {
        // Super-early debugger wait
        for (String s : args) {
            if ("--wait-dbg".equals(s)) {
                Debug.waitForDebugger();
            }
        }

        // Default values for some command-line options
        mVerbose = 0;
        mCount = 1000;
        mSeed = 0;
        mThrottle = 0;

        // prepare for command-line processing
        mArgs = args;
        mNextArg = 0;

        // set a positive value, indicating none of the factors is provided yet
        for (int i = 0; i < MonkeySourceRandom.FACTORZ_COUNT; i++) {
            mFactors[i] = 1.0f;
        }

        if (!processOptions()) {
            return -1;
        }

        if (!loadPackageLists()) {
            return -1;
        }

        // now set up additional data in preparation for launch
        if (mMainCategories.size() == 0) {
            mMainCategories.add(Intent.CATEGORY_LAUNCHER);
            mMainCategories.add(Intent.CATEGORY_MONKEY);
        }

        if (mVerbose > 0) {
            System.out.println(":Monkey: seed=" + mSeed + " count=" + mCount);
            if (mValidPackages.size() > 0) {
                Iterator<String> it = mValidPackages.iterator();
                while (it.hasNext()) {
                    System.out.println(":AllowPackage: " + it.next());
                }
            }
            if (mInvalidPackages.size() > 0) {
                Iterator<String> it = mInvalidPackages.iterator();
                while (it.hasNext()) {
                    System.out.println(":DisallowPackage: " + it.next());
                }
            }
            if (mMainCategories.size() != 0) {
                Iterator<String> it = mMainCategories.iterator();
                while (it.hasNext()) {
                    System.out.println(":IncludeCategory: " + it.next());
                }
            }
        }

        if (!checkInternalConfiguration()) {
            return -2;
        }

        if (!getSystemInterfaces()) {
            return -3;
        }

        if (!getMainApps()) {
            return -4;
        }

        mRandom = new SecureRandom();
        mRandom.setSeed((mSeed == 0) ? -1 : mSeed);

        if (mScriptFileNames != null && mScriptFileNames.size() == 1) {
            // script mode, ignore other options
            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) {
                System.out.println("Error binding to network socket.");
                return -5;
            }
            mCount = Integer.MAX_VALUE;
        } else {
            // random source by default
            if (mVerbose >= 2) { // check seeding performance
                System.out.println("// Seeded: " + mSeed);
            }
            mEventSource = new MonkeySourceRandom(mRandom, mMainApps, mThrottle, mRandomizeThrottle);
            mEventSource.setVerbose(mVerbose);
            // set any of the factors that has been set
            for (int i = 0; i < MonkeySourceRandom.FACTORZ_COUNT; i++) {
                if (mFactors[i] <= 0.0f) {
                    ((MonkeySourceRandom) mEventSource).setFactors(i, mFactors[i]);
                }
            }

            // in random mode, we start with a random activity
            ((MonkeySourceRandom) mEventSource).generateActivity();
        }

        // validate source generator
        if (!mEventSource.validate()) {
            return -5;
        }

        // If we're profiling, do it immediately before/after the main monkey
        // loop
        if (mGenerateHprof) {
            signalPersistentProcesses();
        }

        mNetworkMonitor.start();
        int crashedAtCycle = runMonkeyCycles();
        mNetworkMonitor.stop();

        synchronized (this) {
            if (mRequestAnrTraces) {
                reportAnrTraces();
                mRequestAnrTraces = false;
            }
            if (mRequestAnrBugreport){
                System.out.println("Print the anr report");
                getBugreport("anr_" + mReportProcessName + "_");
                mRequestAnrBugreport = false;
            }
            if (mRequestAppCrashBugreport){
                getBugreport("app_crash" + mReportProcessName + "_");
                mRequestAppCrashBugreport = false;
            }
            if (mRequestDumpsysMemInfo) {
                reportDumpsysMemInfo();
                mRequestDumpsysMemInfo = false;
            }
            if (mRequestPeriodicBugreport){
                getBugreport("Bugreport_");
                mRequestPeriodicBugreport = false;
            }
        }

        if (mGenerateHprof) {
            signalPersistentProcesses();
            if (mVerbose > 0) {
                System.out.println("// Generated profiling reports in /data/misc");
            }
        }

        try {
            mAm.setActivityController(null);
            mNetworkMonitor.unregister(mAm);
        } catch (RemoteException e) {
            // just in case this was latent (after mCount cycles), make sure
            // we report it
            if (crashedAtCycle >= mCount) {
                crashedAtCycle = mCount - 1;
            }
        }

        // report dropped event stats
        if (mVerbose > 0) {
            System.out.print(":Dropped: keys=");
            System.out.print(mDroppedKeyEvents);
            System.out.print(" pointers=");
            System.out.print(mDroppedPointerEvents);
            System.out.print(" trackballs=");
            System.out.print(mDroppedTrackballEvents);
            System.out.print(" flips=");
            System.out.println(mDroppedFlipEvents);
        }

        // report network stats
        mNetworkMonitor.dump();

        if (crashedAtCycle < mCount - 1) {
            System.err.println("** System appears to have crashed at event " + crashedAtCycle
                    + " of " + mCount + " using seed " + mSeed);
            return crashedAtCycle;
        } else {
            if (mVerbose > 0) {
                System.out.println("// Monkey finished");
            }
            return 0;
        }
    }

這個run中的內容基本就是Monkey執行的流程,主要做了:
1、處理命令列引數:

if (!processOptions()) {
            return -1;
        }

2、處理要拉起的應用程式的Activity:
 我們在執行Monkey的時候,如果指定了“ -p 包名 ”,那麼Monkey一定會拉起這個App的第一個Activity,這個究竟是怎麼實現的呢?就是藉助Intent這個東西:

 // now set up additional data in preparation for launch
 if (mMainCategories.size() == 0) {
        mMainCategories.add(Intent.CATEGORY_LAUNCHER);
        mMainCategories.add(Intent.CATEGORY_MONKEY);
 }

3、處理Source模組:
Source模組,以MonkeyEventSource為介面,衍生出三種Source類:MonkeySourceRandom類(隨機生成事件)、MonkeySourceScript(從指令碼獲取事件)、MonkeySourceNetwork(從網路獲取事件)。    

if (mScriptFileNames != null && mScriptFileNames.size() == 1) {
            // script mode, ignore other options
            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) {
                System.out.println("Error binding to network socket.");
                return -5;
            }
            mCount = Integer.MAX_VALUE;
        } else {
            // random source by default
            if (mVerbose >= 2) { // check seeding performance
                System.out.println("// Seeded: " + mSeed);
            }
            mEventSource = new MonkeySourceRandom(mRandom, mMainApps, mThrottle, mRandomizeThrottle);
            mEventSource.setVerbose(mVerbose);
            // set any of the factors that has been set
            for (int i = 0; i < MonkeySourceRandom.FACTORZ_COUNT; i++) {
                if (mFactors[i] <= 0.0f) {
                    ((MonkeySourceRandom) mEventSource).setFactors(i, mFactors[i]);
                }
            }

            // in random mode, we start with a random activity
            ((MonkeySourceRandom) mEventSource).generateActivity();
        }

這部分只要是來判斷Monkey的事件源來自何方,根據這些事件的來源,由不同的類做處理。MonkeySourceRandom事件的來源就是我們在命令列輸入引數後的偽隨機壓力測試;MonkeySourceScript事件來源於Monkey識別的一種指令碼,事實上Monkey也可以做到通過指令碼指定位置點選,滑動等操作,但是該指令碼的可讀性非常的差,編寫不易,因此這裡我也沒有介紹;第三種MonkeySourceNetwork來自於後面我們要講的Monkeyrunner,Monkeyrunner通過socket將一些要處理的事件發給Monkey,由Monkey來完成最後的處理。

4、迴圈處理事件:

        mNetworkMonitor.start();
        int crashedAtCycle = runMonkeyCycles();
        mNetworkMonitor.stop();  

主要看看 runMonkeyCycles() 這個函式主要做了什麼:

/** 
     * Run mCount cycles and see if we hit any crashers. 
     * <p> 
     * TODO: Meta state on keys 
     * 
     * @return Returns the last cycle which executed. If the value == mCount, no 
     *         errors detected. 
     */  
    private int runMonkeyCycles() {  
        int eventCounter = 0;  
        int cycleCounter = 0;  

        boolean shouldReportAnrTraces = false;  
        boolean shouldReportDumpsysMemInfo = false;  
        boolean shouldAbort = false;  
        boolean systemCrashed = false;  

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

這裡涉及到了一個重要的東西就是MonkeyEvent。
以MonkeyEvent為基類,衍生出各種Event類,如Monkey中常見的點選,輸入,滑動事件;
那麼一個點選的操作究竟是怎麼進行下去的呢?我們可以到上面呼叫的是injectEvent,這個方法是由基類定義的,每一個子類去實現不同的內容,點選、滑動等這個方法都是通過第一個引數一個iWindowManager的物件而完成的,當然也有不需要這個引數,例如MonkeyThrottleEvent這個類的實現方法,根本沒有用到iwm:

@Override  
public int injectEvent(IWindowManager iwm, IActivityManager iam, int verbose) {  

    if (verbose > 1) {  
        System.out.println("Sleeping for " + mThrottle + " milliseconds");  
    }  
    try {  
        Thread.sleep(mThrottle);  
    } catch (InterruptedException e1) {  
        System.out.println("** Monkey interrupted in sleep.");  
        return MonkeyEvent.INJECT_FAIL;  
    }  

    return MonkeyEvent.INJECT_SUCCESS;  
}

那麼這個iWindowManager的物件究竟是什麼呢?這個事系統隱藏的一個介面,通過這個介面可以注入一些操作事件,那麼我們以後是不是也可以用這個介面來進行事件的注入呢?答案是no
我們來看看:谷歌為了方便Monkey能夠輕鬆的完成一些點選、滑動事件,因此在使用了這個系統隱藏的介面,Monkey這個應用擁有這個兩個獨特的許可權:第一個是SET_ACTIVITY_WATCHER這個許可權,它允許monkey對activity的生命週期進行全權控制。第二個就是INJECT_EVENTS這個許可權它允許monkey去模擬觸控和按鍵事件。為了防止這個系統隱藏介面暴露出的漏洞,普通的App是不能請求到這些許可權的,只有android系統同意的應用才會得到允許獲得這些許可權。為了防止壞人使用Monkey來進行這個事件的注入,Monkey也只被允許root執行或者是shell這個組的成員執行。

相關文章