為了另外一篇性能優化實戰方案講解部落格的結構清晰和篇幅,
我們“斷章取義”,把架構的源碼解析部分搬到這邊哈~
項目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
架構配置類的超類,提供給外部進行內建和配置資訊: