I. Overview
When optimizing UI performance, it is very important to compare before and after optimization. Otherwise, how to judge whether your optimization is effective and how effective it is? In terms of comparison, I personally think it can be divided into two categories, one is intuitive comparison through observation, and the other is objective comparison through data.
Intuitive comparison, we can use and in 开发者选项
to check whether there is transition drawing and UI in the page GPU rendering during rendering过渡绘制
GPU 分析
For objective comparison, we can analyze and compare through various testing tools, such as NetEase'sEmmagee and Tencent'smatrix Wait, unfortunately Emmagee cannot be used on Android 7.0 or above due to system limitations, so I use matrix
When doing software development, you must not only know what is happening, but also why it is happening, so that you can make greater progress. When I was optimizing UI performance, I used the TraceCanary module in matrix. Its function is mainly to detect UI freezes, startup time, page switching and slow function detection. Here we mainly analyze how the frame rate is calculated. , the code for other functions should not be very complicated, and interested friends can analyze it by themselves.
2. Principle
From the beginning of Android application development, I often heard that time-consuming operations (such as IO data reading, network requests, etc.) cannot be done in the main thread, which will cause problems such as UI lag, because the main thread every A frame will be rendered in 16.67 ms. If there are time-consuming tasks in the main thread, the next frame may not be rendered within 16.67 ms, resulting in dropped frames. Visually, dropped frames are commonly referred to as lags. . Although there are many reasons for dropped frames, two variables are fixed: the main thread and 16.67 ms. We can start with these two fixed values to analyze and determine whether there is lag in the main thread.
Friends who have learned about Android performance optimization may know that the mainstream idea in the industry to implement lagging monitoring is to monitor the time-consuming status of tasks in the main thread. If the task time-consuming exceeds a certain threshold, dump The stack information of the current main thread can be used to analyze the cause of the lag. Typical representatives of these two ideas are ArgusAPM and BlockCanary, BlockCanaryEx, etc. The idea is like this. The implementation methods can be divided into two categories. One is through the Looper mechanism of the main thread, and the other is through the Choreographer module. Let’s briefly introduce these two methods
2.1 Looper mechanism
Everyone knows that there is aMainLooper
in the main thread. After the main thread is started, the MainLooper#loop()
method will be called, and the tasks executed in the main thread will be indirect It is executed by of an internal class (which is a subclass of Handler) in ActivityThread
. Let’s analyze it. The method is as followsH
handleMessage(Message msg)
Looper.loop()
- Code 1:
Looper.loop()
There will be an infinite loop offor(;;)
in the method that continuously reads messages fromMessageQueue
and Process - Code 3: Finally,
msg.target.dispatchMessage(msg)
is called to process this Message, andmsg.target
is actually Handler, which refers to all Handlers corresponding to MainLooper< /span> - Code 2 & Code 4: Before and after processing the message, the object obtained through
myLooper().mLogging
will be printed separately andPrinter
>>>>> Dispatching to
<<<<< Finished to
myLooper().mLogging
can be set viaLooper.getMainLooper().setMessageLogging(Printer printer)
So we only need to determine whether the time between the two logs >>>>> Dispatching to
and <<<<< Finished to
exceeds the threshold, and then we can determine whether the main thread is executing at this time Whether the task is a time-consuming task
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);
}
......
}
}
......
}
The code is as follows
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")) {
...... // 结束记录,判断处理时间间隔是否超过阈值
}
}
});
2.2 Choreographer module
The Android system will send a VSYNC signal every 16.67 ms to trigger the rendering of the UI. Under normal circumstances, the interval between two VSYNC signals is 16.67 ms. If it exceeds 16.67 ms, the rendering can be considered to be stuck.
Choreographer
in class FrameCallback.doFrame(long l)
in Android SDK will be called back every time the VSYNC signal is sent, so we only need to determine the adjacent Whether the interval between two timesFrameCallback.doFrame(long l)
exceeds the threshold. If it exceeds the threshold, a freeze occurs. You can dump the stack information of the current main thread in another sub-thread for analysis
The schematic code is as follows. It should be noted that in the FrameCallback.doFrame(long l)
callback method, we need to re-register the callback every time 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 Implementation of Matrix-TraceCanary
Determining whether there is lag in the UI can be roughly divided into the above two implementation methods.
In the TraceCanary module of Matrix, the old version used the Choreographer
method, but the new version uses the Looper
mechanism. As for why It is not yet clear whether there will be such a difference
However, there is a disadvantage in using theLooper
mechanism: when printing the message processing start log and the message processing end log, string splicing will be performed, and strings will be spliced frequently. Splicing also affects performance
3. Source code analysis
The analysis here of Matrix and TraceCanary is version 0.5.2
Now that we know its implementation principle, we will go straight to Huanglong to find the code related to Looper.getMainLooper().setMessageLogging()
and follow the clues, so that the analysis will be clearer.
The directory structure of Matrix and TraceCanary is as shown below
3.1 Add Printer to Looper
The selectedLooperMonitor
class in the above picture is the class we started with. A Printer object is added by calling the Looper.getMainLooper().setMessageLogging()
method. The code is as follows
-
Code 1:The constructor of
LooperMonitor
is private and has a static final objectLooperMonitor monitor
-
Code 2 & Code 3:
LooperMonitor
implements theMessageQueue.IdleHandler
interface and its abstract methodqueueIdle()
, and in the construction In the method, different methods are used to add thisMessageQueue.IdleHandler
instance object toMessageQueue
according to the version difference. ThequeueIdle()
method returns true to ensure the message queue. After each message in is processed, thequeueIdle()
method will be called back -
Code 4: The method will be called in the constructor and
queueIdle()
method, which is the actual pass Where to set up PrinterresetPrinter()
Looper.getMainLooper().setMessageLogging()
Before each addition, the current
Looper.getMainLooper()
setmLogging
object will be obtained through reflection, and it will be judged whether it was previously set by LooperMonitor. If < The object in /span>< /span>Looper.getMainLooper()
is set by LooperMonitor and will not be set again. Otherwise, its own Printer object will be set formLogging
Looper.getMainLooper()
-
Code 5: In
Printer.println(String x)
, it will be judged based on the first character of the incoming parameterString x
whether it is valid< a i=3> log, the log starting with is the log where message processing starts, the log starting with is the log where message processing ends, and < /span> methodLooper.loop()
>
>
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) == '>');
}
}
});
}
}
The start and end of Looper.loop() message processing have been monitored. Next, let’s take a look at how the start and end of message processing are handled. Mainly look at the dispatch(boolean isBegin)
method a>
- In
dispatch(boolean isBegin)
, the LooperDispatchListener listening callbacks added to LooperMonitor will be processed in sequence - Code 1: The
dispatch(boolean isBegin)
method will be based on the parametersboolean isBegin
,listener.isValid()
andlistener.isHasDispatchStart
Conditional executionlistener.dispatchStart()
andlistener.dispatchEnd()
methods, what to do at the beginning and end of message processing are all inlistener.dispatchStart()
and a>listener.dispatchEnd()
implemented in - Code 2: This line of code, personally understands it as standing on the last post. When
listener.isValid() == false && isBegin == false && listener.isHasDispatchStart == true
, thelistener.dispatchEnd()
method is called for the last time. - Code 3: You can set to LooperMonitor through the
LooperMonitor.register(LooperDispatchListener listener)
method. Next, let’s see where to call this method to set < a i=3> If you listen, you can see the specific logic at the beginning and end of message processingLooperDispatchListener listener
LooperDispatchListener listener
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 Message processing
As shown in the figure below, there are two classes AppMethodBeat
and UIThreadMonitor
in the trace-canary module through the LooperMonitor.register(LooperDispatchListener listener)
method Set LooperDispatchListener listener
in LooperMonitor. Related to this article is the UIThreadMonitor
class. Let’s focus on analyzing this class
First analyze theUIThreadMonitor
classinitialization method, which mainly obtains some properties of Choreographer through reflection , and set it to LooperMonitor through LooperMonitor.register(LooperDispatchListener listener)
methodLooperDispatchListener listener
- Code 1: Obtained the
mCallbackQueues
attribute of the Choreographer instance through reflection,mCallbackQueues
is a callback queue arrayCallbackQueue[] mCallbackQueues
, It includes four callback queues, the first is the input event callback queueCALLBACK_INPUT = 0
, the second is the animation callback queueCALLBACK_ANIMATION = 1
, and the third is the traversal drawing callback queueCALLBACK_TRAVERSAL = 2
, and the fourth is the submission callback queueCALLBACK_COMMIT = 3
. These four stages are executed sequentially in the UI rendering of each frame. At the beginning of each stage in each frame, the callback method of the corresponding callback queue inmCallbackQueues
will be called back. Recommended articles about Choreographer: Android Choreographer source code analysis - Code 2: Get the input event callback queue’s
addCallbackLocked
method through reflection - Code 3: Get the animation callback queue’s
addCallbackLocked
method through reflection - Code 4: Obtain the method of traversing the drawing callback queue through reflection
addCallbackLocked
- Code 5: Set to LooperMonitor through
LooperMonitor.register(LooperDispatchListener listener)
methodLooperDispatchListener listener
- Code 6: Callback at the beginning of message processing in Looper.loop()
- Listing 7: Callback at the end of message processing in Looper.loop()
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
}
});
......
}
}
As mentioned in Section 3.1, when Looper starts and ends processing messages, the dispatchStart()
and dispatchEnd()
methods of LooperDispatchListener will be called back respectively, that is, as above The code is shown at code 6 and code 7
dispatchStart()
The method is relatively simple. It callsUIThreadMonitor.this.dispatchBegin()
to record two start times, one is the thread start time and the other is the cpu start time, and callbacks in sequence LooperObserver#dispatchBegin()
method, as shown below
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()
The method is relatively complicated. It determines whether to call based on the variable isBelongFrame
, then records the thread end time and CPU end time, and finally calls back MethoddoFrameEnd(long token)
LooperObserver#dispatchEnd()
- Code 1, there are two arrays with a length of three in UIThreadMonitor
queueStatus
andqueueCost
respectively corresponding to the input event stage and animation in each frame Phase, status and time consumption of traversing the drawing phase,queueStatus
has three values: DO_QUEUE_DEFAULT, DO_QUEUE_BEGIN and DO_QUEUE_END - Code 2, the initial value of the variable
isBelongFrame
isfalse
, which is set in thedoFrameBegin(long token)
method a>true
,doFrameBegin(long token)
is called in therun()
method,UIThreadMonitor
is implemented TheRunnable
interface naturally overrides therun()
method, so when is theUIThreadMonitor
object executed by the thread? Analysis below - Code 3, at the end of message processing, the
dispatchEnd()
method will be called, in which the variableisBelongFrame
is used to determine whether to calldoFrameEnd(long token)
- Code 4, in the
run()
method, first calldoFrameBegin(long token)
to set the variableisBelongFrame
totrue
, and then use thedoQueueBegin()
method to record the status and time when the input event callbackCALLBACK_INPUT
starts; then use theaddFrameCallback()
method Set the animation callbackChoreographer
forCALLBACK_ANIMATION
and the callback method for traversal drawing callbackCALLBACK_TRAVERSAL
- Code 5, in the callback method of animation callback, record the end status and time of , and also record the start status and time of
CALLBACK_ANIMATION
run()
CALLBACK_INPUT
CALLBACK_ANIMATION
- Code 6, in the callback method of traversal drawing
CALLBACK_TRAVERSAL
, record the end status and time of , Also record the start status and time ofrun()
CALLBACK_ANIMATION
CALLBACK_TRAVERSAL
- In fact, it can be guessed here that
UIThreadMonitor
implements theRunnable
interface in order to useUIThreadMonitor
as an input event callback The callback method of a>CALLBACK_INPUT
is set toChoreographer
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) 记录输入事件回调 CALLBACK_INPUT 开始状态和时间
addFrameCallback(CALLBACK_ANIMATION, new Runnable() { // 通过 addFrameCallback() 方法向 Choreographer 中添加动画 CALLBACK_ANIMATION 回调
// 每个回调其实都是一个 Runnable,执行时会主动调用 `run()` 方法
@Override
public void run() { // 代码 5
doQueueEnd(CALLBACK_INPUT); // 输入事件回调 CALLBACK_INPUT 结束
doQueueBegin(CALLBACK_ANIMATION); // 动画回调 CALLBACK_ANIMATION 开始
}
}, true);
addFrameCallback(CALLBACK_TRAVERSAL, new Runnable() { // 通过 addFrameCallback() 方法向 Choreographer 中添加遍历绘制 CALLBACK_TRAVERSAL 回调
@Override
public void run() { // 代码 6
doQueueEnd(CALLBACK_ANIMATION); // 动画回调 CALLBACK_ANIMATION 结束
doQueueBegin(CALLBACK_TRAVERSAL); // 遍历绘制回调 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;
}
}
......
}
Let’s take a look at when the input event callback CALLBACK_INPUT
was set into Choreographer
As shown in the figure above, the addFrameCallback()
method has two things besides adding animation callbacksCALLBACK_ANIMATION
and traversal drawing callbacksCALLBACK_TRAVERSAL
AddedCALLBACK_INPUT
callbacks, respectively in the following two places
- Code 1, when the
UIThreadMonitor#onStart()
method is called for the first time, theaddFrameCallback(CALLBACK_INPUT, this, true)
method will be called once, and thisRunnable
will be used as an input event< /span>The callback ofCALLBACK_INPUT
is added to Choreographyer - Code 2, in the
doFrameEnd(long token)
method, theaddFrameCallback(CALLBACK_INPUT, this, true)
method will be called once after each frame is completed, and added again for the next frame callback. Three callbacks. ThedoFrameEnd(long token)
method is called back in thedispatchEnd()
method at the end of processing the message - Code 3, in the above code analysis, only
doQueueBegin(CALLBACK_TRAVERSAL)
was called in the end, anddoFrameEnd(long token)
method 3>, Completed the monitoring of traversal drawing callbackdoQueueEnd(CALLBACK_TRAVERSAL)
CALLBACK_TRAVERSAL
- Code 4, create a new array with a length of 3 and assign it to
queueStatus
. In fact, there is a question here, thedoFrameEnd(long token)
method draws in each frame will be called back every time. Will creating an array object in this method cause memory jitter? - Code 5, traverse the HashSet object of
LooperObserver
and call back itsdoFrame(String focusedActivityName, long start, long end, long frameCostMs, long inputCostNs, long animationCostNs, long traversalCostNs)
method, which takes input events, animation, and traversal drawing time as parameters Incoming
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 Frame rate calculation
From the above code, you can see that the time information of each frame will be called back through HashSet<LooperObserver> observers
. Let’s see where it is added to observers
LooperObserver
callback, as shown in the figure below
Here we mainly look at FrameTracer
this class, which involves the calculation of frame rate FPS. Friends who are interested in other classes can analyze it themselves
The code of FrameTracer is as follows. I believe it is not difficult to understand if you look closely. The main logic is in the notifyListener(final String focusedActivityName, final long frameCostMs)
method, which involves a variable frameIntervalMs = 16.67
ms
- Code 1: It will traverse
HashSet<IDoFrameListener> listeners
and calculate the number of dropped frames based on the time passed indoFrame()
, and finally call back and through the two methods in the interfacelong frameCost
frameCostMs
dropFrame
IDoFrameListener
- The number of dropped frames is calculated through:
final int dropFrame = (int) (frameCostMs / frameIntervalMs)
,frameCostMs
is the time taken to draw this frame,frameIntervalMs
Actually it is 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++;
}
}
}
}
Look at where it is calledaddListener(IDoFrameListener listener)
AddedIDoFrameListener
to FrameTracer, as shown in the figure below, there are two main places: FrameDecorator and FrameTracer, The logic of these two places is actually similar, mainly look at addListener(new FPSCollector());
FPSCollector
is an internal class of FrameTracer
and implements the IDoFrameListener
interface. The main logic is in the doFrameAsync()
method a>
- Code 1: A corresponding FrameCollectItem object will be created based on the current ActivityName and stored in the HashMap
- Code 2: Call
FrameCollectItem#collect()
to calculate frame rate FPS and other information - Code 3: If the total drawing time of this Activity exceeds timeSliceMs (default is 10s), call
FrameCollectItem#report()
to report statistical data, and remove the current ActivityName and the corresponding FrameCollectItem from the HashMap Object
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
is also an internal class of FrameTracer, the most important of which are the two methods collect(int droppedFrames)
and report()
methods
- Code 1: Add the time consuming of each frame, sumFrameCost represents the total time consuming
- Code 2: sumDroppedFrames counts the total number of dropped frames
- Code 3: sumFrame represents the total number of frames
- Code 4: Based on the number of dropped frames, determine the extent of the frame drop behavior and record the number.
- Code 5: Calculate fps value based on
float fps = Math.min(60.f, 1000.f * sumFrame / sumFrameCost)
- Code 6: Encapsulate the statistical information into an Issue object and call it back through the
TracePlugin#onDetectIssue()
method
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);
}
}
Here we mainly count the number of dropped frames, FPS and other data based on the total time spent on each frame, and finally call back these data in a certain format.
4. Summary
This article mainly analyzes the principle and general process of data such as frame rate FPS and number of dropped frames in the Matrix-TraceCanary module. The principle is to add Printer object monitoring to it throughLooper.getMainLooper().setMessageLogging(Printer printer)
The processing time of each message of MainLooper in the main thread. Whether the current processing is a message generated in the drawing phase is judged based on the Choreographer mechanism. If the current processing is a message generated in the drawing phase, the callback in Choreographer will be called back while processing the message. Callback method in queuemCallbackQueues
, adding callback to callback queuemCallbackQueues
is implemented in UIThreadMonitor through reflection.
There has always been a problem with implementing FPS statistics throughLooper.getMainLooper().setMessageLogging(Printer printer)
. As mentioned in Sections 2.1 and 2.3, there is a way to implement it using the Looper
mechanism. One disadvantage: When printing the message processing start log and the message processing end log, string splicing will be performed. Frequent string splicing will also affect performance.