BlockCanary是我利用個(gè)人時(shí)間開發(fā)的Android平臺(tái)上的一個(gè)輕量的,非侵入式的性能監(jiān)控組件,應(yīng)用只需要簡(jiǎn)單地加幾行,提供一些該組件需要的上下文環(huán)境就可以在使用應(yīng)用的時(shí)候檢測(cè)主線程上的各種卡頓問題,并通過(guò)組件提供的各種信息分析出原因并進(jìn)行修復(fù)。
開源代碼:markzhai/AndroidPerformanceMonitor
背景
在復(fù)雜的項(xiàng)目環(huán)境中,由于歷史代碼龐大,業(yè)務(wù)復(fù)雜,包含各種第三方庫(kù),偶爾再來(lái)個(gè)jni調(diào)用,所以在出現(xiàn)了卡頓的時(shí)候,我們很難定位到底是哪里出現(xiàn)了問題,即便知道是哪一個(gè)Activity/Fragment,也仍然需要進(jìn)去里面一行一行看,動(dòng)輒數(shù)千行的類再加上跳來(lái)跳去調(diào)來(lái)調(diào)去的,結(jié)果就是不了了之隨它去了,實(shí)在不行了再優(yōu)化吧。于是一拖再拖,最后可能壓根就改不動(dòng)了,客戶端越來(lái)越卡。
事實(shí)上,很多情況下卡頓不是必現(xiàn)的,它們可能與機(jī)型、環(huán)境、操作等有關(guān),存在偶然性,即使發(fā)生了,再去查那如山般的logcat,也不一定能找到卡頓的原因,是我們自己的應(yīng)用導(dǎo)致的還是其他應(yīng)用搶占資源導(dǎo)致的?是哪些方法導(dǎo)致的?很難去回朔。有些機(jī)型自己修改了api導(dǎo)致的卡頓,還必須拿那臺(tái)機(jī)器才能去調(diào)試找原因。
BlockCanary就是來(lái)解決這個(gè)問題的。
告別打點(diǎn),告別Debug,哪里卡頓,一目了然。
介紹
BlockCanary對(duì)主線程操作進(jìn)行了完全透明的監(jiān)控,并能輸出有效的信息,幫助開發(fā)分析、定位到問題所在,迅速優(yōu)化應(yīng)用。其特點(diǎn)有:
- 非侵入式,簡(jiǎn)單的兩行就打開監(jiān)控,不需要到處打點(diǎn),破壞代碼優(yōu)雅性。
- 精準(zhǔn),輸出的信息可以幫助定位到問題所在(精確到行),不需要像Logcat一樣,慢慢去找。
目前包括了核心監(jiān)控輸出文件,以及UI顯示卡頓信息功能。僅支持Android端。
原理
熟悉Message/Looper/Handler系列的同學(xué)們一定知道Looper.java中這么一段:
private static Looper sMainLooper; // guarded by Looper.class
...
/**
* Initialize the current thread as a looper, marking it as an
* application's main looper. The main looper for your application
* is created by the Android environment, so you should never need
* to call this function yourself. See also: {@link #prepare()}
*/
public static void prepareMainLooper() {
prepare(false);
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException("The main Looper has already been prepared.");
}
sMainLooper = myLooper();
}
}
/** Returns the application's main looper, which lives in the main thread of the application.
*/
public static Looper getMainLooper() {
synchronized (Looper.class) {
return sMainLooper;
}
}
即整個(gè)應(yīng)用的主線程,只有這一個(gè)looper,不管有多少handler,最后都會(huì)回到這里。
如果再細(xì)心一點(diǎn)會(huì)發(fā)現(xiàn)在Looper的loop方法中有這么一段
public static void loop() {
...
for (;;) {
...
// This must be in a local variable, in case a UI event sets the logger
Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
msg.target.dispatchMessage(msg);
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
...
}
}
是的,就是這個(gè)Printer - mLogging,它在每個(gè)message處理的前后被調(diào)用,而如果主線程卡住了,不就是在dispatchMessage里卡住了嗎?
核心流程圖:

