Android穩定性測試-- Monkey原始碼分析
背景
最近由於公司要求對移動端的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這個組的成員執行。
相關文章
- Android穩定性測試-- Monkey二次開發Android
- App穩定性測試APP
- 實用測試技能分享:APP壓力穩定性測試之Monkey入門實戰APP
- 軟體穩定性測試的測試點
- app穩定性測試-iOS篇APPiOS
- 智慧支付穩定性測試實戰
- 伺服器穩定性測試方法伺服器
- 伺服器如何測試穩定性伺服器
- 軟體測試中伺服器穩定性測試方法伺服器
- 【Android】【MonkeyDemons】針對性的進行穩定性測試Android
- android monkey 測試方法實踐Android
- 軟體測試中伺服器穩定性測試幾種方法伺服器
- Android Monkey 壓力測試 介紹Android
- Android 自動化測試之 MonkeyAndroid
- Android 測試入門之---Monkey testAndroid
- 直播系統原始碼,利用重試機制保證服務穩定性原始碼
- 提高GUI自動化測試穩定性解決方案GUI
- Windows10 10176 RTM分支穩定性測試Windows
- Android命令Monkey壓力測試,詳解Android
- [android]Monkey自動化測試引數Android
- android測試常用的adb命令以及進行Monkey測試Android
- 伺服器穩定性的測試方法,1分鐘搞定伺服器
- 演算法穩定性測試如何設計測試方案,在有限的時間內。演算法
- 神經網路穩定性分析神經網路
- OC-測試:monkey For OC(iOS 猴子測試)iOS
- 排序穩定性排序
- [android]android自動化測試四之Monkey與MonkeyRunnerAndroid
- iOS 11.4.1測試版釋出:修復小Bug 提升穩定性iOS
- iOS App 穩定性指標及監測iOSAPP指標
- 【穩定性】穩定性建設之依賴設計
- Android 原始碼分析之 AsyncTask 原始碼分析Android原始碼
- Kafka 的穩定性Kafka
- 伺服器的穩定性怎麼檢測?伺服器
- 【Android原始碼】Fragment 原始碼分析Android原始碼Fragment
- 【Android原始碼】Intent 原始碼分析Android原始碼Intent
- 【穩定性】從專案風險管理角度探討系統穩定性
- 【Android原始碼】AlertDialog 原始碼分析Android原始碼
- 麒麟960評測:穩定性甚於驍龍821