BlockCanary
BlockCanary是markzhai同學的一個很實用的作品, 能檢測到主線程的卡頓, 并將結果記錄下來, 以友好的方式展示, 實屬性能監(jiān)測的良品, 他重用了LeakCanary的UI展示, 其它與LeakCanary的關系并不是太大.
BlockCanary使用
BlockCanary使用方式也比較簡單, 要在Application中進行設置一下就可以了:
BlockCanary.install(this, new AppBlockCanaryContext()).start();
其中的AppBlockCanaryContext繼承自BlockCanaryContext, 是對BlockCanary中各個參數進行配置的類, 我們可以重寫其中的getXXX()方法來對其進行配置. 其中或配置的參數有很多, 看名字就可以知道了:
//卡頓閥值
int getConfigBlockThreshold();
boolean isNeedDisplay();
String getQualifier();
String getUid();
String getNetworkType();
Context getContext();
String getLogPath();
boolean zipLogFile(File[] src, File dest);
//可將卡頓日志上傳到自己的服務
void uploadLogFile(File zippedFile);
String getStackFoldPrefix();
int getConfigDumpIntervalMillis();
然后就可以進行操作了, 在某個消息執(zhí)行時間超過設定的標準時會彈出通知進行提醒.
BlockCanary分析
BlockCanary的檢測原理, 作者已經進行了說明, 就是在主線程消息循環(huán)打出日志的地入手, 當一個消息操作時間超過閥值后, 記錄系統各種資源的狀態(tài), 并展示出來. 下面從代碼的層面學習一下.
還是從Application的install開始分析,
public static BlockCanary install(Context context, BlockCanaryContext blockCanaryContext) {
BlockCanaryContext.init(context, blockCanaryContext);
setEnabled(context, DisplayBlockActivity.class, BlockCanaryContext.get().isNeedDisplay());
return get();
}
其中的get()
方法是BlockCanary作為單例向外提供拿實例的方法, 在第一次取實例的時候構造對象, 構造時創(chuàng)建一個BlockCanaryCore類型的成員, 也是類似單例模式, 在構造BlockCanaryCore的時候進行了初始化工作:
public BlockCanaryCore() {
threadStackSampler = new ThreadStackSampler(Looper.getMainLooper().getThread(),
sBlockCanaryContext.getConfigDumpIntervalMillis());
cpuSampler = new CpuSampler(sBlockCanaryContext.getConfigDumpIntervalMillis());
setMainLooperPrinter(new LooperPrinter(new BlockListener() {
@Override
public void onBlockEvent(long realTimeStart, long realTimeEnd,
long threadTimeStart, long threadTimeEnd) {
// Get recent thread-stack entries and cpu usage
ArrayList<String> threadStackEntries = threadStackSampler
.getThreadStackEntries(realTimeStart, realTimeEnd);
// Log.d("BlockCanary", "threadStackEntries: " + threadStackEntries.size());
if (!threadStackEntries.isEmpty()) {
Block block = Block.newInstance()
.setMainThreadTimeCost(realTimeStart, realTimeEnd, threadTimeStart, threadTimeEnd)
.setCpuBusyFlag(cpuSampler.isCpuBusy(realTimeStart, realTimeEnd))
.setRecentCpuRate(cpuSampler.getCpuRateInfo())
.setThreadStackEntries(threadStackEntries)
.flushString();
LogWriter.saveLooperLog(block.toString());
if (getContext().isNeedDisplay() && mOnBlockEventInterceptor != null) {
mOnBlockEventInterceptor.onBlockEvent(getContext().getContext(), block.timeStart);
}
}
}
}, getContext().getConfigBlockThreshold()));
LogWriter.cleanOldFiles();
}
其中創(chuàng)建兩個真正去拿系統狀態(tài)的對象:threadStackSampler和cpuSampler.
創(chuàng)建一個LooperPrinter的對象設置給系統, 被系統當作記錄日志Printer使用.
系統會在執(zhí)行每個消息的前后都會調用Printer的println方法來記錄日志, 下面看看LoopPrinter中是怎么寫的.
public void println(String x) {
if (!mStartedPrinting) {
mStartTimeMillis = System.currentTimeMillis();
mStartThreadTimeMillis = SystemClock.currentThreadTimeMillis();
mStartedPrinting = true;
startDump();
} else {
final long endTime = System.currentTimeMillis();
mStartedPrinting = false;
if (isBlock(endTime)) {
notifyBlockEvent(endTime);
}
stopDump();
}
}
從消息執(zhí)行開始startDump()
, 開始記錄系統的各種資源狀態(tài), 在下次執(zhí)行時, 也就是消息執(zhí)行結束時判斷是否達到了卡頓的閥值, 如果達到通知監(jiān)聽(在構造時傳入)已經卡頓,同時停止記錄系統資源狀態(tài). 其中的開始記錄和結束記錄都是直接調用了構造BlockCanaryCore時創(chuàng)建的兩個監(jiān)視資源的成員.
卡頓的監(jiān)聽也是在BlockCanaryCore構造時創(chuàng)建的, 他在收到卡頓通知后取得資源監(jiān)視器中存儲的數據, 存儲到外存, 最后通知mOnBlockEventInterceptor, 而這個成員是什么時候傳入的呢? 答案是在構造BlockCanary的時候, 先得到BlockCanaryCore實例, 然后就是初始化通知. 這里是通過反射取得Notifier的實例, 并將之設置為BlockCanaryCore中的mOnBlockEventInterceptor成員. 在Notifier中就直接彈出通知, 展示數據了.
下面看看兩個資源監(jiān)控的兩個對象是怎么工作的, ThreadStackSampler
protected void doSample() {
// Log.d("BlockCanary", "sample thread stack: [" + mThreadStackEntries.size() + ", " + mMaxEntryCount + "]");
StringBuilder stringBuilder = new StringBuilder();
// Fetch thread stack info
for (StackTraceElement stackTraceElement : mThread.getStackTrace()) {
stringBuilder.append(stackTraceElement.toString())
.append(Block.SEPARATOR);
}
// Eliminate obsolete entry
synchronized (mThreadStackEntries) {
if (mThreadStackEntries.size() == mMaxEntryCount && mMaxEntryCount > 0) {
mThreadStackEntries.remove(mThreadStackEntries.keySet().iterator().next());
}
mThreadStackEntries.put(System.currentTimeMillis(), stringBuilder.toString());
}
}
直接去拿主線程的棧信息, 每半秒去拿一次, 記錄下來, 如果發(fā)生卡頓就顯之顯示出來
拿CPU的信息較麻煩, 從/proc/stat
下面拿實時的CPU狀態(tài), 再從/proc/" + mPid + "/stat
中讀取進程時間, 再計算各CPU時間占比和CPU的工作狀態(tài). 下面兩張圖是BlockCanary的Demo中得到的實例, 看起來和LeakCanary挺像, 其實數據完全不一樣
尾巴
有聽說UC通過在Handler中放入idel消息來檢測每條消息的耗時, 相比之下這種方式要容易理解得多, 還聽說這個項目已經運用在多個實際項目中, 別等了, 快點加入吧, 哪怕是在自己本地跑一下也能對自己系統的卡頓有個客觀的了解.