Android卡頓優化 | AndroidPerformanceMonitor(BlockCanary)源碼詳析(真的很詳細哦!)
- 2020 年 4 月 9 日
- 筆記
為了另外一篇性能優化實戰方案講解部落格的結構清晰和篇幅, 我們「斷章取義」,把框架的源碼解析部分搬到這邊哈~ 項目GitHub
目錄 1. 監控周期的 定義 2. dump模組 / 關於.log文件 3. 採集堆棧周期的 設定 4. 框架的 配置存儲類 以及 文件系統操作封裝 5. 文件寫入過程(生成.log文件的源碼) 6. 上傳文件 7. 設計模式、技巧 8. 框架中各個主要類的功能劃分
1. 【監控周期的 定義】 blockCanary列印一輪資訊的周期, 是從主執行緒一輪阻塞的開始開始,到阻塞的結束結束,為一輪資訊; 這個周期我們也可以成為BlockCanary的監控周期/監控時間段;
2. 【dump模組 / 關於.log文件】 這一個周期的資訊,除了展現在通知處,還會展示在logcat處, 同時框架封裝了dump模組, 即框架會把我們這一輪資訊,在手機(移動終端)記憶體中, 輸出成一個.log文件; 【當然,前提是在終端需要給這個APP授權,允許APP讀寫記憶體】 存放.log文件的目錄名,我們可以在上面提到的配置類中自定義:

如這裡定義成blockcanary,
在終端生成的文件與目錄便是這樣:

3. 【採集堆棧周期的 設定】 我們說過配置類中,這個函數可以指定認定為卡頓的閾值時間:

這裡指定為500ms,使得剛剛那個2s的阻塞被確定為卡頓問題;
其實還有一個函數,
用於指定在一個監控周期內,採集數據的周期!!!:

這裡返回的同樣是500ms,
即從執行緒阻塞開始,每500ms採集一次數據,
給出一個阻塞問題出現的根源;
而剛剛那個卡頓問題阻塞的時間是2s,
那毫無疑問我們可以猜到,剛剛那個.log文件的內容裡邊,
有2s/500ms = 4次採集的堆棧資訊!!
但是一個監控周期/log文件只列印一次現場的詳細資訊:

如果設置為250ms,那便是有2s/250ms = 8次採集的堆棧資訊了:


4. 【框架的 配置存儲類 以及 文件系統操作封裝】 框架準備了一個存儲配置的類,用於存儲響應的配置: 配置存儲類:

–getPath():拿到sd卡根目錄到存儲log文件夾的目錄路徑;!!!!!!!!
–detectedBlockDirectory():返回file類型的 存儲log文件的文件夾的目錄(如果沒有這個文件夾,就創建文件夾,再返回file類型的這個文件夾);!!!!!!!!!!
–getLogFiles():
如果detectedBlockDirectory()返回的那個存儲log文件的文件夾的目錄存在的話,
就把這個目錄下所有的.log文件過濾提取出來,
並存儲在一個File[](即File數組)裡邊,最後返回這個File數組;!!!!!!!
–getLogFiles()中的listFiles()是JDK中的方法,
用來返回文件夾類型的File類實例其 對應文件夾中(對應目錄下)所有的文件,
這裡用的是它的重載方法,
就是傳入一個過濾器,可以過濾掉不需要的文件;!!!!!!!
–BlockLogFileFilter是過濾器,用於過濾出.log文件;

###下面稍微實戰一下這個文件封裝:
吶我們在MainActivity的onCreate中,使用getLogFiles(),
功能是剛說的獲取BlockCanary生成的所有.log文件,以.log文件的形式返回,
完了我們把它列印出來:

運行之後,吶,毫無懸念,BlockCanary生成的所有.log文件都被列印出來了:

拿到了文件, 意味著我們可以在適當的時機, 將之上傳到伺服器處理!!!
5. 【文件寫入過程(生成.log文件的源碼)】
- 一切要從框架的初始化開始說起:

install()做了什麼,install()裡邊,初始化了BlockCanaryContext和Notification等的一些對象, 重要的,最後return調用了,get(); 有點單例的味道哈,BlockCanary的構造方法是私有的(下圖可以見得),get()正是返回一個BlockCanary實例, 當然new這一下也就調用了BlockCanary的構造方法; 哦~BlockCanary的構造方法中, 調用了BlockCanaryInternals.getInstance();, 拿到一個BlockCanaryInternals實例,賦給類中的全局變數!


BlockCanaryInternals.getInstance();同樣是使用了單例模式, 返回一個BlockCanaryInternals實例:

同樣也是new時候調用了BlockCanaryInternals的構造方法:

可以看到BlockCanaryInternals的構造方法中 出現了關於配置資訊存儲類以及文件的寫入邏輯了; LogWriter.save(blockInfo.toString());注意這裡傳入的是配置資訊的字元串,接著是LogWriter.save(),這裡的str便是剛剛的blockInfo.toString(),即配置資訊;

往下還有一層save(一參對應剛剛的字元串"looper",二參為Block字元串資訊【最早是來自BlockCanaryInternals中的LogWriter.save(blockInfo.toString());中的 blockInfo.toString() 】):

可以看到.log文件名的命名規則的就是定義在這裡了, 往.log文件寫入的輸入流邏輯,也都在這裡了;

對比一下剛剛實驗的結果,也就是實際生成的.log文件的文件名, 可見文件名跟上面save()方法中定義好的規則是一樣的,無誤;

這兩個在表頭的字元串格式化器, 第一個是用來給.log文件命名的,.log文件名中的時間序列來自這裡; 第二個是在save()函數中,用來寫入文件的, 用時間來區分堆棧資訊的每一次收集:

下面這個方法是用來構造zip文件實例的, 給出一個文件名,再構造一個成對應的File實例;

這個則是用來刪除本框架生成的所有log文件的:

