在移動APP性能評測-流暢度評測中,我們介紹了如何準確客觀評價APP的流暢度,最終采用SM指標來評價應用的流暢度,在知道如何評價流暢度之后,我們應該如何來檢測出APP中的UI卡頓就是我們面臨的一個新的問題;在Android性能優(yōu)化-App卡頓中介紹了Google官方提供的檢測卡頓的方法,除此之外還有那邊比較好的方法來檢測應用卡頓?目前主流的方法主要有:
1.利用UI線程Looper打印的日志,典型代表就是BlockCanary;
2.采用Choreographer;
BlockCanary:blockcanary是國內(nèi)開發(fā)者MarkZhai開發(fā)的一套性能監(jiān)控組件,它對主線程操作進行了完全透明的監(jiān)控,并能輸出有效的信息,幫助開發(fā)分析、定位到問題所在,迅速優(yōu)化應用;
BlockCanary核心原理:通過自定義一個Printer,設(shè)置到主線程ActivityThread的MainLooper中。MainLooper在dispatch消息前后都會調(diào)用Printer進行打印。從而獲取前后執(zhí)行的時間差值,判斷是否超過設(shè)置的閾值。如果超過,則會將記錄的棧信息及cpu信息發(fā)通知到前臺。和利用UI線程Looper打印日志原理一樣;
下面通過Blockcanary來簡單介紹它是如何來檢測應用卡頓的,然后簡單介紹通過Choreographer來檢測應用卡頓;
Blockcanary檢測APP卡頓
GitHub地址:BlockCanary
Blog in Chinese: BlockCanary.
blockcanary源碼學習隨筆
BlockCanary原理圖如下圖所示:

其中最核心的兩步是在調(diào)用msg.target.dispatchMessage(msg),進行消息的分發(fā)前記錄時間T1,調(diào)用msg.target.dispatchMessage(msg)進行消息分發(fā)后記錄時間T2,如果T2-T1大于設(shè)置的卡頓閾值就會打印當前方法調(diào)用堆棧以及顯示其他相關(guān)提示或打印日志;
blockcanary充分的利用了Loop的機制,在MainLooper的loop方法中執(zhí)行dispatchMessage前后都會執(zhí)行printer的println進行輸出,并且提供了方法設(shè)置printer。通過分析前后打印的時差與閾值進行比對,從而判定是否卡頓。下面我們來看一下Looper中的loop方法;
Looper.java
public static void loop() {
// 獲取一個Looper對象
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
// 獲取Looper中的消息隊列
final MessageQueue queue = me.mQueue;
// 死循環(huán),對消息隊列里面的消息進行遍歷
for (;;) {
// 通過queue.next()取出消息,消息是在Handler.sendMessage方法中存到消息隊列里的
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
final Printer logging = me.mLogging;
if (logging != null) {
//用戶設(shè)置自己的Printer,在消息分發(fā)前調(diào)用Printer打印相關(guān)信息,此時獲取消息分發(fā)前的時間T1;
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
final long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;
final long traceTag = me.mTraceTag;
if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
}
final long start = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
final long end;
try {
// 調(diào)用msg.target.dispatchMessage(msg),進行消息的分發(fā)。這里的msg.target就是發(fā)送這條消息的Handler對象。
// 這樣Handler發(fā)送的消息最終又交回到它的dispatchMessage方法來處理。不同的是,Handler的dispatchMessage
// 方法是在創(chuàng)建Handler時所使用的Looper中執(zhí)行的,這樣就成功將代碼邏輯切換到指定線程中去執(zhí)行了。
msg.target.dispatchMessage(msg);
end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
} finally {
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}
if (logging != null) {
//消息分發(fā)完成后,調(diào)用用戶自己設(shè)置的Printer.println()方法,此時獲取消息分發(fā)之后時間T2;
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
}
}
/**
* Control logging of messages as they are processed by this Looper. If
* enabled, a log message will be written to <var>printer</var>
* at the beginning and ending of each message dispatch, identifying the
* target Handler and message contents.
*
* @param printer A Printer object that will receive log messages, or
* null to disable message logging.
* 用戶可以設(shè)置自己的Printer,這樣在知道消息分發(fā)前后的時間,
* 通過前后的時差與閾值進行對比,從而確定是否發(fā)生了卡頓
*/
public void setMessageLogging(@Nullable Printer printer) {
mLogging = printer;
}
通過設(shè)置Printer我們可以檢測msg.target.dispatchMessage(msg)執(zhí)行時間,這樣就可以知道部分UI線程是否有耗時操作了。
BlockCanary的LooperMonitor的println方法如下:
LooperMonitor
@Override
public void println(String x) {
if (!mPrintingStarted) {
//dispatchMesage前執(zhí)行的println
//記錄開始時間
mStartTimestamp = System.currentTimeMillis();
mStartThreadTimestamp = SystemClock.currentThreadTimeMillis();
mPrintingStarted = true;
//開始采集棧及cpu信息,最終會調(diào)用Stacksampler.start()方法;
startDump();
} else {
//dispatchMesage后執(zhí)行的println
//獲取結(jié)束時間
final long endTime = System.currentTimeMillis();
mPrintingStarted = false;
//判斷耗時是否超過閾值
if (isBlock(endTime)) {
notifyBlockEvent(endTime);
}
//最終會調(diào)用Stacksampler.stop()方法;
stopDump();
}
}
//判斷是否超過閾值
private boolean isBlock(long endTime) {
return endTime - mStartTimestamp > mBlockThresholdMillis;
}
//回調(diào)監(jiān)聽
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);
}
});
}
其中在startDump方法最終會調(diào)用Stacksampler.start()方法;stopDump最終會調(diào)用Stacksampler.stop()方法;相關(guān)方法如下:
Stacksampler
public void start() {
//在mRunable進行信息采集;
HandlerThreadFactory.getTimerThreadHandler().removeCallbacks(mRunnable);
//通過一個HandlerThread延時執(zhí)行了mRunnable
HandlerThreadFactory.getTimerThreadHandler().postDelayed(mRunnable,
BlockCanaryInternals.getInstance().getSampleDelay());
}
public void stop() {
//取消handler消息,如果未超時就不會采集相關(guān)信息
HandlerThreadFactory.getTimerThreadHandler().removeCallbacks(mRunnable);
}
在開始進行msg.target.dispatchMessage(msg)消息分發(fā)前通過HandlerThread發(fā)送一個延時runable,在msg.target.dispatchMessage(msg)消息分發(fā)后會remove該runable,如果指定的時間消息分發(fā)沒有完成,說明應用發(fā)生了卡頓,這之后開始執(zhí)行mRunable,在mRunable進行相關(guān)信息采集及提示APP發(fā)生卡頓;以上就是BlockCanary監(jiān)測卡頓的核心原理;
利用Choreographer監(jiān)測APP卡頓
Android系統(tǒng)每隔16ms發(fā)出VSYNC信號,觸發(fā)對UI進行渲染。開發(fā)者可以使用Choreographer#postFrameCallback設(shè)置自己的callback與Choreographer交互,你設(shè)置的FrameCallCack(doFrame方法)會在下一個frame被渲染時觸發(fā)。理論上來說兩次回調(diào)的時間周期應該在16ms,如果超過了16ms我們則認為發(fā)生了卡頓,我們主要就是利用兩次回調(diào)間的時間周期來判斷,
Choreographer.getInstance()
.postFrameCallback(new Choreographer.FrameCallback() {
@Override
public void doFrame(long l) {
//移除消息
Handler.removeMessage();
//發(fā)送延時消息
Hnadler.sendMessageAtTime(...)
Choreographer.getInstance().postFrameCallback(this);
}
});
發(fā)送的延時消息在執(zhí)行的時間沒有被remove掉,說明發(fā)生了卡頓,這時候可以進行卡頓相關(guān)信息的采集,如果在渲染下一幀的時候該消息還沒有被處理,這時候?qū)⒃撓emove掉,此場景說明未發(fā)生卡頓;該檢測卡頓的思想和BlockCanary類似;
最后,我們可以結(jié)合上述原理以及自己需求開發(fā)出一個適合自己的卡頓監(jiān)測方案,也可以參考已有開源方案。
其它
為什么主線程Looper.loop進行消息分發(fā)耗時就代表APP卡頓?
答:為了保證應用的平滑性,每一幀渲染時間不能超過16ms,達到60幀每秒;如果UI渲染慢的話,就會發(fā)生丟幀,這樣用戶就會感覺到不連貫性,我們稱之為Jank(APP卡頓);VSync信號由SurfaceFlinger實現(xiàn)并定時發(fā)送(每16ms發(fā)送),Choreographer.FrameDisplayEventReceiver收到信號后,調(diào)用onVsync方法組織消息發(fā)送到主線程處理。Choreographer主要功能是當收到VSync信號時,去調(diào)用使用通過postCallBack設(shè)置的回調(diào)函數(shù),在postCallBack調(diào)用doFrame,在doFrame中渲染下一幀;FrameDisplayEventReceiver相關(guān)代碼如下:
Choreographer.java
/**
* FrameDisplayEventReceiver繼承自DisplayEventReceiver接收底層的VSync信號開始處理UI過程。
* VSync信號由SurfaceFlinger實現(xiàn)并定時發(fā)送。FrameDisplayEventReceiver收到信號后,
* 調(diào)用onVsync方法組織消息發(fā)送到主線程處理。這個消息主要內(nèi)容就是run方法里面的doFrame了,
* 這里mTimestampNanos是信號到來的時間參數(shù)。
*/
private final class FrameDisplayEventReceiver extends DisplayEventReceiver
implements Runnable {
private boolean mHavePendingVsync;
private long mTimestampNanos;
private int mFrame;
public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
super(looper, vsyncSource);
}
@Override
public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
mTimestampNanos = timestampNanos;
mFrame = frame;
// 發(fā)送Runnable(callback參數(shù)即當前對象FrameDisplayEventReceiver)到FrameHandler,請求執(zhí)行doFrame
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true);
// 此處mHandler為FrameHandler,該Handler對應的Looper是主線程的Looper
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}
@Override
public void run() {
mHavePendingVsync = false;
doFrame(mTimestampNanos, mFrame);
}
}
在mHandler.sendMessageAtTime發(fā)送消息之后,最終會在主線程的Looper.loop()方法中調(diào)用msg.target.dispatchMessage(msg);Looper.loop相關(guān)代碼可以參考在本文上邊進行查看;然后在Handler.dispatchMeassange分發(fā)消息,如下所示:
Handler.java
public void dispatchMessage(Message msg) {
// Message的callback實際上就是Handler的post方法所傳遞的Runnable參數(shù)
// 這里首先檢查是否有由Runnable封裝的消息,如果有,首先處理;
if (msg.callback != null) {
handleCallback(msg);
} else {
// 其次處理mCallback
if (mCallback != null) {
// 如果mCallback的handleMessage方法返回true,那么handler中的handleMessage方法是不會被執(zhí)行的
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
private static void handleCallback(Message message) {
//在此執(zhí)行FrameDisplayEventReceiver中的run方法,最終執(zhí)行doFrame渲染下一幀;
message.callback.run();
}
通過以上流程可以發(fā)現(xiàn),Android渲染每一幀都是通過消息機制來實現(xiàn)的,最終都會在主線Looper.loop()方法中開始渲染下一幀,因為Looper.loop方法在進行消息分發(fā)時是串行執(zhí)行的,這樣如果上一個消息分發(fā)時間過長即msg.target.dispatchMessage(msg)執(zhí)行時間過長,就會導致在VSYNC到來時進行下一幀渲染延遲執(zhí)行,就不能保證該幀在16ms內(nèi)完成渲染,從而導致丟幀;所以主線程Looper.loop方法中msg.target.dispatchMessage(msg)執(zhí)行時間過長就會導致APP卡頓;因此通過檢測msg.target.dispatchMessage(msg)執(zhí)行時間就可以檢測APP卡頓;
Android消息機制的重要性:
1.在卡頓監(jiān)測會用到消息機制;主要是發(fā)送一個延時消息來監(jiān)測是否,在執(zhí)行時間內(nèi)沒有remove該消息就代碼APP發(fā)生卡頓;
2.ANR監(jiān)測也是通過發(fā)送一個延時消息來監(jiān)測是否發(fā)生ANR;ANR是APP卡頓的極端情況;
3.View監(jiān)測事件是否長按也用到消息機制,在發(fā)生Down的時候會發(fā)送一個延時消息,在Up的時候會將該消息Remove掉,如果指定的時間沒有發(fā)生UP就會觸發(fā)長按事件;
4.Choreographer在渲染每一幀的時候也是通過發(fā)送一個消息,然后在Looper.loop中處理下一個消息時才會去渲染下一幀;
5.Activity生命周期的控制也是在ActivityThread發(fā)送不同的消息來切換Activity生命周期;
6.消息機制可以將一個任務(wù)切換到其它指定的線程,如AsyncTask;
以上這些場景都用到Android消息機制,還有很多其他未知的場景可能也會用到Android消息機制,所以消息機制在Android中具有很重要的地位;
參考資料
鴻洋:Android UI性能優(yōu)化 檢測應用中的UI卡頓
BlockCanary GitHub地址
Blog in Chinese: BlockCanary.
blockcanary源碼學習隨筆
Android Choreographer 源碼分析 講的很好很重要