該組件利用了主線程的消息隊(duì)列處理機(jī)制,通過(guò)
Looper.getMainLooper().setMessageLogging(mainLooperPrinter);
并在mainLooperPrinter中判斷start和end,來(lái)獲取主線程dispatch該message的開始和結(jié)束時(shí)間,并判定該時(shí)間超過(guò)閾值(如2000毫秒)為主線程卡慢發(fā)生,并dump出各種信息,提供開發(fā)者分析性能瓶頸。
...
@Override
public void println(String x) {
if (!mStartedPrinting) {
mStartTimeMillis = System.currentTimeMillis();
mStartThreadTimeMillis = SystemClock.currentThreadTimeMillis();
mStartedPrinting = true;
} else {
final long endTime = System.currentTimeMillis();
mStartedPrinting = false;
if (isBlock(endTime)) {
notifyBlockEvent(endTime);
}
}
}
private boolean isBlock(long endTime) {
return endTime - mStartTimeMillis > mBlockThresholdMillis;
}
...
說(shuō)到此處,想到是不是可以用mainLooperPrinter來(lái)做更多事情呢?既然主線程都在這里,那只要parse出app包名的第一行,每次打印出來(lái),是不是就不需要打點(diǎn)也能記錄出用戶操作路徑? 再者,比如想做onClick到頁(yè)面創(chuàng)建后的耗時(shí)統(tǒng)計(jì),是不是也能用這個(gè)原理呢? 之后可以試試看這個(gè)思路(目前存在問題是獲取線程堆棧是定時(shí)3秒取一次的,很可能一些比較快的方法操作一下子完成了沒法在stacktrace里面反映出來(lái))。
功能
BlockCanary會(huì)在發(fā)生卡頓(通過(guò)MonitorEnv的getConfigBlockThreshold設(shè)置)的時(shí)候記錄各種信息,輸出到配置目錄下的文件,并彈出消息欄通知(可關(guān)閉)。
簡(jiǎn)單的使用如在開發(fā)、測(cè)試、Monkey的時(shí)候,Debug包啟用
- 開發(fā)可以通過(guò)圖形展示界面直接看信息,然后進(jìn)行修復(fù)
- 測(cè)試可以把log丟給開發(fā),也可以通過(guò)卡慢詳情頁(yè)右上角的更多按鈕,分享到各種聊天軟件(不要懷疑,就是抄的LeakCanary)
- Monkey生成一堆的log,找個(gè)專人慢慢過(guò)濾記錄下重要的卡慢吧
還可以通過(guò)Release包用戶端定時(shí)開啟監(jiān)控并上報(bào)log,后臺(tái)匹配堆棧過(guò)濾同類原因,提供給開發(fā)更大的樣本環(huán)境來(lái)優(yōu)化應(yīng)用。
本項(xiàng)目提供了一個(gè)友好的展示界面,供開發(fā)測(cè)試直接查看卡慢信息(基于LeakCanary的界面修改)。
dump的信息包括:
- 基本信息:安裝包標(biāo)示、機(jī)型、api等級(jí)、uid、CPU內(nèi)核數(shù)、進(jìn)程名、內(nèi)存、版本號(hào)等
- 耗時(shí)信息:實(shí)際耗時(shí)、主線程時(shí)鐘耗時(shí)、卡頓開始時(shí)間和結(jié)束時(shí)間
- CPU信息:時(shí)間段內(nèi)CPU是否忙,時(shí)間段內(nèi)的系統(tǒng)CPU/應(yīng)用CPU占比,I/O占CPU使用率
- 堆棧信息:發(fā)生卡慢前的最近堆棧,可以用來(lái)幫助定位卡慢發(fā)生的地方和重現(xiàn)路徑
sample如下圖,可以精確定位到代碼中哪一個(gè)類的哪一行造成了卡慢。

總結(jié)
BlockCanary作為一個(gè)Android組件,目前還有局限性,因?yàn)槠湓谝粋€(gè)完整的監(jiān)控系統(tǒng)中只是一個(gè)生產(chǎn)者,還需要對(duì)應(yīng)的消費(fèi)者去分析日志,比如歸類排序,以便看出哪些卡慢更有修復(fù)價(jià)值,需要優(yōu)先處理;又比如需要過(guò)濾機(jī)型,有些奇葩機(jī)型的問題造成的卡慢,到底要不要去修復(fù)是要斟酌的。扯遠(yuǎn)一點(diǎn)的話,像是埋點(diǎn)除了統(tǒng)計(jì)外,完全還能用來(lái)做鏈路監(jiān)控,比如一個(gè)完整的流程是A -> B -> D -> E, 但是某個(gè)時(shí)間節(jié)點(diǎn)突然A -> B -> D后沒有到達(dá)E,這時(shí)候監(jiān)控平臺(tái)就可以發(fā)出預(yù)警,讓開發(fā)人員及時(shí)定位。很多監(jiān)控方案都需要C/S兩端的配合。
目前阿里內(nèi)多個(gè)Android項(xiàng)目接入并使用BlockCanary來(lái)優(yōu)化Android應(yīng)用的性能。