這次疫情還沒有過去,但是生活依舊,還是要工作,還是要掙錢,在這裏多的話不說,“武漢加油!”閒言少敘,我們直接走入主題,我們在開發複雜項目的時候,代碼的迭代,修改等,都會出現UI卡頓,或者出現ANR的時候,造成的程序崩潰,等,我們如何定位到卡頓的位置等,所以國內開發者,給我送來一個福利,BlockCanary這個框架。
1.介紹 BlockCanary
BlockCanary 這個框架是android平臺,非侵入式的性能監控組件。使用時提供一個抽象類,傳一個上下文環境就可以使用了,使用方便.
2.UI卡頓問題
原理:
在android開發中,我們的APP的幀頻性能最優的目標就是保持在60fps上。
60fps --->16ms/幀
所以我們儘量保證每次在16ms內處理完所有的CPU與GPU計算,繪製,渲染等操作,否則會造成丟幀卡頓問題。
3.UI卡頓的原因分析
1.UI線程中做耗時操作
1)主線程的作用:把事件分發給合適的view或者widget
解決辦法:我們通過handler在子線程中做耗時操作
runOnUiThread方法:
View.post 方法
VIew.postDelayed方法
2.佈局layout過於複雜,沒辦法在16ms中完成渲染
3.View的過度繪製,由於過度繪製導致在同一幀重複繪製
4.view頻繁的觸發measure,layout
5.內存頻繁觸發GC過多(在同一幀內頻繁的創建臨時變量)
4.BlockCanary的簡單實用
1)添加開源庫的依賴:
compile 'com.github.markzhai:blockcanary-android:1.5.0'
2)在Application中註冊我們的BlockCanary
BlockCanary.install(this, new AppBlockContext()).start();
3).創建一個類AppBlockContext 繼承 BlockCanaryContext:
public class AppBlockContext extends BlockCanaryContext {
// 實現各種上下文,包括應用標示符,用戶uid,網絡類型,卡慢判斷闕值,Log保存位置等
/**
* Implement in your project.
*
* @return Qualifier which can specify this installation, like version + flavor.
*/
public String provideQualifier() {
return "unknown";
}
/**
* Implement in your project.
*
* @return user id
*/
public String provideUid() {
return "uid";
}
/**
* Network type
*
* @return {@link String} like 2G, 3G, 4G, wifi, etc.
*/
public String provideNetworkType() {
return "unknown";
}
/**
* Config monitor duration, after this time BlockCanary will stop, use
* with {@code BlockCanary}'s isMonitorDurationEnd
*
* @return monitor last duration (in hour)
*/
public int provideMonitorDuration() {
return -1;
}
/**
* Config block threshold (in millis), dispatch over this duration is regarded as a BLOCK. You may set it
* from performance of device.
*
* @return threshold in mills
*/
public int provideBlockThreshold() {
return 1000;
}
/**
* Thread stack dump interval, use when block happens, BlockCanary will dump on main thread
* stack according to current sample cycle.
* <p>
* Because the implementation mechanism of Looper, real dump interval would be longer than
* the period specified here (especially when cpu is busier).
* </p>
*
* @return dump interval (in millis)
*/
public int provideDumpInterval() {
return provideBlockThreshold();
}
/**
* Path to save log, like "/blockcanary/", will save to sdcard if can.
*
* @return path of log files
*/
public String providePath() {
return "/blockcanary/";
}
/**
* If need notification to notice block.
*
* @return true if need, else if not need.
*/
public boolean displayNotification() {
return true;
}
/**
* Implement in your project, bundle files into a zip file.
*
* @param src files before compress
* @param dest files compressed
* @return true if compression is successful
*/
public boolean zip(File[] src, File dest) {
return false;
}
/**
* Implement in your project, bundled log files.
*
* @param zippedFile zipped file
*/
public void upload(File zippedFile) {
throw new UnsupportedOperationException();
}
/**
* Packages that developer concern, by default it uses process name,
* put high priority one in pre-order.
*
* @return null if simply concern only package with process name.
*/
public List<String> concernPackages() {
return null;
}
/**
* Filter stack without any in concern package, used with @{code concernPackages}.
*
* @return true if filter, false it not.
*/
public boolean filterNonConcernStack() {
return false;
}
/**
* Provide white list, entry in white list will not be shown in ui list.
*
* @return return null if you don't need white-list filter.
*/
public List<String> provideWhiteList() {
LinkedList<String> whiteList = new LinkedList<>();
whiteList.add("org.chromium");
return whiteList;
}
/**
* Whether to delete files whose stack is in white list, used with white-list.
*
* @return true if delete, false it not.
*/
public boolean deleteFilesInWhiteList() {
return true;
}
/**
* Block interceptor, developer may provide their own actions.
*/
public void onBlock(Context context, BlockInfo blockInfo) {
}
}
5.BlockCanary的原理源碼實現:
在ActivityThread中有一個main方法,在main方法中會創建一個Looper,在Looper當中會關聯一個MessageQueue消息隊列,主線程創建好MainLooper之後,他就會在應用的生命週期內不斷的輪訓,通過Looper.loop方法。然後獲取到我們消息隊列當中的message,最後通知我們的主線程去更新UI.
2)實現的核心原理
通過Hander.postMessage發送一個消息給主線程(sMainLooper.loop),主線程會通過輪訓器Looper不斷的輪訓MessageQueue中的消息隊列,通過queue.next方法獲取消息隊列中的消息,然後我們計算出調用dispatchMessage方法的前後時間值(T1,T2),通過T2減去T1的時間差來判斷是否超過我們之前設定好的閾值,如果超過了我們設定的閾值,我們就dump出我們收集的信息,來定位我們UI卡頓的原因。
如果我們在調用dispatchmessage這個方法的時候,超過我們設定的閾值的0.8倍的時候,也會Dump出我們需要的信息。
3)我們通過源碼進行分析
BlockCanary.install(this, new AppBlockContext()).start();
首先我們看看他的入口,install這個方法,我們點開:
/**
* 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();
}
這裏調用三行代碼.我們接着點init方法:
static void init(Context context, BlockCanaryContext blockCanaryContext) {
sApplicationContext = context;
sInstance = blockCanaryContext;
}
這個init方法就做了一個賦值的操作,將我們傳遞過來的context進行賦值。
我們返回到install方法中。看setEnabled方法:
更據用戶的通知欄消息,來開啓或者關閉展示我們BlockCanary這個消息界面.
我們看這個方法的第三個參數,dispalyNotification這個方法就是決定開啓或者關閉:
/**
* If need notification to notice block.
*
* @return true if need, else if not need.
*/
public boolean displayNotification() {
return true;
}
這裏默認返回的true。如果是debug方法是true,relese返回false
我們在看看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;
}
他其實就是一個單例模式,我們看看這個BlockCanary是如何實現的那?
private BlockCanary() {
BlockCanaryInternals.setContext(BlockCanaryContext.get());
mBlockCanaryCore = BlockCanaryInternals.getInstance();
mBlockCanaryCore.addBlockInterceptor(BlockCanaryContext.get());
if (!BlockCanaryContext.get().displayNotification()) {
return;
}
mBlockCanaryCore.addBlockInterceptor(new DisplayService());
}
BlockCanaryInternals.setContext,做了一個賦值操作,
mBlockCanaryCode這個類型的變量就是BlockCanaryInternals,我們看一下getInstance方法:
/**
* Get BlockCanaryInternals singleton
*
* @return BlockCanaryInternals instance
*/
static BlockCanaryInternals getInstance() {
if (sInstance == null) {
synchronized (BlockCanaryInternals.class) {
if (sInstance == null) {
sInstance = new BlockCanaryInternals();
}
}
}
return sInstance;
}
一個單例模式完成了BlockCanaryInternals的實例化。
我們接着看addBlockInterceptor這行代碼:
這是一個攔截器,傳入一個上下文
主要代碼,判斷是否開啓,展開這個攔截器,通知我們的DisplayActivity。
接下來我們看一看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();
}
我們看一下這個構造方法,和重要的三個變量:
1)stackSampleer
參數一:傳入我們的主線程
參數二:Dump的間隔時間
2) cpuSampler
他會dump出我們cup的一些情況
3)LooperMonitor
這是一個非常重要的東西,如何打印上下時間(T1,T2),就是通過它控制的,然後通過onBlockEvent回調監聽並打印數據。
4)cleanObsolete這個方法就是刪除我們打印的日誌
上邊講解的是BlockCanary的install初始化的,接下來我們講解start()的打印是如何打印的:
/**
* Start monitoring.
*/
public void start() {
if (!mMonitorStarted) {
mMonitorStarted = true;
Looper.getMainLooper().setMessageLogging(mBlockCanaryCore.monitor);
}
}
最關鍵的代碼:就是Looper.getMainLooper這行代碼:調取主線程的setMessageLogging方法,來打點我們時間。
我們接下來看看代碼Monitor是如何實現的:
我們點擊LooperMonitor繼續查看:
這個類實現了Printer這個接口。
這個類中重要的方法是:
@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();
}
}
這個方法就是用來打點時間的,
首先看代碼實現:
首先判斷dispacthMessage這個方法之前調用的,如果是就會記錄開始時間,調用這個startDump這個方法,來打印出我們的堆棧信息。
接下來我們看看這個startDump這個方法的實現:
private void startDump() {
if (null != BlockCanaryInternals.getInstance().stackSampler) {
BlockCanaryInternals.getInstance().stackSampler.start();
}
if (null != BlockCanaryInternals.getInstance().cpuSampler) {
BlockCanaryInternals.getInstance().cpuSampler.start();
}
}
這個方法主要就是通過BlockCanaryInternals中的stackSampler和cpuSampler分別打印出重要信息。
我們繼續深入,看看他們的start方法實現:
public void start() {
if (mShouldSample.get()) {
return;
}
mShouldSample.set(true);
HandlerThreadFactory.getTimerThreadHandler().removeCallbacks(mRunnable);
HandlerThreadFactory.getTimerThreadHandler().postDelayed(mRunnable,
BlockCanaryInternals.getInstance().getSampleDelay());
}
這裏沒啥說的,主要看看postDelayed這個方法的第一個參數,mRunnalbe:
private Runnable mRunnable = new Runnable() {
@Override
public void run() {
doSample();
if (mShouldSample.get()) {
HandlerThreadFactory.getTimerThreadHandler()
.postDelayed(mRunnable, mSampleInterval);
}
}
};
我們在看看doSample是什麼?
abstract void doSample();
這是一個抽象方法,這也就是意味着stackSampler和cpuSampler是有不同實現的,
我們接着看看這個抽象方法的StackSampler的實現:
@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());
}
}
到這裏就是真正要打印的數據了:
我們看最後一行代碼:執行了打印,第一個參數是以我們的當前時間戳爲例,並放到HashMap當中,我們看看是什麼HashMap?
private static final LinkedHashMap<Long, String> sStackMap = new LinkedHashMap<>();
他是一個linkHashMap: 爲什麼要用這個HashMap,因爲這個LinkHashMap能夠記錄插入的順序。
所以這裏是按着先後順序插入的,
我們回到這Printer這個接口的println方法:
@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();
}
}
我們看看這個isBlock這個方法:
private boolean isBlock(long endTime) {
return endTime - mStartTimestamp > mBlockThresholdMillis;
}
到這裏我們就明白了,這裏是不是就是我們BlockCanary的核心原理,T2減去T1的時間,並判斷是否打印並返回true,就會執行notifyBlockEvent,我們看看這個實現:
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);
}
});
}
我們看看這個方法中的LooperMonitor,this這個代碼,onBlockEvent是不是就是我們前面的監聽時間回調?
到這裏是不是就把我們要打印的數據通過這個回調方法返回去了.所以到這裏我們的分析就全部完成了。如果對您有幫助,麻煩關注我一下,我會給你繼續帶來乾貨。