BlockCanary — 輕松找出Android App界面卡頓元兇
BlockCanary是我利用個人時間開發的Android平臺上的一個輕量的,非侵入式的性能監控組件,應用只需要簡單地加幾行,提供一些該組件需要的上下文環境就可以在使用應用的時候檢測主線程上的各種卡頓問題,并通過組件提供的各種信息分析出原因并進行修復。
由于該組件在阿里內部開源,所以對外暫時不放出源碼,這里對原理和部分實現做描述。
背景
在復雜的項目環境中,由于歷史代碼龐大,業務復雜,包含各種第三方庫,偶爾再來個jni調用,所以在出現了卡頓的時候,我們很難定位到底是哪里出現了問題,即便知道是哪一個Activity/Fragment,也仍然需要進去里面一行一行看,動輒數千行的類再加上跳來跳去調來調去的,結果就是不了了之隨它去了,實在不行了再優化吧。于是一拖再拖,最后可能壓根就改不動了,客戶端越來越卡。
事實上,很多情況下卡頓不是必現的,它們可能與機型、環境、操作等有關,存在偶然性,即使發生了,再去查那如山般的logcat,也不一定能找到卡頓的原因,是我們自己的應用導致的還是其他應用搶占資源導致的?是哪些方法導致的?很難去回朔。有些機型自己修改了api導致的卡頓,還必須拿那臺機器才能去調試找原因。
BlockCanary就是來解決這個問題的。告別打點,告別Debug,哪里卡頓,一目了然。
介紹
BlockCanary對主線程操作進行了完全透明的監控,并能輸出有效的信息,幫助開發分析、定位到問題所在,迅速優化應用。其特點有:
- 非侵入式,簡單的兩行就打開監控,不需要到處打點,破壞代碼優雅性。
- 精準,輸出的信息可以幫助定位到問題所在(精確到行),不需要像Logcat一樣,慢慢去找。
目前包括了核心監控輸出文件,以及UI顯示卡頓信息功能。僅支持Android端。
原理
熟悉Message/Looper/Handler系列的同學們一定知道 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; } }
</div>
即整個應用的主線程,只有這一個looper,不管有多少handler,最后都會回到這里。
如果再細心一點會發現在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); } ... } }
</div>
是的,就是這個Printer - mLogging,它在每個message處理的前后被調用,而如果主線程卡住了,不就是在dispatchMessage里卡住了嗎?
核心流程圖:

該組件利用了主線程的消息隊列處理機制,通過
Looper.getMainLooper().setMessageLogging(mainLooperPrinter);
</div>
并在 mainLooperPrinter 中判斷start和end,來獲取主線程dispatch該message的開始和結束時間,并判定該時間超過閾值(如2000毫秒)為主線程卡慢發生,并dump出各種信息,提供開發者分析性能瓶頸。
... @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; } ...
</div>
說到此處,想到是不是可以用mainLooperPrinter來做更多事情呢?既然主線程都在這里,那只要parse出app包名的第一行,每次打印出來,是不是就不需要打點也能記錄出用戶操作路徑? 再者,比如想做onClick到頁面創建后的耗時統計,是不是也能用這個原理呢? 之后可以試試看這個思路(目前存在問題是獲取線程堆棧是定時3秒取一次的,很可能一些比較快的方法操作一下子完成了沒法在stacktrace里面反映出來)。
功能
BlockCanary會在發生卡頓(通過MonitorEnv的getConfigBlockThreshold設置)的時候記錄各種信息,輸出到配置目錄下的文件,并彈出消息欄通知(可關閉)。
簡單的使用如在開發、測試、Monkey的時候,Debug包啟用
- 開發可以通過圖形展示界面直接看信息,然后進行修復
- 測試可以把log丟給開發,也可以通過卡慢詳情頁右上角的更多按鈕,分享到各種聊天軟件(不要懷疑,就是抄的LeakCanary)
- Monkey生成一堆的log,找個專人慢慢過濾記錄下重要的卡慢吧
還可以通過Release包用戶端定時開啟監控并上報log,后臺匹配堆棧過濾同類原因,提供給開發更大的樣本環境來優化應用。
本項目提供了一個友好的展示界面,供開發測試直接查看卡慢信息(基于LeakCanary的界面修改)。
dump的信息包括:
- 基本信息:安裝包標示、機型、api等級、uid、CPU內核數、進程名、內存、版本號等
- 耗時信息:實際耗時、主線程時鐘耗時、卡頓開始時間和結束時間
- CPU信息:時間段內CPU是否忙,時間段內的系統CPU/應用CPU占比,I/O占CPU使用率
- 堆棧信息:發生卡慢前的最近堆棧,可以用來幫助定位卡慢發生的地方和重現路徑
sample如下圖,可以精確定位到代碼中哪一個類的哪一行造成了卡慢。

總結
BlockCanary作為一個Android組件,目前還有局限性,因為其在一個完整的監控系統中只是一個生產者,還需要對應的消費者去分析日志,比如歸類排序,以便看出哪些卡慢更有修復價值,需要優先處理;又比如需要過濾機型,有些奇葩機型的問題造成的卡慢,到底要不要去修復是要斟酌的。扯遠一點的話,像是埋點除了統計外,完全還能用來做鏈路監控,比如一個完整的流程是A -> B -> D -> E, 但是某個時間節點突然A -> B -> D后沒有到達E,這時候監控平臺就可以發出預警,讓開發人員及時定位。很多監控方案都需要C/S兩端的配合。
目前阿里內多個Android項目接入并使用BlockCanary來優化Android應用的性能。
來自: http://blog.zhaiyifan.cn/2016/01/16/BlockCanaryTransparentPerformanceMonitor/