Android卡頓優化 | AndroidPerformanceMonitor(BlockCanary)源碼詳析(真的很詳細哦!)

為了另外一篇性能優化實戰方案講解部落格的結構清晰和篇幅, 我們「斷章取義」,把框架的源碼解析部分搬到這邊哈~ 項目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 單例模式,不用多說, 剛剛提到BlockCanaryBlockCanaryInternals裡邊都用到了; 7.2 回調機制設計: 內部介面,供給回調:

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

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

8 .框架中各個主要類的功能劃分

BlockCanary 提供給外部使用的,負責框架整體的方法調度;整體的、最頂層的調度;

BlockCanaryInternals

 封裝控制 周期性採集堆棧資訊列印、輸入的關鍵邏輯;

(卡頓判定閾值採集資訊周期 的配置,都在這裡首先被使用)

(注意這裡的onBlockEvent() 回調方法

 封裝文件操作模組(創建文件、創建文件目錄、獲取相關路徑等等 這些

從SD卡根目錄到存儲.log文件目錄 這個級別的處理,往下的目錄下文件單位級別的處理,交給LogWriter)等核心邏輯;

 調用了LogWriter.save()進行log文件存儲等;

 創建CpuSamplerStackSample實例,用於協助完成周期性採集

LogWriter 封裝了文件流寫入、處理等邏輯;

LooperMonitor協助完成周期性採集

【主要是阻塞任務始末的各種調度,即面向卡頓閾值

當然,調度的內容也包括對周期性採集啟閉調度!!!!】;

如上,

&println()有點像鬧鐘的角色,

它在主執行緒的任務分發dispatchMessage前後分別被調用一次

它在採集周期開始的時候,就記錄下開始時間

在阻塞任務完成之後,會再次被調用,記錄下結束時間

&isBlock():藉助println()中記錄的關於主執行緒任務分發開始時間結束時間

來判斷阻塞的時間是不是大於我們設定的或者默認卡頓判定時間

如果是,調用notifyBlockEvent(),間接調用到回調方法 onBlockEvent()

這個方法上面說了,在BlockCanaryInternals 的構造器中被具體實現了,

可以調用LogWriter 最終輸出.log文件;

&startDump()stopDump()

我們可以看到在println()中還有startDump()stopDump()這兩個方法,

分別也是在主執行緒任務分發開始結束時,隨著println()被調用而被調用;

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

CpuSamplerStackSample

同樣負責協助完成周期性採集

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來開啟;

而這裡處理這個RunnableHandler正是

HandlerThreadFactory.getTimerThreadHandler()

HandlerThreadFactory是框架的提供的一個內部執行緒類,

源碼解析如下,使用了工廠模式吼:

如此便可以獲得,綁定了工作執行緒(子執行緒)的LooperHandler

有了這個Handler就可以處理剛剛說的Runnable任務單元了;

【2. HandlerRunnable任務單元啟閉是在哪個地方?】

當然是在AbstractSampler提供的start()stop()裡邊了;

CpuSamplerStackSample都會繼承自AbstractSampler,自然也就繼承了這start()stop()

最後上面講過了,

LooperMonitorprintln()中,

startDump()stopDump()會被調用,

而在startDump()stopDump()中,

CpuSamplerStackSample實例的start()stop()也會被調用了,

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

從而控制了周期採集資訊工作執行緒(子執行緒)任務單元【上述的Runnable實例】的啟閉

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

【3. doSample()的實現】

CpuSamplerStackSample都繼承自AbstractSample

使用的都是從AbstractSample繼承過來的Runnable實例

前面說過這個Runnable單元start()之後,

將會每隔一個採集周期,就執行一次run()和其中的doSample()

進行堆棧資訊CPU資訊的周期性採集工作;

是這樣的,

然後CpuSamplerStackSample通過對父類抽象方法doSample()做了不同的實現,

使得各自循環處理的任務內容不同罷了;

CpuSampler的面向CPU資訊的處理,

StackSample則對堆棧資訊的收集;】

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