引子
在復雜的項目環(huán)境中袁辈,由于歷史代碼龐大挨措,業(yè)務復雜汗洒,包含各種第三方庫议纯,所以在出現(xiàn)了卡頓的時候,很難定位到底是哪里出現(xiàn)了問題溢谤,即便知道是哪一個Activity/Fragment瞻凤,動輒數(shù)千行的類再加上跳來跳去調來調去的憨攒,結果就是不了了之隨它去了。
事實上阀参,很多情況下卡頓不是必現(xiàn)的肝集,它們可能與機型、環(huán)境蛛壳、操作等有關杏瞻,存在偶然性,即使發(fā)生了衙荐,再去查那如山般的logcat捞挥,也不一定能找到卡頓的原因。
BlockCanary就是來解決這個問題的忧吟。告別打點和調試砌函,哪里卡頓,一目了然溜族。
一讹俊、介紹
BlockCanary是一個Android平臺的一個非侵入式的性能監(jiān)控組件,應用只需要實現(xiàn)一個抽象類斩祭,提供一些該組件需要的上下文環(huán)境劣像,就可以在平時使用應用的時候檢測主線程上的各種卡慢問題,并通過組件提供的各種信息分析出原因并進行修復摧玫。官方地址:Android Performance Monitor。
BlockCanary對主線程操作進行了完全透明的監(jiān)控绑青,并能輸出有效的信息诬像,幫助開發(fā)分析、定位到問題所在闸婴,迅速優(yōu)化應用坏挠。其特點有:
- 非侵入式,簡單的兩行就打開監(jiān)控邪乍,不需要到處打點降狠,破壞代碼優(yōu)雅性。
- 精準庇楞,輸出的信息可以幫助定位到問題所在(精確到行)榜配,不需要像Logcat一樣,慢慢去找吕晌。
目前包括了核心監(jiān)控輸出文件蛋褥,以及UI顯示卡頓信息功能。
目前的問題:由于需要獲取CPU的信息睛驳,而在API 26(Android O)以后烙心,除非系統(tǒng)級應用膜廊,普通應用無法獲取 /proc/stat目錄下的信息,導致這個插件幾乎失效淫茵,不過不妨礙我們進行學習爪瓜。
二、實用方法
2.1 引入
dependencies {
compile 'com.github.markzhai:blockcanary-android:1.5.0'
// 僅在debug包啟用BlockCanary進行卡頓監(jiān)控和提示的話匙瘪,可以這么用
debugCompile 'com.github.markzhai:blockcanary-android:1.5.0'
releaseCompile 'com.github.markzhai:blockcanary-no-op:1.5.0'
}
2.2 使用
在Application中:
public class DemoApplication extends Application {
@Override
public void onCreate() {
// 在主進程初始化調用哈
BlockCanary.install(this, new AppBlockCanaryContext()).start();
}
}
繼承BlockCanaryContext實現(xiàn)自己的AppBlockCanaryContext :
public class AppBlockCanaryContext extends BlockCanaryContext {
// 實現(xiàn)各種上下文铆铆,包括應用標示符,用戶uid辆苔,網(wǎng)絡類型算灸,卡慢判斷闕值,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) {
}
}
三驻啤、原理
可翻看筆者前一篇文章:安卓中的消息循環(huán)模型
利用Android中的消息處理機制菲驴,在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;
}
}
即整個應用的主線程,只有這一個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);
}
...
}
}
mLogging在每個message處理的前后被調用贼涩,而如果主線程卡住了巧涧,就是在dispatchMessage里卡住了。
核心流程圖(圖源作者博客):
BlockCanary啟動一個線程負責保存UI線程當前堆棧信息遥倦,將堆棧信息以及CPU信息保存分別保存在 mThreadStackEntries和mCpuInfoEntries中谤绳,每條信息都以時間撮為key保存。
BlockCanary注冊了logging來獲取事件開始結束時間袒哥。如果檢測到事件處理時間超過閾值(默認值1s)缩筛,則從mThreadStackEntries中查找T1T2這段時間內(nèi)的堆棧信息,并且從mCpuInfoEntries中查找T1T2這段時間內(nèi)的CPU及內(nèi)存信息堡称。并且將信息格式化后保存到本地文件瞎抛,并且通知用戶。
該組件利用了主線程的消息隊列處理機制却紧,通過
Looper.getMainLooper().setMessageLogging(mainLooperPrinter);
并在mainLooperPrinter
中判斷start和end桐臊,來獲取主線程dispatch該message的開始和結束時間,并判定該時間超過閾值(如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;
}
...
四挺物、源碼解讀
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()方法, 記錄
Application
和BlockCanaryContext
, 為后面的處理提供上下文Context和配置參數(shù)(例如: 卡頓閾值,是否顯示通知 等等...) - 調用setEnabled()方法, 判斷桌面是否顯示黃色的logo圖標
- 調用get()方法, 創(chuàng)建BlockCanary的實例,并且創(chuàng)建BlockCanaryInternals實例, 賦值給mBlockCanaryCore屬性, 用來處理后面的流程
static void init(Context context, BlockCanaryContext blockCanaryContext) {
sApplicationContext = context;
sInstance = blockCanaryContext;
}
這個init方法就做了一個賦值的操作,將我們傳遞過來的context進行賦值。
我們繼續(xù)看BlockCanary.start()做了什么事:
public void start() {
if (!mMonitorStarted) {
mMonitorStarted = true;
Looper.getMainLooper().setMessageLogging(mBlockCanaryCore.monitor);
}
}
start()方法只做了一件事: 給Looper設置一個Printer
那么當Looper處理消息的前后, 就會調用mBlockCanaryCore.monitor的println()方法砚著。
mBlockCanaryCore.monitor是BlockCanaryInternals的成員屬性LooperMonitor
class LooperMonitor implements Printer {
...
@Override
public void println(String x) {
//如果StopWhenDebugging, 就不檢測
if (mStopWhenDebugging && Debug.isDebuggerConnected()) {
return;
}
if (!mPrintingStarted) {
mStartTimestamp = System.currentTimeMillis();
mStartThreadTimestamp = SystemClock.currentThreadTimeMillis();
mPrintingStarted = true;
startDump(); //在子線程中獲取調用棧和CPU信息
} else {
final long endTime = System.currentTimeMillis();
mPrintingStarted = false;
if (isBlock(endTime)) { //判斷是否超過設置的閾值
notifyBlockEvent(endTime);
}
stopDump(); //停止獲取調用棧和CPU信息
}
}
//判斷是否超過設置的閾值
private boolean isBlock(long endTime) {
return endTime - mStartTimestamp > mBlockThresholdMillis;
}
...
}
LooperMonitor的println()就是最核心的地方, 實現(xiàn)代碼也很簡單:
- Looper處理消息前, 獲取當前時間并且保存, 調用startDump()啟動一個任務定時去采集 調用棧/CPU 等等信息
- Looper處理消息完成, 獲取當前時間, 判斷是否超過我們自定義的閾值isBlock(endTime)如果超過了, 就調用notifyBlockEvent(endTime)來通知處理后面的流程
- 調用stopDump()停止獲取調用棧以及CPU的任務
startDump采集的信息包括:
- 基本信息:機型, CPU內(nèi)核數(shù), 進程名, 內(nèi)存, 版本號 等等
- 耗時信息:實際耗時, 主線程時鐘耗時, 卡頓開始時間和結束時間
- CPU信息:時間段內(nèi)CPU是否忙, 時間段內(nèi)的系統(tǒng)CPU/應用CPU占比, I/O占- - CPU使用率
- 堆棧信息:發(fā)生卡頓前的最近堆棧
五次伶、總結
blockcanary完美利用了安卓上的消息機制,給Looper設置一個Printer稽穆,通過記錄堆棧和CPU信息冠王,計算主線程處理消息的時間,如果超過了閾值舌镶,就檢索此時的堆棧和cpu信息來幫助分析卡頓原因柱彻。