卡頓檢測是個至關大的話題,檢測場景小到本機測試、自動化測試、本地監控,大到線上抽樣採集上報。卡頓緣由也千差萬別,跟CPU、內存、I/O可能都有關。本系列文章旨在經過一些經常使用的本地卡頓檢測工具來定位卡頓緣由,並分析其底層實現原理。若是想自研一些APM工具這些原理必須掌握。html
談到卡頓首先想到的就是BlockCanary,它以其簡單易用的特色被普遍用於檢測全局的卡頓狀況,咱們有必要首先了解一下它內部的原理。本篇先來看看BlockCanary項目傳送門戳這裏。java
com.github.markzhai:blockcanary-android:1.5.0
複製代碼
咱們知道Android Framework 不少業務都是經過消息機制完成的,包括UI繪製更新、四大組件生命週期、ANR檢查等等。android
消息機制給咱們一個啓發,咱們能夠監測主線程消息處理的狀況來追蹤卡頓問題。以UI渲染爲例,主線程Choreographer(Android 4.1及之後)每16ms請求一個vsync信號,當信號到來時觸發doFrame操做,它內部又依次進行了input、Animation、Traversal過程(具體流程分析參考好文Android Choreographer 源碼分析),而這些都是經過消息機制驅動的。git
BlockCanary檢測的原理也是基於主線程消息的處理流程。既然要檢測主線程消息處理狀況,那先要清楚主線程Looper對象的建立。github
# -> ActivityThread
public static void main(String[] args) {
...
Looper.prepareMainLooper();
ActivityThread thread = new ActivityThread();
thread.attach(false);
if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
}
if (false) {
Looper.myLooper().setMessageLogging(new
LogPrinter(Log.DEBUG, "ActivityThread"));
}
...
Looper.loop();
throw new RuntimeException("Main thread loop unexpectedly exited");
}
複製代碼
ActivityThread的main函數是Android程序的入口,它並非一個線程類,它運行在主線程中。能夠看到經過prepareMainLooper和loop函數使主線程的looper跑起來了。shell
再看loop方法bash
# -> Looper.java
public static void loop() {
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
final MessageQueue queue = me.mQueue;
...
for (;;) {
//從消息隊列中取出一條消息,沒有消息則休眠
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
// 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);
}
...
msg.recycleUnchecked();
}
}
複製代碼
這裏先留一個問題loop函數內部使用了死循環,主線程爲何不會卡死?爲何不會觸發ANR?文末有參考文章。網絡
dispatchMessage函數會對消息進行分發,並交由對應的runnable或handler處理,因此監控主線程的卡頓問題實際上就是監控dispatchMessage函數的耗時狀況。app
能夠看到在dispatchMessage先後各有一次logging的打印,而且調用println方法的logging對象還能夠經過setMessageLogging方法設置,也就是說Looper內部自己就提供了hook點。ide
# -> Looper.java
public void setMessageLogging(@Nullable Printer printer) {
mLogging = printer;
}
複製代碼
咱們能夠自定義一個Printer並複寫其println函數來實現卡頓的監控。事實上,BlockCanary就是這麼作的。監控到卡頓點後,dump函數調用堆棧並獲取CPU運行狀況,即可綜合分析卡頓的緣由。
來看看BlockCanary初始化的方法install和start。
# -> BlockCanary.java
/**
* 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();
}
複製代碼
# -> BlockCanary.java
public void start() {
if (!mMonitorStarted) {
mMonitorStarted = true;
//設置自定義printer
Looper.getMainLooper().setMessageLogging(mBlockCanaryCore.monitor);
}
}
複製代碼
這裏的mBlockCanaryCore.monitor就是LooperMonitor對象,它實現了Printer接口。 咱們重點看一下它的println方法。
# -> LooperMonitor.java
public void println(String x) {
if (mStopWhenDebugging && Debug.isDebuggerConnected()) {
return;
}
if (!mPrintingStarted) {
//dispatchMessage前一次打印進入這裏
mStartTimestamp = System.currentTimeMillis();
mStartThreadTimestamp = SystemClock.currentThreadTimeMillis();
mPrintingStarted = true;
//開始dump信息
startDump();
} else {
//dispatchMessage後一次打印進入這裏
final long endTime = System.currentTimeMillis();
mPrintingStarted = false;
//判斷是否發生卡頓
if (isBlock(endTime)) {
//存儲dump下來的信息並通知
notifyBlockEvent(endTime);
}
//中止dump
stopDump();
}
}
複製代碼
主線已經清楚,咱們先大體看一下BlockCanary運行的核心流程把握全局。
再來看startDump和stopDump# -> LooperMonitor.java
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();
}
}
複製代碼
可見內部有一個調用堆棧採樣器和cpu採樣器。 這裏有一點須要注意:採樣開始的時間點爲0.8*卡頓閾值。爲何不在卡頓閾值那個點採樣呢?這裏實際上是一種容錯處理。 假設當前函數調用及實際耗時狀況以下,卡頓閾值設置爲220。
fun foo () {
a()//函數耗時200
b()//函數耗時20
c()//函數耗時10
}
複製代碼
可見致使卡頓的罪魁禍首應該是函數a,但若是在卡頓閾值220纔開始dump調用堆棧,有可能捕獲到的卡頓堆棧爲foo() -> b()或c(),設置0.8倍的預採樣點就是爲了下降這種狀況出現的概率。咱們悲觀的認爲當前已超過80%卡頓閾值的函數就是致使卡頓的主因。
回到採樣流程來,首先看stackSampler是如何採樣的。
# -> StackSampler.java
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());
}
}
複製代碼
很簡單,就是獲取當前線程的堆棧信息,並保存在一個LinkedHashMap對象sStackMap中。
再來看cpuSampler的處理
# -> CpuSampler
@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 {
//release resource
...
}
}
複製代碼
這裏是依據Linux系統cpu的統計方式,Linux系統會將cpu信息和當前進程信息分別存放在/proc/stat和/proc/pid/stat文件中,具體統計原理參看Linux平臺Cpu使用率的計算。
經過CPU的使用狀況能夠大體瞭解系統的運行狀況,CPU若是處於高負載狀態,多是在作CPU密集型計算。若是CPU負載正常,可能處於IO密集狀態。
當信息都採集完成後咱們回到主線代碼。
# -> LooperMonitor
@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();
}
}
//判斷是否發生了卡頓
private boolean isBlock(long endTime) {
return endTime - mStartTimestamp > mBlockThresholdMillis;
}
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);
}
});
}
複製代碼
這裏須要注意的是對於threadTime的統計,它經過函數SystemClock.currentThreadTimeMillis()
獲取,它反映的是線程處於running狀態下的時間,這裏須要一張Thread運行狀態圖。
因此好比經過調用thread.sleep方式致使卡頓時並不會統計到threadTime中的。也就是說threadTime反映的是線程真正運行的時間,中間好比鎖的獲取、cpu的調度及其餘非running狀態等狀況不計算在內。
onBlockEvent的實如今BlockCanary建立之初。
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();
}
複製代碼
mInterceptorChain目前註冊了兩個回調,一個是DisplayService,它收到block消息會發送通知。另外一個是BlockCanaryContext,咱們能夠經過自定義BlockCanaryContext並複寫onBlock方法作額外的處理,好比上報網絡。
# -> BlockCanaryContext
/**
* Block interceptor, developer may provide their own actions.
*/
@Override
public void onBlock(Context context, BlockInfo blockInfo) {
}
複製代碼
接下來就能夠經過通知消息查看卡頓的具體信息。電視端若是屏蔽了通知欄,可在應用列表中找到入口,若是應用列表入口也被系統屏蔽,可直接使用adb命令打開。
adb shell am start <packageName>/com.github.moduth.blockcanary.ui.DisplayActivity
複製代碼