本文對BlockCanary源碼進(jìn)行了分析。
《行宮》
寥落古行宮,宮花寂寞紅。
白頭宮女在,閑坐說玄宗。
—唐,元稹
原理(轉(zhuǎn)自BlockCanary)
熟悉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;
}
}
即整個應(yīng)用的主線程,只有這一個looper,不管有多少handler,最后都會回到這里。
如果再細(xì)心一點會發(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);
}
...
}
}
是的,就是這個Printer - mLogging,它在每個message處理的前后被調(diào)用,而如果主線程卡住了,不就是在dispatchMessage里卡住了嗎?
核心流程圖:

該組件利用了主線程的消息隊列處理機制,通過
Looper.getMainLooper().setMessageLogging(mainLooperPrinter);
并在mainLooperPrinter中判斷start和end,來獲取主線程dispatch該message的開始和結(jié)束時間,并判定該時間超過閾值(如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;
}
說到此處,想到是不是可以用mainLooperPrinter來做更多事情呢?既然主線程都在這里,那只要parse出app包名的第一行,每次打印出來,是不是就不需要打點也能記錄出用戶操作路徑? 再者,比如想做onClick到頁面創(chuàng)建后的耗時統(tǒng)計,是不是也能用這個原理呢? 之后可以試試看這個思路(目前存在問題是獲取線程堆棧是定時3秒取一次的,很可能一些比較快的方法操作一下子完成了沒法在stacktrace里面反映出來)。
源碼解析
使用方法:
BlockCanary.install(this, new AppBlockCanaryContext()).start();
從上面的入口先來看一下BlockCanary類,可以看到只是簡單的初始化賦值等操作,start方法中給MainLooper設(shè)置了打印消息的監(jiān)聽,構(gòu)造方法中判斷如果需要顯示通知會使用mBlockCanaryCore.addBlockInterceptor方法添加阻塞事件監(jiān)聽。
BlockCanary.java
private BlockCanaryInternals mBlockCanaryCore;
private BlockCanary() {
BlockCanaryInternals.setContext(BlockCanaryContext.get());
mBlockCanaryCore = BlockCanaryInternals.getInstance();
mBlockCanaryCore.addBlockInterceptor(BlockCanaryContext.get());
if (!BlockCanaryContext.get().displayNotification()) {
return;
}
mBlockCanaryCore.addBlockInterceptor(new DisplayService());
}
/**
* Install {@link BlockCanary}
*
* @param context Application context
* @param blockCanaryContext BlockCanary context
* @return {@link BlockCanary}
*/
public static BlockCanary install(Context context, BlockCanaryContext blockCanaryContext) {
BlockCanaryContext.init(context, blockCanaryContext);
setEnabled(context, DisplayActivity.class, BlockCanaryContext.get().displayNotification());
return get();
}
/**
* Get {@link BlockCanary} singleton.
*
* @return {@link BlockCanary} instance
*/
public static BlockCanary get() {
if (sInstance == null) {
synchronized (BlockCanary.class) {
if (sInstance == null) {
sInstance = new BlockCanary();
}
}
}
return sInstance;
}
/**
* Start monitoring.
*/
public void start() {
if (!mMonitorStarted) {
mMonitorStarted = true;
Looper.getMainLooper().setMessageLogging(mBlockCanaryCore.monitor);
}
}
其中BlockCanaryInternals類中monitor變量的類型是LooperMonitor類,該類實現(xiàn)了Printer接口,從原理部分我們知道如果我們使用Looper.getMainLooper().setMessageLogging()方法設(shè)置了打印日志的監(jiān)聽之后,主線程中所有的事件都會調(diào)用此方法:
LooperMonitor.java
@Override
public void println(String x) {
if (mStopWhenDebugging && Debug.isDebuggerConnected()) {
return;
}
if (!mPrintingStarted) {
mStartTimestamp = System.currentTimeMillis();
mStartThreadTimestamp = SystemClock.currentThreadTimeMillis();
mPrintingStarted = true;
startDump();
} else {
final long endTime = System.currentTimeMillis();
mPrintingStarted = false;
if (isBlock(endTime)) {
notifyBlockEvent(endTime);
}
stopDump();
}
}
當(dāng)事件開始會調(diào)用startDump方法開始采樣,獲取堆棧信息和CPU信息,事件結(jié)束會判斷是否超過指定阻塞時間值,如果超過會在HandlerThreadFactory提供的HandlerThread子線程中通過接口回調(diào)到BlockCanaryInternals類中,該接口通過LooperMonitor構(gòu)造方法傳入,然后調(diào)用stopDump方法停止采樣。
LooperMonitor.java
private BlockListener mBlockListener = null;
public interface BlockListener {
void onBlockEvent(long realStartTime,
long realTimeEnd,
long threadTimeStart,
long threadTimeEnd);
}
public LooperMonitor(BlockListener blockListener, long blockThresholdMillis, boolean stopWhenDebugging) {
if (blockListener == null) {
throw new IllegalArgumentException("blockListener should not be null.");
}
mBlockListener = blockListener;
mBlockThresholdMillis = blockThresholdMillis;
mStopWhenDebugging = stopWhenDebugging;
}
private void notifyBlockEvent(final long endTime) {
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);
}
});
}
private void startDump() {
if (null != BlockCanaryInternals.getInstance().stackSampler) {
BlockCanaryInternals.getInstance().stackSampler.start();
}
if (null != BlockCanaryInternals.getInstance().cpuSampler) {
BlockCanaryInternals.getInstance().cpuSampler.start();
}
}
private void stopDump() {
if (null != BlockCanaryInternals.getInstance().stackSampler) {
BlockCanaryInternals.getInstance().stackSampler.stop();
}
if (null != BlockCanaryInternals.getInstance().cpuSampler) {
BlockCanaryInternals.getInstance().cpuSampler.stop();
}
}
接下來我們看下AbstractSampler抽象類,CpuSampler和StackSampler繼承自該類,該類中主要處理了start和stop方法,以及一個Runnable調(diào)用抽象方法doSample,runnable會在HandlerThreadFactory類中提供的HandleThread子線程中執(zhí)行。
AbstractSampler.java
private Runnable mRunnable = new Runnable() {
@Override
public void run() {
doSample();
if (mShouldSample.get()) {
HandlerThreadFactory.getTimerThreadHandler()
.postDelayed(mRunnable, mSampleInterval);
}
}
};
...
public void start() {
if (mShouldSample.get()) {
return;
}
mShouldSample.set(true);
HandlerThreadFactory.getTimerThreadHandler().removeCallbacks(mRunnable);
HandlerThreadFactory.getTimerThreadHandler().postDelayed(mRunnable,
BlockCanaryInternals.getInstance().getSampleDelay());
}
public void stop() {
if (!mShouldSample.get()) {
return;
}
mShouldSample.set(false);
HandlerThreadFactory.getTimerThreadHandler().removeCallbacks(mRunnable);
}
abstract void doSample();
StackSampler類中通過mCurrentThread.getStackTrace()獲取堆棧信息,存儲到sStackMap靜態(tài)變量中。
StackSampler.java
private static final LinkedHashMap<Long, String> sStackMap = new LinkedHashMap<>();
@Override
protected void doSample() {
StringBuilder stringBuilder = new StringBuilder();
for (StackTraceElement stackTraceElement : mCurrentThread.getStackTrace()) {
stringBuilder
.append(stackTraceElement.toString())
.append(BlockInfo.SEPARATOR);
}
synchronized (sStackMap) {
if (sStackMap.size() == mMaxEntryCount && mMaxEntryCount > 0) {
sStackMap.remove(sStackMap.keySet().iterator().next());
}
sStackMap.put(System.currentTimeMillis(), stringBuilder.toString());
}
}
CpuSampler類中對CPU進(jìn)行采樣,并將CPU信息存儲到mCpuInfoEntries變量中:
CpuSampler.java
private final LinkedHashMap<Long, String> mCpuInfoEntries = new LinkedHashMap<>();
@Override
protected void doSample() {
BufferedReader cpuReader = null;
BufferedReader pidReader = null;
try {
cpuReader = new BufferedReader(new InputStreamReader(
new FileInputStream("/proc/stat")), BUFFER_SIZE);
String cpuRate = cpuReader.readLine();
if (cpuRate == null) {
cpuRate = "";
}
if (mPid == 0) {
mPid = android.os.Process.myPid();
}
pidReader = new BufferedReader(new InputStreamReader(
new FileInputStream("/proc/" + mPid + "/stat")), BUFFER_SIZE);
String pidCpuRate = pidReader.readLine();
if (pidCpuRate == null) {
pidCpuRate = "";
}
parse(cpuRate, pidCpuRate);
} catch (Throwable throwable) {
Log.e(TAG, "doSample: ", throwable);
} finally {
try {
if (cpuReader != null) {
cpuReader.close();
}
if (pidReader != null) {
pidReader.close();
}
} catch (IOException exception) {
Log.e(TAG, "doSample: ", exception);
}
}
}
private void parse(String cpuRate, String pidCpuRate) {
String[] cpuInfoArray = cpuRate.split(" ");
if (cpuInfoArray.length < 9) {
return;
}
long user = Long.parseLong(cpuInfoArray[2]);
long nice = Long.parseLong(cpuInfoArray[3]);
long system = Long.parseLong(cpuInfoArray[4]);
long idle = Long.parseLong(cpuInfoArray[5]);
long ioWait = Long.parseLong(cpuInfoArray[6]);
long total = user + nice + system + idle + ioWait
+ Long.parseLong(cpuInfoArray[7])
+ Long.parseLong(cpuInfoArray[8]);
String[] pidCpuInfoList = pidCpuRate.split(" ");
if (pidCpuInfoList.length < 17) {
return;
}
long appCpuTime = Long.parseLong(pidCpuInfoList[13])
+ Long.parseLong(pidCpuInfoList[14])
+ Long.parseLong(pidCpuInfoList[15])
+ Long.parseLong(pidCpuInfoList[16]);
if (mTotalLast != 0) {
StringBuilder stringBuilder = new StringBuilder();
long idleTime = idle - mIdleLast;
long totalTime = total - mTotalLast;
stringBuilder
.append("cpu:")
.append((totalTime - idleTime) * 100L / totalTime)
.append("% ")
.append("app:")
.append((appCpuTime - mAppCpuTimeLast) * 100L / totalTime)
.append("% ")
.append("[")
.append("user:").append((user - mUserLast) * 100L / totalTime)
.append("% ")
.append("system:").append((system - mSystemLast) * 100L / totalTime)
.append("% ")
.append("ioWait:").append((ioWait - mIoWaitLast) * 100L / totalTime)
.append("% ]");
synchronized (mCpuInfoEntries) {
mCpuInfoEntries.put(System.currentTimeMillis(), stringBuilder.toString());
if (mCpuInfoEntries.size() > MAX_ENTRY_COUNT) {
for (Map.Entry<Long, String> entry : mCpuInfoEntries.entrySet()) {
Long key = entry.getKey();
mCpuInfoEntries.remove(key);
break;
}
}
}
}
mUserLast = user;
mSystemLast = system;
mIdleLast = idle;
mIoWaitLast = ioWait;
mTotalLast = total;
mAppCpuTimeLast = appCpuTime;
}
接下來回到BlockCanaryInternals類看下阻塞事件發(fā)生時的處理:
BlockCanaryInternals.java
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();
}
先從堆棧采樣類中獲取阻塞時間期間的堆棧信息:
StackSampler.java
public ArrayList<String> getThreadStackEntries(long startTime, long endTime) {
ArrayList<String> result = new ArrayList<>();
synchronized (sStackMap) {
for (Long entryTime : sStackMap.keySet()) {
if (startTime < entryTime && entryTime < endTime) {
result.add(BlockInfo.TIME_FORMATTER.format(entryTime)
+ BlockInfo.SEPARATOR
+ BlockInfo.SEPARATOR
+ sStackMap.get(entryTime));
}
}
}
return result;
}
然后創(chuàng)建BlockInfo存儲到本地文件中,如果設(shè)置了阻塞監(jiān)聽,會逐一回調(diào)給監(jiān)聽者。如果我們設(shè)置了顯示通知,會回調(diào)給DisplayService類處理通知的顯示。
判斷CPU是否比較busy的值為采樣時間間隔的1.2倍,如果兩次采樣時間間隔大于采樣時間間隔的1.2倍,則認(rèn)為CPU當(dāng)前是busy的。
CpuSampler.java
public CpuSampler(long sampleInterval) {
super(sampleInterval);
BUSY_TIME = (int) (mSampleInterval * 1.2f);
}
public boolean isCpuBusy(long start, long end) {
if (end - start > mSampleInterval) {
long s = start - mSampleInterval;
long e = start + mSampleInterval;
long last = 0;
synchronized (mCpuInfoEntries) {
for (Map.Entry<Long, String> entry : mCpuInfoEntries.entrySet()) {
long time = entry.getKey();
if (s < time && time < e) {
if (last != 0 && time - last > BUSY_TIME) {
return true;
}
last = time;
}
}
}
}
return false;
}
至此,BlockCanary中的關(guān)鍵源碼處理流程我們就分析完了,剩下的就是blockcanary-android模塊中的通知及相關(guān)頁面的處理,感興趣的同學(xué)請自行了解。