solopi源碼(監(jiān)控部分)導讀...持續(xù)更新

監(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的連接過程宪摧。

  1. screenRecordBtn.setOnClickListener對錄制按鈕設置點擊監(jiān)聽事件;
  2. 點擊錄制工具按鈕后颅崩,方法PermissionUtil.grantHighPrivilegePermissionAsync(new CmdTools.GrantHighPrivPermissionCallback() {...檢測是否具備adb連接條件几于,如果不具備,則提示“請在命令行執(zhí)行 adb tcpip 5555”沿后;
  3. 用戶執(zhí)行命令后孩革,設備的adbd守護進程開始監(jiān)聽端口5555,準備建立連接得运;
  4. 再次點擊錄制工具按鈕膝蜈,重新檢測后,執(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文件來進行的式廷,包括計算方式和單獨的應用獲取網絡數據基本相同咐扭,因此這里就不再多加闡述。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末滑废,一起剝皮案震驚了整個濱河市蝗肪,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蠕趁,老刑警劉巖薛闪,帶你破解...
    沈念sama閱讀 211,817評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異俺陋,居然都是意外死亡豁延,警方通過查閱死者的電腦和手機昙篙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來诱咏,“玉大人苔可,你說我怎么就攤上這事∫人眨” “怎么了硕蛹?”我有些...
    開封第一講書人閱讀 157,354評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長硕并。 經常有香客問我法焰,道長,這世上最難降的妖魔是什么倔毙? 我笑而不...
    開封第一講書人閱讀 56,498評論 1 284
  • 正文 為了忘掉前任埃仪,我火速辦了婚禮,結果婚禮上陕赃,老公的妹妹穿的比我還像新娘卵蛉。我一直安慰自己,他們只是感情好么库,可當我...
    茶點故事閱讀 65,600評論 6 386
  • 文/花漫 我一把揭開白布傻丝。 她就那樣靜靜地躺著,像睡著了一般诉儒。 火紅的嫁衣襯著肌膚如雪葡缰。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,829評論 1 290
  • 那天忱反,我揣著相機與錄音泛释,去河邊找鬼。 笑死温算,一個胖子當著我的面吹牛怜校,可吹牛的內容都是我干的。 我是一名探鬼主播注竿,決...
    沈念sama閱讀 38,979評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼茄茁,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了巩割?” 一聲冷哼從身側響起裙顽,我...
    開封第一講書人閱讀 37,722評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎喂分,沒想到半個月后锦庸,有當地人在樹林里發(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 44,189評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡蒲祈,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,519評論 2 327
  • 正文 我和宋清朗相戀三年甘萧,在試婚紗的時候發(fā)現(xiàn)自己被綠了萝嘁。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,654評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡扬卷,死狀恐怖牙言,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情怪得,我是刑警寧澤咱枉,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站徒恋,受9級特大地震影響蚕断,放射性物質發(fā)生泄漏。R本人自食惡果不足惜入挣,卻給世界環(huán)境...
    茶點故事閱讀 39,940評論 3 313
  • 文/蒙蒙 一亿乳、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧径筏,春花似錦葛假、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至恢氯,卻和暖如春带斑,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背酿雪。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評論 1 266
  • 我被黑心中介騙來泰國打工遏暴, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留侄刽,地道東北人指黎。 一個月前我還...
    沈念sama閱讀 46,382評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像州丹,于是被迫代替她去往敵國和親醋安。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,543評論 2 349

推薦閱讀更多精彩內容

  • 1墓毒、前言 很多時候在使用APP的時候吓揪,手機可能會發(fā)熱發(fā)燙。這是因為CPU使用率過高所计,CPU過于繁忙柠辞,會使整個手機無...
    羽墨_99e8閱讀 3,049評論 0 5
  • 安卓性能測試的重要方面是對各項性能指標的采集和分析,如常見性能指標內存主胧、cpu叭首、電量习勤、流量等,本文整理了cpu占有...
    隋胖胖LoveFat閱讀 53,737評論 8 35
  • 1焙格、前言 很多時候在使用APP的時候图毕,手機可能會發(fā)熱發(fā)燙。這是因為CPU使用率過高眷唉,CPU過于繁忙予颤,會使整個手機無...
    醉馬當前闖閱讀 2,750評論 1 2
  • 1、前言 很多時候在使用APP的時候冬阳,手機可能會發(fā)熱發(fā)燙蛤虐。這是因為CPU使用率過高,CPU過于繁忙肝陪,會使整個手機無...
    蕭竹閱讀 45,230評論 3 32
  • 親愛的金剛學友們大家晚上好笆焰,那今天呢,是我們一個分享的微客见坑,我們在這里呢嚷掠,特別的請到了非常棒的兩位進行榜樣,是來自...
    趙婉合閱讀 472評論 0 1