卡頓監(jiān)控

App層面監(jiān)控卡頓
需要準(zhǔn)確分析卡頓發(fā)生在什么函數(shù),資源占用情況如何,目前業(yè)界兩種主流有效的app監(jiān)控方式如下:
1、 利用UI線程的Looper打印的日志匹配;
2、 使用Choreographer.FrameCallback

Looper日志檢測卡頓

Android主線程更新UI。如果界面1秒鐘刷新少于60次,即FPS小于60,用戶就會產(chǎn)生卡頓感覺。簡單來說,
Android使用消息機(jī)制進(jìn)行UI更新,UI線程有個Looper,在其loop方法中會不斷取出message,調(diào)用其綁定的
Handler在UI線程執(zhí)行。如果在handler的dispatchMesaage方法里有耗時操作,就會發(fā)生卡頓。

public void setMessageLogging(@Nullable Printer printer) {
        mLogging = printer;
    }
public static void loop() {
//......
for (;;) {
//......
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.target.dispatchMessage(msg) 的執(zhí)行時間,就能檢測到部分UI線程是否有耗時的操作。注意到這行
執(zhí)行代碼的前后,有兩個logging.println函數(shù),如果設(shè)置了logging,會分別打印出>>>>> Dispatching to和
<<<<< Finished to 這樣的日志,這樣我們就可以通過兩次log的時間差值,來計算dispatchMessage的執(zhí)行時
間,從而設(shè)置閾值判斷是否發(fā)生了卡頓。
系統(tǒng)中的打印源碼

  
public final class Looper {
private Printer mLogging;
public void setMessageLogging(@Nullable Printer printer) {
mLogging = printer;
}
}
public interface Printer {
void println(String x);
}

Looper 提供了 setMessageLogging(@Nullable Printer printer) 方法,所以我們可以自己實現(xiàn)一個Printer,在
通過setMessageLogging()方法傳入即可:

public class BlockCanary {
public static void install() {
LogMonitor logMonitor = new LogMonitor();
Looper.getMainLooper().setMessageLogging(logMonitor);
}
}
public class LogMonitor implements Printer {
private StackSampler mStackSampler;
private boolean mPrintingStarted = false;
private long mStartTimestamp;
// 卡頓閾值
private long mBlockThresholdMillis = 3000;
//采樣頻率
private long mSampleInterval = 1000;
private Handler mLogHandler;
public LogMonitor() {
mStackSampler = new StackSampler(mSampleInterval);
HandlerThread handlerThread = new HandlerThread("block-canary-io");
handlerThread.start();
mLogHandler = new Handler(handlerThread.getLooper());
}
@Override
public void println(String x) {
//從if到else會執(zhí)行 dispatchMessage,如果執(zhí)行耗時超過閾值,輸出卡頓信息
if (!mPrintingStarted) {
//記錄開始時間
mStartTimestamp = System.currentTimeMillis();
mPrintingStarted = true;
mStackSampler.startDump();
} else {
final long endTime = System.currentTimeMillis();
mPrintingStarted = false;
//出現(xiàn)卡頓
if (isBlock(endTime)) {
notifyBlockEvent(endTime);
}
mStackSampler.stopDump();
}
}
private void notifyBlockEvent(final long endTime) {
mLogHandler.post(new Runnable() {
@Override
public void run() {
//獲得卡頓時 主線程堆棧
享學(xué)課堂
List<String> stacks = mStackSampler.getStacks(mStartTimestamp, endTime);
for (String stack : stacks) {
Log.e("block-canary", stack);
}
}
});
}
private boolean isBlock(long endTime) {
return endTime - mStartTimestamp > mBlockThresholdMillis;
}
}
public class StackSampler {
public static final String SEPARATOR = "\r\n";
public static final SimpleDateFormat TIME_FORMATTER =
new SimpleDateFormat("MM-dd HH:mm:ss.SSS");
private Handler mHandler;
private Map<Long, String> mStackMap = new LinkedHashMap<>();
private int mMaxCount = 100;
private long mSampleInterval;
//是否需要采樣
protected AtomicBoolean mShouldSample = new AtomicBoolean(false);
public StackSampler(long sampleInterval) {
mSampleInterval = sampleInterval;
HandlerThread handlerThread = new HandlerThread("block-canary-sampler");
handlerThread.start();
mHandler = new Handler(handlerThread.getLooper());
}
/**
* 開始采樣 執(zhí)行堆棧
*/
public void startDump() {
//避免重復(fù)開始
if (mShouldSample.get()) {
return;
}
mShouldSample.set(true);
mHandler.removeCallbacks(mRunnable);
mHandler.postDelayed(mRunnable, mSampleInterval);
}
public void stopDump() {
if (!mShouldSample.get()) {
return;

其實這種方式也就是 BlockCanary 原理。

使用Choreographer.FrameCallback檢測屏幕幀數(shù)

Android系統(tǒng)每隔16ms發(fā)出VSYNC信號,來通知界面進(jìn)行重繪、渲染,每一次同步的周期約為16.6ms,代表一幀
的刷新頻率。通過Choreographer類設(shè)置它的FrameCallback函數(shù),當(dāng)每一幀被渲染時會觸發(fā)回調(diào)
FrameCallback.doFrame (long frameTimeNanos) 函數(shù)。frameTimeNanos是底層VSYNC信號到達(dá)的時間戳 。

public class ChoreographerHelper {
public static void start() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
long lastFrameTimeNanos = 0;
@Override
public void doFrame(long frameTimeNanos) {
//上次回調(diào)時間
if (lastFrameTimeNanos == 0) {
lastFrameTimeNanos = frameTimeNanos;
Choreographer.getInstance().postFrameCallback(this);
return;
}
long diff = (frameTimeNanos - lastFrameTimeNanos) / 1_000_000;
if (diff > 16.6f) {
//掉幀數(shù)
int droppedCount = (int) (diff / 16.6);
}
lastFrameTimeNanos = frameTimeNanos;
Choreographer.getInstance().postFrameCallback(this);
}
});
}
}
}

通過 ChoreographerHelper 可以實時計算幀率和掉幀數(shù),實時監(jiān)測App頁面的幀率數(shù)據(jù),發(fā)現(xiàn)幀率過低,還可以自
動保存現(xiàn)場堆棧信息。
Looper比較適合在發(fā)布前進(jìn)行測試或者小范圍灰度測試然后定位問題,ChoreographerHelper適合監(jiān)控線上環(huán)境
的 app 的掉幀情況來計算 app 在某些場景的流暢度然后有針對性的做性能優(yōu)化。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容