本文要點
- 為何需要自動化檢測方案
- 自動卡頓檢測方案原理
- 看一下Looper.loop()源碼
- 實現(xiàn)思路
- AndroidPerformanceMonitor實戰(zhàn)
- 基于AndroidPerformanceMonitor源碼簡析
- 接下來我們討論一下方案的不足
- 自動檢測方案優(yōu)化
項目GitHub
為何需要自動化檢測方案
- 前面提到過的系統(tǒng)工具只適合線下針對性分析艰匙,無法帶到線上膘盖!
- 線上及測試環(huán)節(jié)需要自動化檢測方案
方案原理
- 源于Android的消息處理機制;
一個線程不管有多少Handler磕诊,只會有一個Looper存在嗦篱,
主線程中所有的代碼冰单,都會通過Looper.loop()執(zhí)行; -
loop()中有一個
mLogging
對象灸促,
它在每個Message
處理前后都會被調(diào)用: -
如果主線程發(fā)生卡頓诫欠,
一定是在dispatchMessage
執(zhí)行了耗時操作!Handler機制圖
由此腿宰,我們便可以通過
mLogging
對象
對dispatchMessage
執(zhí)行的時間進行監(jiān)控呕诉;
看一下Looper.loop()源碼
public static void loop() {
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
final MessageQueue queue = me.mQueue;
// Make sure the identity of this thread is that of the local process,
// and keep track of what that identity token actually is.
Binder.clearCallingIdentity();
final long ident = Binder.clearCallingIdentity();
// Allow overriding a threshold with a system prop. e.g.
// adb shell 'setprop log.looper.1000.main.slow 1 && stop && start'
final int thresholdOverride =
SystemProperties.getInt("log.looper."
+ Process.myUid() + "."
+ Thread.currentThread().getName()
+ ".slow", 0);
boolean slowDeliveryDetected = false;
for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
// This must be in a local variable, in case a UI event sets the logger
final Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
final long traceTag = me.mTraceTag;
long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;
long slowDeliveryThresholdMs = me.mSlowDeliveryThresholdMs;
if (thresholdOverride > 0) {
slowDispatchThresholdMs = thresholdOverride;
slowDeliveryThresholdMs = thresholdOverride;
}
final boolean logSlowDelivery = (slowDeliveryThresholdMs > 0) && (msg.when > 0);
final boolean logSlowDispatch = (slowDispatchThresholdMs > 0);
final boolean needStartTime = logSlowDelivery || logSlowDispatch;
final boolean needEndTime = logSlowDispatch;
if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
}
final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;
final long dispatchEnd;
try {
msg.target.dispatchMessage(msg);
dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
} finally {
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}
if (logSlowDelivery) {
if (slowDeliveryDetected) {
if ((dispatchStart - msg.when) <= 10) {
Slog.w(TAG, "Drained");
slowDeliveryDetected = false;
}
} else {
if (showSlowLog(slowDeliveryThresholdMs, msg.when, dispatchStart, "delivery",
msg)) {
// Once we write a slow delivery log, suppress until the queue drains.
slowDeliveryDetected = true;
}
}
}
if (logSlowDispatch) {
showSlowLog(slowDispatchThresholdMs, dispatchStart, dispatchEnd, "dispatch", msg);
}
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
// Make sure that during the course of dispatching the
// identity of the thread wasn't corrupted.
final long newIdent = Binder.clearCallingIdentity();
if (ident != newIdent) {
Log.wtf(TAG, "Thread identity changed from 0x"
+ Long.toHexString(ident) + " to 0x"
+ Long.toHexString(newIdent) + " while dispatching to "
+ msg.target.getClass().getName() + " "
+ msg.callback + " what=" + msg.what);
}
msg.recycleUnchecked();
}
}
-
里邊有一個for循環(huán),
會不斷地讀取消息隊列隊頭進行處理:
- 處理之前吃度,會調(diào)用
logging.println()
開始
和結(jié)束
甩挫;
實現(xiàn)思路
- 通過
Looper.getMainLooper().setMessageLogging();
,
來設(shè)置我們自己的Logging椿每;
這樣每次Message處理的前后伊者,
調(diào)用的就是我們自己的Logging; - 如果匹配到
>>>>> Dispatching
间护,
我們就可以執(zhí)行一個代碼亦渗,
即在指定的閾值時間之后,
在子線程中開始執(zhí)行一個【獲取當前子線程的堆棧信息以及當前的一些場景信息(如內(nèi)存大小汁尺、變量法精、網(wǎng)絡(luò)狀態(tài)等)】的任務(wù);
如果匹配到<<<<< Finished
痴突,
則說明在指定的閾值時間內(nèi)搂蜓,Message被執(zhí)行完成,沒有發(fā)生卡頓辽装,
那便將這個任務(wù)取消掉帮碰;
AndroidPerformanceMonitor實戰(zhàn)
AndroidPerformanceMonitor原理:便是上述的實現(xiàn)思路和原理;
特性1:非侵入式的
性能監(jiān)控組件
拾积,
可以用通知的方式
彈出卡頓信息殉挽,同時用logcat
打印出關(guān)于卡頓的詳細信息;
可以檢測所有線程中執(zhí)行的任何方法拓巧,又不需要手動埋點斯碌,
設(shè)置好閾值等配置,就“坐享其成”肛度,等卡頓問題“愿者上鉤”J淠础!特性2:方便精確贤斜,可以把問題定位到代碼的具體某一行2叻汀9淇恪!
【方案的
不足
以及框架源碼解析
在下面實戰(zhàn)之后總結(jié):锬ā带族!】
實戰(zhàn)開始---------------------------------------------------
庫的依賴:
implementation 'com.github.markzhai:blockcanary-android:1.5.0'
在項目中引入依賴后,
在Application進行初始化蟀给,
BlockCanary.install(this, new AppBlockCanaryContext()).start();
第一個參數(shù)是上下文
蝙砌,
第二個參數(shù)是需要傳入一個Block的配置類實例
【BlockCanaryContext類實例或者其子類實例】:
public class TestApp extends Application {
@Override
public void onCreate() {
super.onCreate();
...
//AndroidPerformanceMonitor測試
BlockCanary.install(this, new AppBlockCanaryContext()).start();
}
}
AppBlockCanaryContext
是我們自定義的類,
配置了BlockCanary
的各種信息跋理,
代碼較多择克,可以看下GitHub,這里就不貼全部代碼了~
下面兩個配置方法分別是
給出一個uid
前普,可以用于在上報時上報當前的用戶信息
肚邢;
第二個是自定義卡頓的閾值時間
,過了閾值便認為是卡頓拭卿,
這里指定的是500ms
骡湖;
/**
* Implement in your project.
*
* @return user id
*/
public String provideUid() {
return "uid";
}
/**
* Config block threshold (in millis), dispatch over this duration is regarded as a BLOCK. You may set it
* from performance of device.
*
* @return threshold in mills
*/
public int provideBlockThreshold() {
return 500;
}
-
接著在
MainActivity
的onCreate()
中,
讓主線程沉睡兩秒(2000ms > 設(shè)定的閾值500ms)峻厚;
- 運行時响蕴,因為主線程停滯時間超過既定閾值,
組件會認為其卡頓并且彈出通知;萏摇浦夷!
當然Android8.0以后比較麻煩,
因為notificationManager需要配置Channel等才能用辜王,
或者允許后臺彈出界面
军拟,blockInfo
誓禁,
看到同樣的詳細的信息:
Block框架打印出來了【當前子線程的堆棧信息
以及當前的一些場景信息
(如內(nèi)存大小肾档、變量摹恰、網(wǎng)絡(luò)狀態(tài)等)】,
從time-start
到time-end
的時間間隔
又可以知道阻塞的時間
,如上圖展示出來的怒见,正是我們設(shè)置的2秒
K状取!G菜!闺阱!
也可以看到uid鍵
的值 便是我們剛剛設(shè)定的字符串“uid”
;
同時還直接幫我們定位到卡頓問題的出處
6姹洹:ɡ!瘦穆!
可見得BlockCanary已然
成功檢測到卡頓
問題的各種具體信息了!I尥恪扛或!
基于AndroidPerformanceMonitor源碼簡析
由于篇幅原因,筆者把以下解析內(nèi)容提取出來單獨作一篇博客哈~
目錄
1. 監(jiān)控周期的 定義
2. dump模塊 / 關(guān)于.log文件
3. 采集堆棧周期的 設(shè)定
4. 框架的 配置存儲類 以及 文件系統(tǒng)操作封裝
5. 文件寫入過程(生成.log文件的源碼)
6. 上傳文件
7. 設(shè)計模式碘饼、技巧
8. 框架中各個主要類的功能劃分
接下來我們討論一下方案的不足
- 不足1:確實檢測到卡頓熙兔,但獲取到的卡頓堆棧信息可能不準確;
- 不足2:和OOM一樣艾恼,最后的打印堆棧只是表象住涉,不是真正的問題;
我們還需要監(jiān)控過程中的一次次log信息來確定原因钠绍;
【假設(shè)初始方案舆声,整個監(jiān)控周期
只采集一次】
如上圖,
假設(shè)主線程
在時間點T1(Message分發(fā)五慈、處理前)
與T2(Message分發(fā)纳寂、處理后)
之間的時間段中發(fā)生了卡頓,
而卡頓檢測方案
是在T2時刻
泻拦,
也就是 阻塞時間完全結(jié)束 (前提是T2-T1大于閾值
毙芜,確定了是卡頓問題)的時刻,
方案才開始
獲取卡頓堆棧的信息
争拐,
而實際發(fā)生卡頓
(如發(fā)生違例耗時處理過程
)的時間點
腋粥,
可能是在這個時間段內(nèi),而非獲取信息的T2點架曹,
那有可能隘冲,
耗時操作
在時間段內(nèi)
,即在T2點之前就已經(jīng)執(zhí)行完成
了绑雄,
T2點獲取到的可能不是卡頓發(fā)生的準確時刻展辞,
也就是說T2時刻
獲取到的信息,不能夠完全反映卡頓的現(xiàn)場
万牺;
最后的T2點的堆棧信息只是表象罗珍,不能反映真正的問題;
我們需要縮小采集堆棧信息的周期脚粟,進行高頻采集
覆旱,詳細如下;
自動檢測方案優(yōu)化
優(yōu)化思路:獲取監(jiān)控周期內(nèi)的多個堆棧核无,而不僅是一個扣唱;
主要步驟:
startMonitor
開始監(jiān)控(Message分發(fā)、處理前),
接著高頻采集堆棧
T肷场A侗搿!
阻塞結(jié)束曲聂,Message分發(fā)霹购、處理后,前后時間差——阻塞時間超過閾值朋腋,即發(fā)生卡頓齐疙,便調(diào)用endMonitor
(Message分發(fā)、處理后)旭咽;
記錄 高頻采集好的堆棧信息 到文件中
贞奋;【具體源碼解析見上面解析部分(另一篇博客)】
在合適的時機上報
給服務(wù)器;【相關(guān)方案以及源碼解析見上面解析部分(另一篇博客)】如此一來穷绵,
便能更清楚地知道在整個卡頓周期(阻塞開始到結(jié)束轿塔;Message分發(fā)、處理前到后)之內(nèi)仲墨,
究竟是哪些方法在執(zhí)行勾缭,哪些方法執(zhí)行比較耗時;
優(yōu)化卡頓現(xiàn)場
不能還原的問題目养;新問題:面對 高頻卡頓堆棧信息的上報俩由、處理,服務(wù)端有壓力癌蚁;
- 突破點:一個卡頓下多個堆棧大概率有重復幻梯;
- 解決:對一個卡頓下的堆棧進行hash排重,
找出重復的堆棧努释;- 效果:極大地減少展示量碘梢,同時更高效地找到卡頓堆棧;
參考: