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
框架配置類的超類,提供給外部進行集成和配置資訊: