一.概述
在做 UI 性能優(yōu)化的時候,很重要的一點就是需要做好優(yōu)化前和優(yōu)化后的對比,否則怎么判斷自己的優(yōu)化是否有效果,效果有多大呢?對比的話,個人認為可以分為兩類,一類是通過觀察直觀的對比,另一類是通過數(shù)據(jù)客觀的對比
直觀對比,我們可以通過 開發(fā)者選項 中的 過渡繪制 和 GPU 分析 查看頁面中是否存在過渡繪制和 UI 渲染時的 GPU 渲染圖
客觀對比,我們可以通過各種測試工具分析、對比,比如網(wǎng)易的 Emmagee 、騰訊的 matrix 等等,很遺憾的是 Emmagee 在 Android 7.0 以上由于系統(tǒng)限制無法使用,所以我使用的是 matrix
做軟件開發(fā),不僅要知其然,而且要知其所以然,這樣才能有更大的進步。我在做 UI 性能優(yōu)化的時候,使用了 matrix 中的 TraceCanary 模塊,它是功能主要是檢測 UI 卡頓、啟動耗時、頁面切換和慢函數(shù)檢測,這里主要分析一下其中的幀率是怎么計算的,其他功能的代碼應(yīng)該也不是很復雜,有興趣的朋友可以自行分析
二.原理
從一開始做 Android 應(yīng)用開發(fā)的時候,就經(jīng)常聽說不能在主線程中做耗時的操作(比如:IO 數(shù)據(jù)讀取、網(wǎng)絡(luò)請求等等),會造成 UI 卡頓等問題,因為主線程每 16.67 ms 就會渲染一幀,如果在主線程中有耗時的任務(wù),可能在 16.67 ms 內(nèi)無法渲染下一幀就會造成掉幀,而掉幀從視覺上來講就是通常所說的卡頓了。雖然造成掉幀的原因有很多種,但是有兩個變量是固定的:主線程和 16.67 ms,我們就可以通過這兩個固定值入手,分析、判斷主線程中是否存在卡頓
了解過 Android 性能優(yōu)化的朋友可能都知道,業(yè)內(nèi)主流的實現(xiàn)卡頓監(jiān)控的思想,都是通過監(jiān)控主線程中任務(wù)的耗時情況,如果任務(wù)耗時超過一定的閾值,則 dump 當前主線程的堆棧信息,就可以分析卡頓原因了,這兩種思想的典型代表有 ArgusAPM、BlockCanary、BlockCanaryEx 等。思想是這樣的,實現(xiàn)的方式可以歸為兩類,一類是通過主線程的 Looper 機制,另一類是通過 Choreographer 模塊,下面先簡單介紹下這兩種方式
2.1 Looper 機制
大家都知道在主線程中有一個 MainLooper,主線程啟動以后就會調(diào)用 MainLooper#loop() 方法,主線程中執(zhí)行的任務(wù)都會間接地在 ActivityThread 中的一個內(nèi)部類 H(它是一個 Handler 的子類)的 handleMessage(Message msg) 來執(zhí)行,我們分析一下 Looper.loop() 方法如下
- 代碼 1:
Looper.loop()方法中會有個for(;;)死循環(huán)不斷地從MessageQueue中讀取消息并處理 - 代碼 3:最終調(diào)用了
msg.target.dispatchMessage(msg)來處理此 Message,而msg.target其實就是 Handler,它是指在 MainLooper 對應(yīng)的所有的 Handler - 代碼 2 & 代碼 4:在處理消息前后,都會通過
myLooper().mLogging得到的Printer對象,分別打印>>>>> Dispatching to和<<<<< Finished to -
myLooper().mLogging是可以通過Looper.getMainLooper().setMessageLogging(Printer printer)設(shè)置的
所以我們只需要判斷 >>>>> Dispatching to 和 <<<<< Finished to 這兩個 log 之間的時間是否超出閾值,就可以判定此時主線程中執(zhí)行的任務(wù)是否是耗時任務(wù)
public final class Looper {
......
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;
// Make sure the identity of this thread is that of the local process,
// and keep track of what that identity token actually is.
Binder.clearCallingIdentity();
final long ident = Binder.clearCallingIdentity();
for (;;) { // 代碼 1
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; // 代碼 2
if (logging != null) {
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 {
msg.target.dispatchMessage(msg); // 代碼 3
end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
} finally {
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}
if (logging != null) { // 代碼 4
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
......
}
}
......
}
示意代碼如下
Looper.getMainLooper().setMessageLogging(new Printer() {
private static final String START = ">>>>> Dispatching";
private static final String END = "<<<<< Finished";
@Override
public void println(String x) {
if (x.startsWith(">>>>> Dispatching")) {
...... // 開始記錄
}
if (x.startsWith("<<<<< Finished")) {
...... // 結(jié)束記錄,判斷處理時間間隔是否超過閾值
}
}
});
2.2 Choreographer 模塊
Android 系統(tǒng)每隔 16.67 ms 都會發(fā)送一個 VSYNC 信號觸發(fā) UI 的渲染,正常情況下兩個 VSYNC 信號之間是 16.67 ms ,如果超過 16.67 ms 則可以認為渲染發(fā)生了卡頓
Android SDK 中的 Choreographer 類中的 FrameCallback.doFrame(long l) 在每次發(fā)送 VSYNC 信號的時候都會被回調(diào),所以我們只需要判斷相鄰的兩次 FrameCallback.doFrame(long l) 間隔是否超過閾值,如果超過閾值則發(fā)生了卡頓,則可以在另外一個子線程中 dump 當前主線程的堆棧信息進行分析
示意代碼如下所示,需要注意的是,在 FrameCallback.doFrame(long l) 回調(diào)方法中,我們每次都需要重新注冊回調(diào) Choreographer.getInstance().postFrameCallback(this)
Choreographer.getInstance()
.postFrameCallback(new Choreographer.FrameCallback() {
@Override
public void doFrame(long l) {
if(frameTimeNanos - mLastFrameNanos > 100) {
...
}
mLastFrameNanos = frameTimeNanos;
Choreographer.getInstance().postFrameCallback(this);
}
});
2.3 Matrix-TraceCanary 的實現(xiàn)方式
判斷 UI 中是否存在卡頓大概可以分為以上兩種實現(xiàn)方式
在 Matrix 的 TraceCanary 模塊中,舊版本使用的是 Choreographer
的方式,但是新版本中使用的是 Looper 機制了,至于為什么會有這種差異,暫時還不清楚
不過使用 Looper 機制實現(xiàn)的方式存在一個缺點:在打印消息處理開始日志和消息處理結(jié)束日志的時候,都會進行字符串的拼接,頻繁地進行字符串的拼接也會影響性能

三.源碼解析
這里分析 Matrix 和 TraceCanary 是 0.5.2 版本
既然已經(jīng)知道了它的實現(xiàn)原理,我們就直搗黃龍,去尋找和 Looper.getMainLooper().setMessageLogging() 相關(guān)的代碼,順藤摸瓜,這樣順著分析會更清晰一些。
Matrix 和 TraceCanary 的目錄結(jié)構(gòu)如下圖所示

3.1 向 Looper 添加 Printer
上圖中被選中的 LooperMonitor 類就是我們?nèi)胧值念?,其中通過調(diào)用 Looper.getMainLooper().setMessageLogging() 方法添加了一個 Printer 對象,代碼如下所示
代碼 1:
LooperMonitor的構(gòu)造方法是私有的,且有一個靜態(tài)的 final 對象LooperMonitor monitor代碼 2 & 代碼 3:
LooperMonitor實現(xiàn)了MessageQueue.IdleHandler接口及其抽象方法queueIdle(),并且在構(gòu)造方法中根據(jù)版本差異使用不同的方法將此MessageQueue.IdleHandler實例對象添加到MessageQueue中,queueIdle()方法返回 true 保證消息隊列中的每個消息處理完以后,都會回調(diào)queueIdle()方法-
代碼 4:在構(gòu)造方法和
queueIdle()方法中會調(diào)用resetPrinter()方法,其中才是真正通過Looper.getMainLooper().setMessageLogging()設(shè)置 Printer 的地方在每次添加前,都會通過反射得到當前
Looper.getMainLooper()設(shè)置的mLogging對象,并判斷是不是之前被 LooperMonitor 設(shè)置的,如果Looper.getMainLooper()中的mLogging對象是被 LooperMonitor 設(shè)置的,則不會再次設(shè)置,否則會為Looper.getMainLooper()設(shè)置自己的 Printer 對象 代碼 5:在
Printer.println(String x)中,會根據(jù)傳入的參數(shù)String x的第一個字符判斷是否是有效的Looper.loop()日志,以>開頭的則是消息處理開始的日志,以>開頭的則是消息處理結(jié)束的日志,最終會調(diào)用dispatch(boolean isBegin)方法
public class LooperMonitor implements MessageQueue.IdleHandler {
private static final HashSet<LooperDispatchListener> listeners = new HashSet<>();
private static Printer printer;
private static final LooperMonitor monitor = new LooperMonitor(); // 代碼 1
private LooperMonitor() { // 代碼 2
resetPrinter();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Looper.getMainLooper().getQueue().addIdleHandler(this);
} else {
MessageQueue queue = reflectObject(Looper.getMainLooper(), "mQueue");
queue.addIdleHandler(this);
}
}
@Override
public boolean queueIdle() { // 代碼 3
resetPrinter();
return true;
}
private static void resetPrinter() { // 代碼 4
final Printer originPrinter = reflectObject(Looper.getMainLooper(), "mLogging");
if (originPrinter == printer && null != printer) {
return;
}
if (null != printer) {
MatrixLog.w(TAG, "[resetPrinter] maybe looper printer was replace other!");
}
Looper.getMainLooper().setMessageLogging(printer = new Printer() { // 代碼 5
boolean isHasChecked = false;
boolean isValid = false;
@Override
public void println(String x) {
if (null != originPrinter) {
originPrinter.println(x);
}
if (!isHasChecked) {
isValid = x.charAt(0) == '>' || x.charAt(0) == '<';
isHasChecked = true;
if (!isValid) {
MatrixLog.e(TAG, "[println] Printer is inValid! x:%s", x);
}
}
if (isValid) {
dispatch(x.charAt(0) == '>');
}
}
});
}
}
已經(jīng)監(jiān)聽到了 Looper.loop() 消息處理的開始和結(jié)束,接下來看下在消息處理開始和結(jié)束時是怎么處理的,主要看 dispatch(boolean isBegin) 方法
- 在
dispatch(boolean isBegin)中,會依次處理添加到 LooperMonitor 中的 LooperDispatchListener 監(jiān)聽回調(diào) - 代碼 1:
dispatch(boolean isBegin)方法中會根據(jù)參數(shù)boolean isBegin、listener.isValid()以及listener.isHasDispatchStart條件執(zhí)行listener.dispatchStart()和listener.dispatchEnd()方法,具體的在消息處理開始和結(jié)束時做什么,都在listener.dispatchStart()和listener.dispatchEnd()中實現(xiàn) - 代碼 2:這行代碼,個人理解的就是要站好最后一班崗的意思,當
listener.isValid() == false && isBegin == false && listener.isHasDispatchStart == true時最后調(diào)用一次listener.dispatchEnd()方法 - 代碼 3:可以通過
LooperMonitor.register(LooperDispatchListener listener)方法向 LooperMonitor 中設(shè)置LooperDispatchListener listener,接下來我們看下在哪兒調(diào)用這個方法設(shè)置LooperDispatchListener listener監(jiān)聽的,就可以看到消息處理開始和結(jié)束時都有哪些具體的邏輯了
public class LooperMonitor implements MessageQueue.IdleHandler {
private static final HashSet<LooperDispatchListener> listeners = new HashSet<>();
......
public abstract static class LooperDispatchListener {
boolean isHasDispatchStart = false;
boolean isValid() {
return false;
}
@CallSuper
void dispatchStart() {
this.isHasDispatchStart = true;
}
@CallSuper
void dispatchEnd() {
this.isHasDispatchStart = false;
}
}
......
public static void register(LooperDispatchListener listener) { // 代碼 3
synchronized (listeners) {
listeners.add(listener);
}
}
private static void dispatch(boolean isBegin) {
for (LooperDispatchListener listener : listeners) {
if (listener.isValid()) { // 代碼 1
if (isBegin) {
if (!listener.isHasDispatchStart) {
listener.dispatchStart();
}
} else {
if (listener.isHasDispatchStart) {
listener.dispatchEnd();
}
}
} else if (!isBegin && listener.isHasDispatchStart) { // 代碼 2
listener.dispatchEnd();
}
}
}
......
}
3.2 消息處理
如下圖所示,在 trace-canary 模塊中有 AppMethodBeat 和 UIThreadMonitor 兩個類通過 LooperMonitor.register(LooperDispatchListener listener) 方法向 LooperMonitor 中設(shè)置 LooperDispatchListener listener,和本文相關(guān)的是 UIThreadMonitor 類,重點分析下這個類

首先分析下 UIThreadMonitor 類的初始化方法,其中主要是通過反射拿到了 Choreographer 一些屬性,并且通過 LooperMonitor.register(LooperDispatchListener listener) 方法向 LooperMonitor 中設(shè)置 LooperDispatchListener listener
- 代碼 1:通過反射拿到了 Choreographer 實例的
mCallbackQueues屬性,mCallbackQueues是一個回調(diào)隊列數(shù)組CallbackQueue[] mCallbackQueues,其中包括四個回調(diào)隊列,第一個是輸入事件回調(diào)隊列CALLBACK_INPUT = 0,第二個是動畫回調(diào)隊列CALLBACK_ANIMATION = 1,第三個是遍歷繪制回調(diào)隊列CALLBACK_TRAVERSAL = 2,第四個是提交回調(diào)隊列CALLBACK_COMMIT = 3。這四個階段在每一幀的 UI 渲染中是依次執(zhí)行的,每一幀中各個階段開始時都會回調(diào)mCallbackQueues中對應(yīng)的回調(diào)隊列的回調(diào)方法。關(guān)于 Choreographer 的文章推薦這篇 Android Choreographer 源碼分析 - 代碼 2:通過反射拿到輸入事件回調(diào)隊列的
addCallbackLocked方法 - 代碼 3:通過反射拿到動畫回調(diào)隊列的
addCallbackLocked方法 - 代碼 4:通過反射拿到遍歷繪制回調(diào)隊列的
addCallbackLocked方法 - 代碼 5:通過
LooperMonitor.register(LooperDispatchListener listener)方法向 LooperMonitor 中設(shè)置LooperDispatchListener listener - 代碼 6:在 Looper.loop() 中的消息處理開始時的回調(diào)
- 代碼 7:在 Looper.loop() 中的消息處理結(jié)束時的回調(diào)
public class UIThreadMonitor implements BeatLifecycle, Runnable {
private static final String ADD_CALLBACK = "addCallbackLocked";
public static final int CALLBACK_INPUT = 0;
public static final int CALLBACK_ANIMATION = 1;
public static final int CALLBACK_TRAVERSAL = 2;
private final static UIThreadMonitor sInstance = new UIThreadMonitor();
private Object callbackQueueLock;
private Object[] callbackQueues;
private Method addTraversalQueue;
private Method addInputQueue;
private Method addAnimationQueue;
private Choreographer choreographer;
private long frameIntervalNanos = 16666666;
......
public static UIThreadMonitor getMonitor() {
return sInstance;
}
public void init(TraceConfig config) {
if (Thread.currentThread() != Looper.getMainLooper().getThread()) {
throw new AssertionError("must be init in main thread!");
}
this.isInit = true;
this.config = config;
choreographer = Choreographer.getInstance();
callbackQueueLock = reflectObject(choreographer, "mLock");
callbackQueues = reflectObject(choreographer, "mCallbackQueues"); // 代碼 1
addInputQueue = reflectChoreographerMethod(callbackQueues[CALLBACK_INPUT], ADD_CALLBACK, long.class, Object.class, Object.class); // 代碼 2
addAnimationQueue = reflectChoreographerMethod(callbackQueues[CALLBACK_ANIMATION], ADD_CALLBACK, long.class, Object.class, Object.class); // 代碼 3
addTraversalQueue = reflectChoreographerMethod(callbackQueues[CALLBACK_TRAVERSAL], ADD_CALLBACK, long.class, Object.class, Object.class); // 代碼 4
frameIntervalNanos = reflectObject(choreographer, "mFrameIntervalNanos");
LooperMonitor.register(new LooperMonitor.LooperDispatchListener() { // 代碼 5
@Override
public boolean isValid() {
return isAlive;
}
@Override
public void dispatchStart() {
super.dispatchStart();
UIThreadMonitor.this.dispatchBegin(); // 代碼 6
}
@Override
public void dispatchEnd() {
super.dispatchEnd();
UIThreadMonitor.this.dispatchEnd(); // 代碼 7
}
});
......
}
}
在 3.1 節(jié)就講過,在 Looper 處理消息開始和結(jié)束時,分別會回調(diào) LooperDispatchListener 的 dispatchStart() 和 dispatchEnd() 方法,也就是如上代碼所示的 代碼6 和 代碼7 處
dispatchStart() 方法相對簡單,其中調(diào)用了 UIThreadMonitor.this.dispatchBegin() 記錄兩個起始時間,一個是線程起始時間,另一個是 cpu 起始時間,并且依次回調(diào) LooperObserver#dispatchBegin() 方法,如下所示
public class UIThreadMonitor implements BeatLifecycle, Runnable {
private long[] dispatchTimeMs = new long[4];
private volatile long token = 0L;
......
private void dispatchBegin() {
token = dispatchTimeMs[0] = SystemClock.uptimeMillis();
dispatchTimeMs[2] = SystemClock.currentThreadTimeMillis();
AppMethodBeat.i(AppMethodBeat.METHOD_ID_DISPATCH);
synchronized (observers) {
for (LooperObserver observer : observers) {
if (!observer.isDispatchBegin()) {
observer.dispatchBegin(dispatchTimeMs[0], dispatchTimeMs[2], token);
}
}
}
}
......
}
dispatchEnd() 方法相對復雜一些,根據(jù)變量 isBelongFrame 判斷是否調(diào)用 doFrameEnd(long token),然后記錄線程結(jié)束時間和 cpu 結(jié)束時間,最終回調(diào) LooperObserver#dispatchEnd() 方法
- 代碼 1,在 UIThreadMonitor 中有兩個長度是三的數(shù)組
queueStatus和queueCost分別對應(yīng)著每一幀中輸入事件階段、動畫階段、遍歷繪制階段的狀態(tài)和耗時,queueStatus有三個值:DO_QUEUE_DEFAULT、DO_QUEUE_BEGIN 和 DO_QUEUE_END - 代碼 2,變量
isBelongFrame初始值是false,是在doFrameBegin(long token)方法中將其置位true的,doFrameBegin(long token)是在run()方法中被調(diào)用的,UIThreadMonitor實現(xiàn)了Runnable接口,自然重寫了run()方法,那么UIThreadMonitor對象是什么時候被線程執(zhí)行的呢?下面再分析 - 代碼 3,在消息處理結(jié)束時,會調(diào)用
dispatchEnd()方法,其中根據(jù)變量isBelongFrame判斷是否調(diào)用doFrameEnd(long token) - 代碼 4,在
run()方法中,首先會調(diào)用doFrameBegin(long token)將變量isBelongFrame設(shè)置為true,然后通過doQueueBegin()方法記錄輸入事件回調(diào)CALLBACK_INPUT開始的狀態(tài)和時間;之后又通過addFrameCallback()方法為Choreographer設(shè)置動畫回調(diào)CALLBACK_ANIMATION和遍歷繪制回調(diào)CALLBACK_TRAVERSAL的回調(diào)方法 - 代碼 5,在動畫回調(diào)
CALLBACK_ANIMATION的回調(diào)方法run()中,記錄CALLBACK_INPUT的結(jié)束的狀態(tài)和時間,同時也記錄CALLBACK_ANIMATION的開始狀態(tài)和時間 - 代碼 6,在遍歷繪制
CALLBACK_TRAVERSAL的回調(diào)方法run()中,記錄CALLBACK_ANIMATION的結(jié)束狀態(tài)和時間,同時也記錄CALLBACK_TRAVERSAL的開始狀態(tài)和時間 - 其實這里已經(jīng)可以猜測到
UIThreadMonitor實現(xiàn)Runnable接口,也是為了將UIThreadMonitor作為輸入事件回調(diào)CALLBACK_INPUT的回調(diào)方法,設(shè)置到Choreographer中去的
public class UIThreadMonitor implements BeatLifecycle, Runnable {
public static final int CALLBACK_INPUT = 0;
public static final int CALLBACK_ANIMATION = 1;
public static final int CALLBACK_TRAVERSAL = 2;
private static final int CALLBACK_LAST = CALLBACK_TRAVERSAL;
private boolean isBelongFrame = false;
private int[] queueStatus = new int[CALLBACK_LAST + 1]; // 代碼 1
private long[] queueCost = new long[CALLBACK_LAST + 1];
private static final int DO_QUEUE_DEDO_QUEUE_DEFAULT、FAULT = 0;
private static final int DO_QUEUE_BEGIN = 1;
private static final int DO_QUEUE_END = 2;
private void doFrameBegin(long token) { // 代碼 2
this.isBelongFrame = true;
}
private void dispatchEnd() { // 代碼 3
if (isBelongFrame) {
doFrameEnd(token);
}
dispatchTimeMs[3] = SystemClock.currentThreadTimeMillis();
dispatchTimeMs[1] = SystemClock.uptimeMillis();
AppMethodBeat.o(AppMethodBeat.METHOD_ID_DISPATCH);
synchronized (observers) {
for (LooperObserver observer : observers) {
if (observer.isDispatchBegin()) {
observer.dispatchEnd(dispatchTimeMs[0], dispatchTimeMs[2], dispatchTimeMs[1], dispatchTimeMs[3], token, isBelongFrame);
}
}
}
}
@Override
public void run() { // 代碼 4
final long start = System.nanoTime();
try {
doFrameBegin(token); // 將 isBelongFrame 置位 true
doQueueBegin(CALLBACK_INPUT); // 通過 doQueueBegin(int type) 記錄輸入事件回調(diào) CALLBACK_INPUT 開始狀態(tài)和時間
addFrameCallback(CALLBACK_ANIMATION, new Runnable() { // 通過 addFrameCallback() 方法向 Choreographer 中添加動畫 CALLBACK_ANIMATION 回調(diào)
// 每個回調(diào)其實都是一個 Runnable,執(zhí)行時會主動調(diào)用 `run()` 方法
@Override
public void run() { // 代碼 5
doQueueEnd(CALLBACK_INPUT); // 輸入事件回調(diào) CALLBACK_INPUT 結(jié)束
doQueueBegin(CALLBACK_ANIMATION); // 動畫回調(diào) CALLBACK_ANIMATION 開始
}
}, true);
addFrameCallback(CALLBACK_TRAVERSAL, new Runnable() { // 通過 addFrameCallback() 方法向 Choreographer 中添加遍歷繪制 CALLBACK_TRAVERSAL 回調(diào)
@Override
public void run() { // 代碼 6
doQueueEnd(CALLBACK_ANIMATION); // 動畫回調(diào) CALLBACK_ANIMATION 結(jié)束
doQueueBegin(CALLBACK_TRAVERSAL); // 遍歷繪制回調(diào) CALLBACK_TRAVERSAL 開始
}
}, true);
} finally {
if (config.isDevEnv()) {
MatrixLog.d(TAG, "[UIThreadMonitor#run] inner cost:%sns", System.nanoTime() - start);
}
}
}
private synchronized void addFrameCallback(int type, Runnable callback, boolean isAddHeader) {
if (callbackExist[type]) {
MatrixLog.w(TAG, "[addFrameCallback] this type %s callback has exist!", type);
return;
}
if (!isAlive && type == CALLBACK_INPUT) {
MatrixLog.w(TAG, "[addFrameCallback] UIThreadMonitor is not alive!");
return;
}
try {
synchronized (callbackQueueLock) {
Method method = null;
switch (type) {
case CALLBACK_INPUT:
method = addInputQueue;
break;
case CALLBACK_ANIMATION:
method = addAnimationQueue;
break;
case CALLBACK_TRAVERSAL:
method = addTraversalQueue;
break;
}
if (null != method) {
method.invoke(callbackQueues[type], !isAddHeader ? SystemClock.uptimeMillis() : -1, callback, null);
callbackExist[type] = true;
}
}
} catch (Exception e) {
MatrixLog.e(TAG, e.toString());
}
}
private void doQueueBegin(int type) {
queueStatus[type] = DO_QUEUE_BEGIN;
queueCost[type] = System.nanoTime();
}
private void doQueueEnd(int type) {
queueStatus[type] = DO_QUEUE_END;
queueCost[type] = System.nanoTime() - queueCost[type];
synchronized (callbackExist) {
callbackExist[type] = false;
}
}
......
}
接下來看下,是什么時候?qū)⑤斎胧录卣{(diào) CALLBACK_INPUT 設(shè)置進 Choreographer 中去的

如上圖所示,addFrameCallback() 方法除了添加動畫回調(diào) CALLBACK_ANIMATION 和遍歷繪制回調(diào) CALLBACK_TRAVERSAL,有兩處添加了 CALLBACK_INPUT 回調(diào),分別是下面兩處
- 代碼 1,首次調(diào)用
UIThreadMonitor#onStart()方法時會調(diào)用一次addFrameCallback(CALLBACK_INPUT, this, true)方法,將此Runnable作為輸入事件CALLBACK_INPUT的回調(diào)添加進 Choreographyer 中 - 代碼 2,在
doFrameEnd(long token)方法中,每一幀完成以后也會調(diào)用一次addFrameCallback(CALLBACK_INPUT, this, true)方法,再次添加以供下一幀回調(diào)這三個回調(diào)。doFrameEnd(long token)方法是在處理消息結(jié)束時的dispatchEnd()方法中回調(diào)的 - 代碼 3,在上面的代碼分析中,最后只調(diào)用了
doQueueBegin(CALLBACK_TRAVERSAL),在doFrameEnd(long token)方法中調(diào)用了doQueueEnd(CALLBACK_TRAVERSAL),完成了遍歷繪制回調(diào)CALLBACK_TRAVERSAL的監(jiān)聽 - 代碼 4,新創(chuàng)建一個長度為 3 的數(shù)組并賦值給
queueStatus,其實這里有一個疑問,doFrameEnd(long token)方法在每一幀繪制的時候都會被回調(diào),在這個方法中創(chuàng)建數(shù)組對象,會不會造成內(nèi)存抖動 - 代碼 5,遍歷
LooperObserver的 HashSet 對象,回調(diào)其doFrame(String focusedActivityName, long start, long end, long frameCostMs, long inputCostNs, long animationCostNs, long traversalCostNs)方法,其中會將輸入事件、動畫、遍歷繪制的耗時作為參數(shù)傳入
public class UIThreadMonitor implements BeatLifecycle, Runnable {
@Override
public synchronized void onStart() {
if (!isInit) {
throw new RuntimeException("never init!");
}
if (!isAlive) {
MatrixLog.i(TAG, "[onStart] %s", Utils.getStack());
this.isAlive = true;
addFrameCallback(CALLBACK_INPUT, this, true); // 代碼 1
}
}
private void doFrameEnd(long token) {
doQueueEnd(CALLBACK_TRAVERSAL); // 代碼 3
for (int i : queueStatus) {
if (i != DO_QUEUE_END) {
queueCost[i] = DO_QUEUE_END_ERROR;
if (config.isDevEnv) {
throw new RuntimeException(String.format("UIThreadMonitor happens type[%s] != DO_QUEUE_END", i));
}
}
}
queueStatus = new int[CALLBACK_LAST + 1]; // 代碼 4
long start = token;
long end = SystemClock.uptimeMillis();
synchronized (observers) { // 代碼 5
for (LooperObserver observer : observers) {
if (observer.isDispatchBegin()) {
observer.doFrame(AppMethodBeat.getFocusedActivity(), start, end, end - start, queueCost[CALLBACK_INPUT], queueCost[CALLBACK_ANIMATION], queueCost[CALLBACK_TRAVERSAL]);
}
}
}
addFrameCallback(CALLBACK_INPUT, this, true); // 代碼 2
this.isBelongFrame = false;
}
}
3.3 幀率計算
從上面代碼,可以看到最后會將每一幀的時間信息通過 HashSet<LooperObserver> observers 回調(diào)出去,看一下是在哪里向 observers 添加 LooperObserver 回調(diào)的,如下圖所示
這里主要看一下 FrameTracer 這個類,其中涉及到了幀率 FPS 的計算,其他的類有興趣的朋友可以自己分析

FrameTracer 的代碼如下所示,我相信仔細看看并不難懂,主要邏輯在 notifyListener(final String focusedActivityName, final long frameCostMs) 方法中,其中涉及到一個變量 frameIntervalMs = 16.67 ms
- 代碼 1:會遍歷
HashSet<IDoFrameListener> listeners,并根據(jù)doFrame()中傳入的long frameCost時間計算掉幀個數(shù),最后將frameCostMs和dropFrame通過IDoFrameListener接口中的兩個方法回調(diào)出去 - 掉幀個數(shù)是通過:
final int dropFrame = (int) (frameCostMs / frameIntervalMs)計算的,frameCostMs是這一幀繪制的耗時,frameIntervalMs其實就是 16.67ms
public class FrameTracer extends Tracer {
private final long frameIntervalMs;
private HashSet<IDoFrameListener> listeners = new HashSet<>();
public FrameTracer(TraceConfig config) {
this.frameIntervalMs = TimeUnit.MILLISECONDS.convert(UIThreadMonitor.getMonitor().getFrameIntervalNanos(), TimeUnit.NANOSECONDS) + 1;
......
}
public void addListener(IDoFrameListener listener) {
synchronized (listeners) {
listeners.add(listener);
}
}
public void removeListener(IDoFrameListener listener) {
synchronized (listeners) {
listeners.remove(listener);
}
}
@Override
public void onAlive() {
super.onAlive();
UIThreadMonitor.getMonitor().addObserver(this);
}
@Override
public void onDead() {
super.onDead();
UIThreadMonitor.getMonitor().removeObserver(this);
}
@Override
public void doFrame(String focusedActivityName, long start, long end, long frameCostMs, long inputCostNs, long animationCostNs, long traversalCostNs) {
notifyListener(focusedActivityName, frameCostMs);
}
private void notifyListener(final String focusedActivityName, final long frameCostMs) {
long start = System.currentTimeMillis();
try {
synchronized (listeners) {
for (final IDoFrameListener listener : listeners) {
final int dropFrame = (int) (frameCostMs / frameIntervalMs); // 代碼 1
listener.doFrameSync(focusedActivityName, frameCostMs, dropFrame);
if (null != listener.getHandler()) {
listener.getHandler().post(new Runnable() {
@Override
public void run() {
listener.doFrameAsync(focusedActivityName, frameCostMs, dropFrame);
}
});
}
}
}
} finally {
long cost = System.currentTimeMillis() - start;
if (config.isDevEnv()) {
MatrixLog.v(TAG, "[notifyListener] cost:%sms", cost);
}
if (cost > frameIntervalMs) {
MatrixLog.w(TAG, "[notifyListener] warm! maybe do heavy work in doFrameSync,but you can replace with doFrameAsync! cost:%sms", cost);
}
if (config.isDebug() && !isForeground()) {
backgroundFrameCount++;
}
}
}
}
看一下在哪些地方調(diào)用 addListener(IDoFrameListener listener) 向 FrameTracer 中添加了 IDoFrameListener,如下圖所示,主要有兩個地方:FrameDecorator 和 FrameTracer,這兩個地方的邏輯其實差不多,主要看一下 addListener(new FPSCollector()); 的

FPSCollector 是 FrameTracer 的一個內(nèi)部類,實現(xiàn)了 IDoFrameListener 接口,主要邏輯是在 doFrameAsync() 方法中
- 代碼 1:會根據(jù)當前 ActivityName 創(chuàng)建一個對應(yīng)的 FrameCollectItem 對象,存放在 HashMap 中
- 代碼 2:調(diào)用
FrameCollectItem#collect(),計算幀率 FPS 等一些信息 - 代碼 3:如果此 Activity 的繪制總時間超過 timeSliceMs(默認是 10s),則調(diào)用
FrameCollectItem#report()上報統(tǒng)計數(shù)據(jù),并從 HashMap 中移除當前 ActivityName 和對應(yīng)的 FrameCollectItem 對象
private class FPSCollector extends IDoFrameListener {
private Handler frameHandler = new Handler(MatrixHandlerThread.getDefaultHandlerThread().getLooper());
private HashMap<String, FrameCollectItem> map = new HashMap<>();
@Override
public Handler getHandler() {
return frameHandler;
}
@Override
public void doFrameAsync(String focusedActivityName, long frameCost, int droppedFrames) {
super.doFrameAsync(focusedActivityName, frameCost, droppedFrames);
if (Utils.isEmpty(focusedActivityName)) {
return;
}
FrameCollectItem item = map.get(focusedActivityName); // 代碼 1
if (null == item) {
item = new FrameCollectItem(focusedActivityName);
map.put(focusedActivityName, item);
}
item.collect(droppedFrames); // 代碼 2
if (item.sumFrameCost >= timeSliceMs) { // report // 代碼 3
map.remove(focusedActivityName);
item.report();
}
}
}
FrameCollectItem 也是 FrameTracer 的一個內(nèi)部類,其中最重要的是兩個方法 collect(int droppedFrames) 和 report() 方法
- 代碼 1:將每一幀的耗時相加,sumFrameCost 代表總耗時
- 代碼 2:sumDroppedFrames 統(tǒng)計總的掉幀個數(shù)
- 代碼 3:sumFrame 表示總幀數(shù)
- 代碼 4:根據(jù)掉幀的個數(shù),判斷此次掉幀行為屬于哪個程度區(qū)間并記錄其個數(shù)
- 代碼 5:根據(jù)
float fps = Math.min(60.f, 1000.f * sumFrame / sumFrameCost)計算 fps 值 - 代碼 6:將統(tǒng)計得到的信息封裝到一個 Issue 對象中,并通過
TracePlugin#onDetectIssue()方法回調(diào)出去
private class FrameCollectItem {
String focusedActivityName;
long sumFrameCost;
int sumFrame = 0;
int sumDroppedFrames;
// record the level of frames dropped each time
int[] dropLevel = new int[DropStatus.values().length];
int[] dropSum = new int[DropStatus.values().length];
FrameCollectItem(String focusedActivityName) {
this.focusedActivityName = focusedActivityName;
}
void collect(int droppedFrames) {
long frameIntervalCost = UIThreadMonitor.getMonitor().getFrameIntervalNanos();
sumFrameCost += (droppedFrames + 1) * frameIntervalCost / Constants.TIME_MILLIS_TO_NANO; // 代碼 1
sumDroppedFrames += droppedFrames; // 代碼 2
sumFrame++; // 代碼 3
if (droppedFrames >= frozenThreshold) { // 代碼 4
dropLevel[DropStatus.DROPPED_FROZEN.index]++;
dropSum[DropStatus.DROPPED_FROZEN.index] += droppedFrames;
} else if (droppedFrames >= highThreshold) {
dropLevel[DropStatus.DROPPED_HIGH.index]++;
dropSum[DropStatus.DROPPED_HIGH.index] += droppedFrames;
} else if (droppedFrames >= middleThreshold) {
dropLevel[DropStatus.DROPPED_MIDDLE.index]++;
dropSum[DropStatus.DROPPED_MIDDLE.index] += droppedFrames;
} else if (droppedFrames >= normalThreshold) {
dropLevel[DropStatus.DROPPED_NORMAL.index]++;
dropSum[DropStatus.DROPPED_NORMAL.index] += droppedFrames;
} else {
dropLevel[DropStatus.DROPPED_BEST.index]++;
dropSum[DropStatus.DROPPED_BEST.index] += (droppedFrames < 0 ? 0 : droppedFrames);
}
}
void report() {
float fps = Math.min(60.f, 1000.f * sumFrame / sumFrameCost); // 代碼 5
MatrixLog.i(TAG, "[report] FPS:%s %s", fps, toString());
try {
TracePlugin plugin = Matrix.with().getPluginByClass(TracePlugin.class); // 代碼 6
JSONObject dropLevelObject = new JSONObject();
dropLevelObject.put(DropStatus.DROPPED_FROZEN.name(), dropLevel[DropStatus.DROPPED_FROZEN.index]);
dropLevelObject.put(DropStatus.DROPPED_HIGH.name(), dropLevel[DropStatus.DROPPED_HIGH.index]);
dropLevelObject.put(DropStatus.DROPPED_MIDDLE.name(), dropLevel[DropStatus.DROPPED_MIDDLE.index]);
dropLevelObject.put(DropStatus.DROPPED_NORMAL.name(), dropLevel[DropStatus.DROPPED_NORMAL.index]);
dropLevelObject.put(DropStatus.DROPPED_BEST.name(), dropLevel[DropStatus.DROPPED_BEST.index]);
JSONObject dropSumObject = new JSONObject();
dropSumObject.put(DropStatus.DROPPED_FROZEN.name(), dropSum[DropStatus.DROPPED_FROZEN.index]);
dropSumObject.put(DropStatus.DROPPED_HIGH.name(), dropSum[DropStatus.DROPPED_HIGH.index]);
dropSumObject.put(DropStatus.DROPPED_MIDDLE.name(), dropSum[DropStatus.DROPPED_MIDDLE.index]);
dropSumObject.put(DropStatus.DROPPED_NORMAL.name(), dropSum[DropStatus.DROPPED_NORMAL.index]);
dropSumObject.put(DropStatus.DROPPED_BEST.name(), dropSum[DropStatus.DROPPED_BEST.index]);
JSONObject resultObject = new JSONObject();
resultObject = DeviceUtil.getDeviceInfo(resultObject, plugin.getApplication());
resultObject.put(SharePluginInfo.ISSUE_SCENE, focusedActivityName);
resultObject.put(SharePluginInfo.ISSUE_DROP_LEVEL, dropLevelObject);
resultObject.put(SharePluginInfo.ISSUE_DROP_SUM, dropSumObject);
resultObject.put(SharePluginInfo.ISSUE_FPS, fps);
Issue issue = new Issue();
issue.setTag(SharePluginInfo.TAG_PLUGIN_FPS);
issue.setContent(resultObject);
plugin.onDetectIssue(issue);
} catch (JSONException e) {
MatrixLog.e(TAG, "json error", e);
}
}
@Override
public String toString() {
return "focusedActivityName=" + focusedActivityName
+ ", sumFrame=" + sumFrame
+ ", sumDroppedFrames=" + sumDroppedFrames
+ ", sumFrameCost=" + sumFrameCost
+ ", dropLevel=" + Arrays.toString(dropLevel);
}
}
這里主要是根據(jù)每一幀的總耗時,統(tǒng)計掉幀個數(shù)、FPS 等數(shù)據(jù),最后將這些數(shù)據(jù)以一定的格式回調(diào)出去
四.總結(jié)
這篇文章主要分析了 Matrix-TraceCanary 模塊中幀率 FPS、掉幀個數(shù)等數(shù)據(jù)的原理和大概流程,原理是通過 Looper.getMainLooper().setMessageLogging(Printer printer) 向其中添加 Printer 對象監(jiān)聽主線程中 MainLooper 每個消息的處理時間,當前處理的是不是繪制階段產(chǎn)生的消息是根據(jù) Choreographer 機制判斷的,如果當前處理的是繪制階段產(chǎn)生的消息,在處理消息的同時一定會回調(diào) Choreographer 中回調(diào)隊列 mCallbackQueues 中的回調(diào)方法,向回調(diào)隊列 mCallbackQueues 添加回調(diào)是通過反射在 UIThreadMonitor 中實現(xiàn)的。
通過 Looper.getMainLooper().setMessageLogging(Printer printer) 方式實現(xiàn) FPS 統(tǒng)計一直有一個問題,在 2.1 和 2.3 小節(jié)中也提到過,使用 Looper 機制實現(xiàn)的方式存在一個缺點:在打印消息處理開始日志和消息處理結(jié)束日志的時候,都會進行字符串的拼接,頻繁地進行字符串的拼接也會影響性能