Android卡頓檢查-BlockCanary淺析
BlockCanary是什麼?
BlockCanary是國內開發者MarkZhai開發的一套效能監控元件,主要通過監控Handler
中的dispatchMessage
過程所消耗的時間是否超過閥值來判斷是否發生卡頓。
檢測原理
介面卡頓主要是因為訊息分發處理的不及時導致的,Android的訊息分發機制主要是由Message/Looper/Handler
構建的,不熟悉Message/Looper/Handler
可以參考我之前的文章ofollow,noindex">Android訊息迴圈機制淺析
由於主執行緒只存在一個Looper
,並且Android系統所有更新UI的操作都是在主執行緒裡面執行的.
因此所有的UI操作都會經過主執行緒的Looper
訊息迴圈。
其中Looper#loop
中有那麼段程式碼
public static void loop() { ... for (;;) { ... //預設為null,可通過setMessageLogging()方法來指定輸出,用於debug功能 Printer logging = me.mLogging; if (logging != null) { //事件分發之前的時間T1 logging.println(">>>>> Dispatching to " + msg.target + " " + msg.callback + ": " + msg.what); } msg.target.dispatchMessage(msg); if (logging != null) { //事件分發之後的時間T2 logging.println("<<<<< Finished to " + msg.target + " " + msg.callback); } ... } }
如果T2-T1
的時間差大於某個閥值,就可以判斷髮生了卡頓,在BlockCanary
中,閥值是3000ms
。超過閥值之後,BlockCanary
就會將一些必要的log日誌輸出.輸出的展現方法類似leakCanary
另外要注意的一點是能不能檢測到卡頓主要看對應的事件經不經過Handler
分發,
在issues
中,有人提到用adb input keyevent E
的方法讓主執行緒休眠30s,這時BlockCanary
就不能檢測出ANR的問題,因為模擬按鍵缺少Handler
的分發。
具體討論在issues
基本使用
- 引用
dependencies { compile 'com.github.markzhai:blockcanary-android:1.5.0' // 僅在debug包啟用BlockCanary進行卡頓監控和提示的話,可以這麼用 debugCompile 'com.github.markzhai:blockcanary-android:1.5.0' releaseCompile 'com.github.markzhai:blockcanary-no-op:1.5.0' }
- 初始化
public class DemoApplication extends Application { private static Context sContext; @Override public void onCreate() { super.onCreate(); sContext = this; BlockCanary.install(this, new AppContext()).start(); } public static Context getAppContext() { return sContext; } }
原始碼解讀
BlockCanary.install(this, new AppContext()).start();
初始化過程實際上做了兩件事情,install
和start
install
public static BlockCanary install(Context context, BlockCanaryContext blockCanaryContext) { //這裡儲存applicationContext和使用者設定的引數 //其中所有的配置引數都可以自定義BlockCanaryContext的子類來實現 BlockCanaryContext.init(context, blockCanaryContext); //開啟或者關閉DisplayActivity檢視以賬號堆疊資訊,這裡會單開一個io資訊來儲存堆疊日誌 setEnabled(context, DisplayActivity.class, BlockCanaryContext.get(). displayNotification()); return get(); }
install
最後的get()方法
會建立BlockCanary
// public static BlockCanary get() { if (sInstance == null) { synchronized (BlockCanary.class) { if (sInstance == null) { sInstance = new BlockCanary(); } } } return sInstance; } //核心類BlockCanaryInternals攔截器,作用整個block流程 private BlockCanary() { BlockCanaryInternals.setContext(BlockCanaryContext.get()); mBlockCanaryCore = BlockCanaryInternals.getInstance(); mBlockCanaryCore.addBlockInterceptor(BlockCanaryContext.get()); //DisplayService只在開啟了通知欄時才會新增,當卡頓發生時將通過DisplayService發起通知欄訊息,調起 //DisplayActivity來展示堆疊資訊 if (!BlockCanaryContext.get().displayNotification()) { return; } mBlockCanaryCore.addBlockInterceptor(new DisplayService()); }
BlockCanaryInternals
public BlockCanaryInternals() { stackSampler = new StackSampler( Looper.getMainLooper().getThread(), sContext.provideDumpInterval()); cpuSampler = new CpuSampler(sContext.provideDumpInterval()); setMonitor(new LooperMonitor(new LooperMonitor.BlockListener() { @Override public void onBlockEvent(long realTimeStart, long realTimeEnd, long threadTimeStart, long threadTimeEnd) { // Get recent thread-stack entries and cpu usage ArrayList<String> threadStackEntries = stackSampler .getThreadStackEntries(realTimeStart, realTimeEnd); if (!threadStackEntries.isEmpty()) { BlockInfo blockInfo = BlockInfo.newInstance() .setMainThreadTimeCost(realTimeStart, realTimeEnd, threadTimeStart,threadTimeEnd) .setCpuBusyFlag(cpuSampler.isCpuBusy(realTimeStart, realTimeEnd)) .setRecentCpuRate(cpuSampler.getCpuRateInfo()) .setThreadStackEntries(threadStackEntries) .flushString(); LogWriter.save(blockInfo.toString()); if (mInterceptorChain.size() != 0) { for (BlockInterceptor interceptor : mInterceptorChain) { interceptor.onBlock(getContext().provideContext(), blockInfo); } } } } }, getContext().provideBlockThreshold(), getContext().stopWhenDebugging())); LogWriter.cleanObsolete(); }
onBlockEvent
start過程
public void start() { if (!mMonitorStarted) { mMonitorStarted = true; //設定Looper物件在分發訊息的時候列印debug日誌,傳入的是之前定義的LooperMonitor物件 Looper.getMainLooper().setMessageLogging(mBlockCanaryCore.monitor); } }
接下來主要看LooperMonitor
的println
函式,因為主執行緒的訊息分發在dispatchMessage
前後分別會列印一次日誌,
public void println(String x) { if (mStopWhenDebugging && Debug.isDebuggerConnected()) { return; } if (!mPrintingStarted) { mStartTimestamp = System.currentTimeMillis(); //SystemClock.currentThreadTimeMillis() 表示執行緒處於running狀態的時候,如果現場進入休眠狀//態,這個函式是不會計入時間的 mStartThreadTimestamp = SystemClock.currentThreadTimeMillis(); mPrintingStarted = true; startDump(); } else { final long endTime = System.currentTimeMillis(); mPrintingStarted = false; if (isBlock(endTime)) { notifyBlockEvent(endTime); } stopDump(); } }
-
在
dispatchMessage
前執行一次println
方法,記錄開始時間並呼叫startDump
記錄堆疊資訊 -
在
dispatchMessage
後在執行一次println
方法,並對比執行時間,
if (isBlock(endTime)) { notifyBlockEvent(endTime); } private boolean isBlock(long endTime) { return endTime - mStartTimestamp > mBlockThresholdMillis; } private void notifyBlockEvent(final long endTime) { //注意這裡需要重新建立long物件來對時間進行賦值,不然會因為淺拷貝的問題導致時間錯落 final long startTime = mStartTimestamp; final long startThreadTime = mStartThreadTimestamp; final long endThreadTime = SystemClock.currentThreadTimeMillis(); HandlerThreadFactory.getWriteLogThreadHandler().post(new Runnable() { @Override public void run() { mBlockListener.onBlockEvent(startTime, endTime, startThreadTime,endThreadTime); } }); }
對比的邏輯十分簡單,結束時間大於開始時間大於預先設定的閥值,即可理解發生block
,這時呼叫notifyBlockEvent
,將發生block
的時間資訊回傳給BlockCanaryInternals
public BlockCanaryInternals() { ... setMonitor(new LooperMonitor(new LooperMonitor.BlockListener() { @Override public void onBlockEvent(long realTimeStart, long realTimeEnd, long threadTimeStart, long threadTimeEnd) { // Get recent thread-stack entries and cpu usage ArrayList<String> threadStackEntries = stackSampler .getThreadStackEntries(realTimeStart, realTimeEnd); if (!threadStackEntries.isEmpty()) { BlockInfo blockInfo = BlockInfo.newInstance() .setMainThreadTimeCost(realTimeStart, realTimeEnd, threadTimeStart,threadTimeEnd) .setCpuBusyFlag(cpuSampler.isCpuBusy(realTimeStart, realTimeEnd)) .setRecentCpuRate(cpuSampler.getCpuRateInfo()) .setThreadStackEntries(threadStackEntries) .flushString(); //將發生block時的執行緒堆疊和cpu堆疊記錄下來 LogWriter.save(blockInfo.toString()); //回撥給DisplayService,DisplayService的邏輯很簡單,建立一個NotificationManager//物件,點選事件設定成跳轉到DisplayActivity if (mInterceptorChain.size() != 0) { for (BlockInterceptor interceptor : mInterceptorChain) { interceptor.onBlock(getContext().provideContext(), blockInfo); } } } } }, getContext().provideBlockThreshold(), getContext().stopWhenDebugging())); ... }
概括
-
自定義一個
Looper
的MessageLogging
設定給主執行緒的Looper -
在
Looper.loop
的dispatchMessage
方法前列印執行緒和CPU的堆疊資訊 -
在
Looper.loop
的dispatchMessage
方法後判斷是否發生block
-
發生
block
時呼叫DisplayService
建立NotificationManager
訊息通過 -
點選
NotificationManager
視窗跳轉到DisplayActivity
,並展示發生block
時的執行緒堆疊以及CPU堆疊