其他的很容易看懂,就不多說了;
6.【上傳文件】 首先框架想得很周到哈,它已經為我們封裝了一個Uploader類,源碼如下:
/* * Copyright (C) 2016 MarkZhai (http://zhaiyifan.cn). * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ ... final class Uploader { private static final String TAG = "Uploader"; private static final SimpleDateFormat FORMAT = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US); private Uploader() { throw new InstantiationError("Must not instantiate this class"); } private static File zip() { String timeString = Long.toString(System.currentTimeMillis()); try { timeString = FORMAT.format(new Date()); } catch (Throwable e) { Log.e(TAG, "zip: ", e); } File zippedFile = LogWriter.generateTempZip("BlockCanary-" + timeString); BlockCanaryInternals.getContext().zip(BlockCanaryInternals.getLogFiles(), zippedFile); LogWriter.deleteAll(); return zippedFile; } public static void zipAndUpload() { HandlerThreadFactory.getWriteLogThreadHandler().post(new Runnable() { @Override public void run() { final File file = zip(); if (file.exists()) { BlockCanaryInternals.getContext().upload(file); } } }); } }
都封裝成zip文件了,想得很周到很齊全吼, 點一下這個upload,又回到配置類BlockCanaryContext這兒來,

或者可以參考一下 這篇部落格!!!!!!!,
可以在後台開啟一個執行緒,定時掃描並上傳。
或者
可以利用一下剛剛提到的 框架的文件系統操作封裝 ,
再結合 自定義網路請求邏輯,
把文件上傳到伺服器也是ok的!
7. 設計模式、技巧: 7.1 單例模式,不用多說, 剛剛提到BlockCanary和BlockCanaryInternals裡邊都用到了; 7.2 回調機制設計: 內部介面,供給回調:

定義內部介面的類,「抽象調用」回調介面方法:

介面暴露給外部,在外部實現回調:

8 .框架中各個主要類的功能劃分
–BlockCanary 提供給外部使用的,負責框架整體的方法調度;整體的、最頂層的調度;
–BlockCanaryInternals
封裝控制 周期性採集堆棧資訊並列印、輸入的關鍵邏輯;
(卡頓判定閾值、採集資訊周期 的配置,都在這裡首先被使用)
(注意這裡的onBlockEvent() 回調方法)

封裝文件操作模組(創建文件、創建文件目錄、獲取相關路徑等等 這些
從SD卡根目錄到存儲.log文件目錄 這個級別的處理,往下的目錄下文件單位級別的處理,交給LogWriter)等核心邏輯;
調用了LogWriter.save()進行log文件存儲等;
創建CpuSampler、StackSample實例,用於協助完成周期性採集;
–LogWriter 封裝了文件流的寫入、處理等邏輯;
–LooperMonitor協助完成周期性採集
【主要是阻塞任務始末的各種調度,即面向卡頓閾值;
當然,調度的內容也包括對周期性採集的啟閉調度!!!!】;

如上,
&println()有點像鬧鐘的角色,
它在主執行緒的任務分發dispatchMessage前後分別被調用一次;
它在採集周期開始的時候,就記錄下開始時間,
在阻塞任務完成之後,會再次被調用,記錄下結束時間,
&isBlock():藉助println()中記錄的關於主執行緒任務分發的開始時間和結束時間,
來判斷阻塞的時間是不是大於我們設定的或者默認的卡頓判定時間,
如果是,調用notifyBlockEvent(),間接調用到回調方法 onBlockEvent(),
這個方法上面說了,在BlockCanaryInternals 的構造器中被具體實現了,
可以調用LogWriter 最終輸出.log文件;
&startDump()和stopDump():
我們可以看到在println()中還有startDump()和stopDump()這兩個方法,
分別也是在主執行緒任務分發的開始和結束時,隨著println()被調用而被調用;

而startDump()和stopDump()的內容正是控制兩個Sample類的啟閉:

–CpuSampler、StackSample
同樣負責協助完成周期性採集
【CpuSampler的邏輯主要是面向CPU資訊的處理,而
StackSample的邏輯主要是對堆棧資訊的收集;
他們都繼承自AbstractSample】
首先在上面的源碼我們可以看到,
在BlockCanaryInternals的構造器中,
就分別創建了一個CpuSampler(參數正為採集堆棧資訊周期屬性)和一個StackSample實例(參數為採集堆棧資訊周期屬性):

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
這個參數一路上走,經過CpuSampler的構造器,
最終是在CpuSampler的父類AbstractSampler中被使用!!!!!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

我們可以看到在AbstractSampler中,
AbstractSampler構造器接收了採集堆棧資訊周期屬性,
同時準備了一個Runnable任務單元,
任務run()中做了兩件事,
第一件事是調用抽象方法doSample();
第二件事是基於這個採集堆棧周期屬性這個Runnable單元,
創建一個循環定時任務!!!!!!!!!!!!!!!!!!!!!!!!!
即,
這個Runnable單元被start()之後,
將會每隔一個採集周期,就執行一次run()和其中的doSample();
進行堆棧資訊和CPU資訊的周期性採集工作;
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
這便是BlockCanary能夠周期採集堆棧資訊的根源!!!!!!!!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

那接下來我們可以展開三個點,
解決這個三個疑問點,脈絡就理得差不多了:
【1. 由哪個Handler來處理這個Runnable】
我們知道,
Android中的多執行緒任務單元可以由一個Handler去post或者postDelayed一個Runnable來開啟;
而這裡處理這個Runnable的Handler正是
HandlerThreadFactory.getTimerThreadHandler(),
HandlerThreadFactory是框架的提供的一個內部執行緒類,
源碼解析如下,使用了工廠模式吼:

如此便可以獲得,綁定了工作執行緒(子執行緒)的Looper的 Handler;
有了這個Handler就可以處理剛剛說的Runnable任務單元了;
【2. Handler對Runnable任務單元的啟閉是在哪個地方?】
當然是在AbstractSampler提供的start()和stop()裡邊了;

CpuSampler而StackSample都會繼承自AbstractSampler,自然也就繼承了這start()和stop();
最後上面講過了,
在LooperMonitor的println()中,
startDump()和stopDump()會被調用,
而在startDump()和stopDump()中,
CpuSampler和StackSample實例的start()和stop()也會被調用了,
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
從而控制了周期採集資訊的工作執行緒(子執行緒)任務單元【上述的Runnable實例】的啟閉
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
【3. doSample()的實現】
CpuSampler、StackSample都繼承自AbstractSample,
使用的都是從AbstractSample繼承過來的Runnable實例;
前面說過這個Runnable單元被start()之後,
將會每隔一個採集周期,就執行一次run()和其中的doSample();
進行堆棧資訊和CPU資訊的周期性採集工作;

是這樣的,
然後CpuSampler、StackSample通過對父類抽象方法doSample()做了不同的實現,
使得各自循環處理的任務內容不同罷了;
【CpuSampler的面向CPU資訊的處理,
而StackSample則對堆棧資訊的收集;】



–BlockCanaryContext 框架配置類的超類,提供給外部進行集成和配置資訊:

