監(jiān)控模塊解析
概述
solopi的監(jiān)控部分主要在工程目錄src的shared下,部分對性能要求較高的監(jiān)控指標采用c語言收集倦微,利用jni技術提供調用接口檀咙。
整體框架解耦性較高,其基礎性能數據監(jiān)控代碼在display目錄下璃诀。
調用鏈解析
displayable接口作為基礎的性能數據監(jiān)控接口,被具體的性能監(jiān)控實現(xiàn)類繼承實現(xiàn)蔑匣,具體的文件在目錄display\items\下劣欢,共有6個文件,實現(xiàn)了對電量裁良、cpu數據凿将、fps數據、內存數據等的監(jiān)控价脾。
每個displayable實現(xiàn)類由注解DisplayItem記錄屬性牧抵,由DisplayItemInfo解釋和使用。
displayable實現(xiàn)類由DisplayProvider類進行服務包裝侨把,統(tǒng)一對外提供運行入口和持續(xù)收集能力犀变。
具體的實現(xiàn)方式是,DisplayProvider提供了一個對外的啟動入口秋柄,startDisplay(name)方法获枝,傳入的參數是displayable實現(xiàn)類的TAG屬性,該屬性記錄了實現(xiàn)類的類名稱骇笔,從而可以運用反射原理省店,對選中的實現(xiàn)類的實現(xiàn)啟動。
完整的調用鏈關系示例如下:
PerformanceActivity加載性能監(jiān)控列表mFlootListView笨触;
---------------------------->
mFlootListView綁定性能監(jiān)控適配器PerformFloatAdapter懦傍,在適配器內的onclick()方法內,調用displayManager.updateRecordingItems方法芦劣;
---------------------------->
updateRecordingItems通過Provider.startDisplay和Provider.stopDisplay方法實現(xiàn)對監(jiān)控服務的啟停粗俱;
---------------------------->
在startDisplay方法內,傳入監(jiān)控實現(xiàn)類的tagname虚吟,通過反射動態(tài)調用監(jiān)控服務源梭。
adb提權
基本原理
由于在性能數據收集中,一些數據的采集會受限于android系統(tǒng)的版本(例如android 7 以上稍味,無法直接讀取/proc/stat文件)或者具體機型(例如 oppo的手機废麻,即使是android 7也無法直接讀取到/proc/stat文件)導致收集失敗,因此模庐,除了傳統(tǒng)的機內讀取文件等形式烛愧,solopi還補充了通過adb執(zhí)行命令的形式來收集數據。
在solopi中,adb功能主要分為兩個部分怜姿,底層的實現(xiàn)(密鑰生成慎冤、連接建立等等)引用自開源項目 adblib,git地址:https://github.com/cgutman/AdbLib 沧卢,其使用說明和api文檔很全蚁堤,這里不再闡述。 還有部分建立在底層之上但狭,是對adb命令的封裝和執(zhí)行披诗,主要集中在CmdLine和CmdTools里。
其基本原理是立磁,在設備上建立與守護進程adbd的連接呈队,從而可以在設備上執(zhí)行adb shell命令。
adb連接過程
以點擊錄制工具時為例唱歧,簡述adb的連接過程宪摧。
- screenRecordBtn.setOnClickListener對錄制按鈕設置點擊監(jiān)聽事件;
- 點擊錄制工具按鈕后颅崩,方法PermissionUtil.grantHighPrivilegePermissionAsync(new CmdTools.GrantHighPrivPermissionCallback() {...檢測是否具備adb連接條件几于,如果不具備,則提示“請在命令行執(zhí)行 adb tcpip 5555”沿后;
- 用戶執(zhí)行命令后孩革,設備的adbd守護進程開始監(jiān)聽端口5555,準備建立連接得运;
- 再次點擊錄制工具按鈕膝蜈,重新檢測后,執(zhí)行 CmdTools.generateConnection()熔掺,建立adb連接饱搏。
cpu性能數據收集
cpu的性能數據收集方法在display目錄下的CPUTools文件內,下面是該文件的解析置逻。
原理概述
solopi內推沸,cpu的主要實現(xiàn)原理只有一個(但是途徑有兩個),就是通過讀取/prpo/stat和/proc/pid/stat文件來計算出所要參數券坞。
/proc/pid/stat和/proc/stat這兩個文件網上的資料很多鬓催,這里就不過多闡述了,主要講一下具體的算法恨锚。
stat讀取途徑
solopi內有兩種讀取stat文件的途徑宇驾,分別是系統(tǒng)內直接讀取(由c實現(xiàn))和adb命令讀取猴伶。
原因主要是在安卓7.0以上课舍,無法直接讀取stat文件塌西,所以這里做了系統(tǒng)判斷,如果是7.0以上的或者是特殊機型筝尾,那么使用adb途徑讀取文件捡需;如果是7.0以下的炼彪,那么直接使用c進行讀取搓谆,使用c來讀取的好處是更快資源消耗更低揍鸟,使用adb是不得已的用法蛹头。
整體cpu使用率的計算
計算cpu總量的方法是getUsage(),
cpu總量的計算代碼(已加注釋)如下:
try {
currentJiffies = Long.parseLong(cpuInfos[1]) + Long.parseLong(cpuInfos[2]) + Long.parseLong(cpuInfos[3])
+ Long.parseLong(cpuInfos[4]) + Long.parseLong(cpuInfos[5]) + Long.parseLong(cpuInfos[6])
+ Long.parseLong(cpuInfos[7]);// 相加得到當前使用總量
currentIdle = Long.parseLong(cpuInfos[4]);// 當前的空閑用量
} catch (ArrayIndexOutOfBoundsException e) {
LogUtil.e(TAG, "ArrayIndexOutOfBoundsException" + e.getMessage(), e);
return -1f;
} catch (NumberFormatException e) {
LogUtil.e(TAG, "CPU行【%s】格式無法解析", load);
}
if (lastJiffies == 0 || lastIdle == 0) {
lastJiffies = currentJiffies; //currentJiffies是總使用量星爪; lastJiffies 最后記錄的總使用量
lastIdle = currentIdle; //currentIdle是空閑時間北苟;lastIdle 最后記錄的空閑時間
return -1f;
} else {
long gapJiffies = currentJiffies - lastJiffies; // gapJiffies 間隔時間段算出的間隔總量
long gapIdle = currentIdle - lastIdle; // gapIdle 間隔時間段算出的空閑總量
lastJiffies = currentJiffies; // 刷新一下最后用量
lastIdle = currentIdle;
if (gapIdle < 0 || gapJiffies < 0) {
return -1f;//數據有問題返回-1f
}
LogUtil.d(TAG, "CPU占用率:" + (gapJiffies - gapIdle) / (float) gapJiffies);
return 100 * (gapJiffies - gapIdle) / (float) gapJiffies;
}
可以看到闪朱,solopi的整體cpu占用率計算公式是: 100 * (gapJiffies - gapIdle) / (float) gapJiffies薛匪,即(總占用-空閑占用)/總占用
指定進程cpu占用率計算
指定進程的cpu占用率計算的方法是getPidsUsage(),
該方法主要使用命令“grep cpu /proc/stat && cat /proc/pid/stat”脓鹃,執(zhí)行后的結果存在數組內逸尖,分為兩個部分,第一部分用于計算總體占用量瘸右,這個和上面的計算過程基本一致娇跟;第二部分用于計算進程的占用量,計算代碼如下:
/**
* 應用CPU處理
* /proc/<b>pid</b>/stat 應用占用情況
* 2265 (id.XXX) S 610 609 0 0 -1 1077952832 130896 1460 185 0 683 329 3 10 14 -6 63 0 1982194 2124587008 28421 18446744073709551615 1 1 0 0 0 0 4612 0 1073798392 18446744073709551615 0 0 17 3 0 0 0 0 0 0 0 0 0 0 0 0 0
* 第14-17位之和為應用占用CPU時間之和
*/
SparseArray<Float> appResult = new SparseArray<>(pids.length + 1);
// 第一行是全局cpu數據
String[] splitLines = new String[origin.length - 1];
System.arraycopy(origin, 1, splitLines, 0, origin.length - 1);
// 處理每行獲取到的數據
SparseArray<Long> newAppProcessTime = new SparseArray<>(appProcessTime.size() + 1);
for (String line: splitLines) {
String[] processInfos = line.trim().split("\\s+");
LogUtil.d(TAG, Arrays.toString(processInfos));
// 獲取失敗的狀態(tài)
if (processInfos.length < 17) {
continue;
}
try {
int pid = Integer.parseInt(processInfos[0]);
Long pidProcessTime = Long.parseLong(processInfos[13]) + Long.parseLong(processInfos[14]) + Long.parseLong(processInfos[15]) + Long.parseLong(processInfos[16]);
Long lastProcessTime = appProcessTime.get(pid);
newAppProcessTime.put(pid, pidProcessTime);
// 如果沒有上次記錄太颤,則跳過
if (lastProcessTime == null) {
continue;
}
// 計算APP進程處理時間
Long processRunning = pidProcessTime - lastProcessTime;
appResult.put(pid, 100 * (processRunning / (float) cpuRunning));
} catch (NumberFormatException e) {
LogUtil.e(TAG, "Format for string: " + line + " failed", e);
}
}
可以看到苞俘,進程單獨的用量的公式是:
(processRunning / (float) cpuRunning)
內存數據收集
內存部分的數據收集主要在display目錄的MemoryTools下,下面是該部分的解析龄章。
原理概述
MemoryTools的數據收集主要依靠安卓原生api吃谣。
系統(tǒng)內存獲取
系統(tǒng)內存獲取主要使用的getTotalMemory(MemoryInfo)方法,入參MemoryInfo是一個內部類做裙,其主要fieid有availMem岗憋、totalMem、threshold和lowMemory锚贱,含義如下:
- availMem:系統(tǒng)上的可用內存仔戈;
- totalMem:內核可訪問的總內存;
- threshold:我們認為內存較低并開始查殺后臺服務和其他非外部進程的閾值拧廊;
- lowMemory:如果系統(tǒng)認為自己當前處于低內存狀態(tài)监徘,則設置為true。
這里系統(tǒng)的內存獲取吧碾,直接使用了availMem和totalMem字段的值凰盔。
該部分的代碼如下:
獲取總內存:
/**
* 獲取總內存數據
* @return
*/
private Long getTotalMemory() {
if (activityManager == null) {
return 0L;
}
MemoryInfo info = new MemoryInfo();
activityManager.getMemoryInfo(info);
return info.totalMem / BYTES_PER_MEGA;
}
獲取可用內存:
public static Long getAvailMemory(Context cx) {// 獲取android當前可用內存大小
if (cx == null) {
return 0L;
}
ActivityManager am = (ActivityManager) cx.getSystemService(Context.ACTIVITY_SERVICE);
MemoryInfo mi = new MemoryInfo();
am.getMemoryInfo(mi);
LogUtil.i(TAG, "Available memory: " + mi.availMem);
// mi.availMem; 當前系統(tǒng)的可用內存
return mi.availMem / BYTES_PER_MEGA;// 將獲取的內存大小規(guī)格化
}
指定進程的內存獲取
指定進程的內存獲取主要使用的Debug.MemoryInfo.getTotalPss()和Debug.MemoryInfo.getTotalPrivateDirty(),分別獲取了應用的總pss內存和PrivateDirty內存倦春。
該部分代碼如下:
獲取指定pid的內存:
if (pid != null && pid.getPid() > 0) {
Debug.MemoryInfo[] memInfos = activityManager.getProcessMemoryInfo(new int[]{pid.getPid()});
if (memInfos != null && memInfos.length > 0) {
Debug.MemoryInfo info = memInfos[0];
return String.format(Locale.CHINA, "pss:%.2fMB/privateDirty:%.2fMB", info.getTotalPss() / 1024f, info.getTotalPrivateDirty() / 1024f);
}
}
電量數據收集
內存部分的數據收集主要在display目錄的BatteryInfo下廊蜒,下面是該部分的解析趴拧。
原理概述
安卓5.0及以上,直接通過調用系統(tǒng)原生api獲取電量信息山叮;以下則讀取/sys/class/power_supply/下一個包含battery的文件夾中的current_now文件著榴;這里主要分析5.0以上的(5.0以下的設備實在太少了且越來越少了)
瞬時電流計算
瞬時電流的值主要通過調用原生api:BatteryManager.getLongProperty()獲取,其入參是規(guī)定的常量屁倔,主要有以下參數:
- BATTERY_PROPERTY_CHARGE_COUNTER: 剩余電池容量脑又,單位為微安時
- BATTERY_PROPERTY_CURRENT_NOW: 瞬時電池電流,單位為微安
- BATTERY_PROPERTY_CURRENT_AVERAGE: 平均電池電流锐借,單位為微安
- BATTERY_PROPERTY_CAPACITY: 剩余電池容量问麸,顯示為整數百分比
- BATTERY_PROPERTY_ENERGY_COUNTER: 剩余能量,單位為納瓦時
在solopi里钞翔,BatteryInfo.getCurrent(...)方法內直接使用BatteryManager.getLongProperty(BATTERY_PROPERTY_CURRENT_NOW)獲取到瞬時電量严卖;有趣的是,我還發(fā)現(xiàn)在getCurrent內布轿,也使用了BatteryManager.getLongProperty(BATTERY_PROPERTY_CURRENT_AVERAGE)來獲取平均電量哮笆,但是最終沒有使用該值作為平均電量的展示,原因會在下面寫到汰扭。
平均電流計算
在solopi里稠肘,平均電流的計算公式是: point / loop;
point是每次獲取的瞬時電量current的累加值萝毛,loop是累加次數项阴,因此,平均電流就是總累加值除以累加次數笆包。
為什么不是直接調用原生api獲然防俊?因為通過BatteryManager.getLongProperty(BATTERY_PROPERTY_CURRENT_AVERAGE)獲取到的值不是從你開啟監(jiān)控的那一刻開始計算的庵佣,而是帶上了之前的用量薯演,如果需要精準到你開始監(jiān)控的點,就只有自己計算了秧了。
通過這樣的計算方式跨扮,在solopi中也可以很方便的清除電流值(也就是初始化point和loop的值)從新時刻再次計算。
網絡數據收集
內存部分的數據收集主要在display目錄的NetWorkTools下验毡,下面是該部分的解析衡创。
原理概述
主要通過讀取/proc/net/xt_qtaguid/stats文件。
應用的上下行網絡數據獲取
網絡數據的獲取晶通,主要是通過adb shell執(zhí)行"/proc/net/xt_qtaguid/stats | grep uid"命令璃氢,讀取到對應文件,讀取出的數據每一行的格式大約是:
138 wlan0 0x0 10141 0 39063593 22051 3175000 24476 39063593 22051 0 0 0 0 3175000 24476 0 0 0 0
其中狮辽,第四列(這里solopi的注釋寫的是第一列一也,應該是誤寫了)代表了應用程序的uid巢寡;第6和8列為 rx_bytes(接收數據)和tx_bytes(上傳數據),包含tcp椰苟,udp等所有網絡流量傳輸抑月。
需要說明的是,這里獲取到的數據舆蝴,并不是瞬時數據谦絮,而是程序自開機以來的累計值,因此這個數據還需要進行相應的計算才能得出間隔時間內上傳和接收的量洁仗。
此外层皱,通過uid獲取應用的程序流量,也并非天衣無縫的赠潦,的確一般而言叫胖,一個應用只會被分配一個單獨的uid,但是如果是同一個開發(fā)方旗下有一些應用需要共享數據她奥,就可能會在menifest配置文件中使用相同的sharedUserId瓮增,這樣Android系統(tǒng)就會在安裝應用時為其分配相同的UID。
具體的計算方法是方淤,
上下行速率
在間隔的時間內钉赁,用獲取的流量差(本次獲取的數據量-上次獲取的數據量)/ 獲取的時間差(本次獲取的時刻 - 上次獲取的時刻)蹄殃,這樣就得到了間隔時間內的速率携茂。上下行累計數據
第一次獲取數據時,記一個開始的數據量诅岩,在結束時讳苦,用最后一次獲取的數據量 - 開始的數據量。
該部分的代碼是:
public static float[] getAppResult(int uid) {
String[] cmds;
/**
* /proc/net/xt_qtaguid/stats 記錄各應用網絡自開機使用情況
* 每一行數據:
* 26 wlan0 0x0 10039 0 10143 20 3061 27 10143 20 0 0 0 0 3061 27 0 0 0 0
* 第一列為UID吩谦,第6和8列為 rx_bytes(接收數據)和tx_bytes(傳輸數據)
*/
cmds = CmdTools.execAdbCmd("cat /proc/net/xt_qtaguid/stats | grep " + uid, 0).split("\n");
Long currentTime = System.currentTimeMillis();
Long rxTotal = 0L;
Long txTotal = 0L;
for (String cmd: cmds) {
String[] data = cmd.trim().split("\\s+");
if (data.length > 8) {
rxTotal += Long.parseLong(data[5]);
txTotal += Long.parseLong(data[7]);
}
}
LogUtil.i(TAG, "get Total Rx: " + rxTotal + " | get Total Tx: " + txTotal);
float rxSpeed = (rxTotal - lastAppRx) * KB_MILLION_SECOND / (currentTime - lastAppTime);
if (rxSpeed >= 0) {
lastAppRx = rxTotal;
} else {
rxSpeed = 0F;
}
float txSpeed = (txTotal - lastAppTx) * KB_MILLION_SECOND / (currentTime - lastAppTime);
if (txSpeed >= 0) {
lastAppTx = txTotal;
} else {
txSpeed = 0F;
}
lastAppTime = currentTime;
LogUtil.d(TAG, "加載Rx: %f, Tx: %f", rxSpeed, txSpeed);
if (startAppRx == 0 || startAppTx == 0) {
startAppRx = lastAppRx;
startAppTx = lastAppTx;
}
if (triggerReload) {
startAppRx = lastAppRx;
startAppTx = lastAppTx;
triggerReload = false;
}
return new float[]{rxSpeed, (lastAppRx - startAppRx) / 1024F, txSpeed, (lastAppTx - startAppTx) / 1024F};
}
整機的上下行網絡數據獲取
整機的上下行網絡數據獲取鸳谜,這里直接使用了TrafficStats類里的方法,其實其底層也是通過讀取/proc/net/xt_qtaguid/stats文件來進行的式廷,包括計算方式和單獨的應用獲取網絡數據基本相同咐扭,因此這里就不再多加闡述。