Android Performance Optimization Series - 04 TraceCanary source code analysis in Matrix

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 followsHhandleMessage(Message msg)Looper.loop()

  • Code 1:Looper.loop() There will be an infinite loop of for(;;) in the method that continuously reads messages from MessageQueue and Process
  • Code 3: Finally, msg.target.dispatchMessage(msg) is called to process this Message, and msg.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 via Looper.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

issue address

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 object LooperMonitor monitor

  • Code 2 & Code 3:LooperMonitor implements the MessageQueue.IdleHandler interface and its abstract methodqueueIdle(), and in the construction In the method, different methods are used to add this MessageQueue.IdleHandler instance object to MessageQueue according to the version difference. The queueIdle() method returns true to ensure the message queue. After each message in is processed, the queueIdle() 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 for mLoggingLooper.getMainLooper()

  • Code 5: InPrinter.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 parameters boolean isBegin, listener.isValid() and listener.isHasDispatchStart Conditional execution listener.dispatchStart() and listener.dispatchEnd() methods, what to do at the beginning and end of message processing are all in listener.dispatchStart() and a>listener.dispatchEnd() implemented in
  • Code 2: This line of code, personally understands it as standing on the last post. Whenlistener.isValid() == false && isBegin == false && listener.isHasDispatchStart == true, the listener.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 listenerLooperDispatchListener 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 queue CALLBACK_TRAVERSAL = 2, and the fourth is the submission callback queue CALLBACK_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 in mCallbackQueues 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 reflectionaddCallbackLocked
  • 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 UIThreadMonitorqueueStatus and queueCost 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 variableisBelongFrame is false, which is set in the doFrameBegin(long token) method a>true, doFrameBegin(long token) is called in the run() method, UIThreadMonitor is implemented The Runnable interface naturally overrides the run() method, so when is the UIThreadMonitor object executed by the thread? Analysis below
  • Code 3, at the end of message processing, the dispatchEnd() method will be called, in which the variable isBelongFrame is used to determine whether to call doFrameEnd(long token)
  • Code 4, in the run() method, first call doFrameBegin(long token) to set the variable isBelongFrame to true, and then use the doQueueBegin() method to record the status and time when the input event callback CALLBACK_INPUT starts; then use the addFrameCallback() method Set the animation callback Choreographer for CALLBACK_ANIMATION and the callback method for traversal drawing callback CALLBACK_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 ofCALLBACK_ANIMATIONrun()CALLBACK_INPUTCALLBACK_ANIMATION
  • Code 6, in the callback method of traversal drawingCALLBACK_TRAVERSAL, record the end status and time of , Also record the start status and time ofrun()CALLBACK_ANIMATIONCALLBACK_TRAVERSAL
  • In fact, it can be guessed here that UIThreadMonitor implements the Runnable interface in order to use UIThreadMonitor as an input event callback The callback method of a>CALLBACK_INPUT is set to 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) 记录输入事件回调 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, the addFrameCallback(CALLBACK_INPUT, this, true) method will be called once, and this Runnable will be used as an input event< /span>The callback of CALLBACK_INPUT is added to Choreographyer
  • Code 2, in thedoFrameEnd(long token) method, the addFrameCallback(CALLBACK_INPUT, this, true) method will be called once after each frame is completed, and added again for the next frame callback. Three callbacks. The doFrameEnd(long token) method is called back in the dispatchEnd() method at the end of processing the message
  • Code 3, in the above code analysis, only doQueueBegin(CALLBACK_TRAVERSAL) was called in the end, and doFrameEnd(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, the doFrameEnd(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 its doFrame(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 traverseHashSet<IDoFrameListener> listeners and calculate the number of dropped frames based on the time passed in doFrame() , and finally call back and through the two methods in the interfacelong frameCostframeCostMsdropFrameIDoFrameListener
  • 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), callFrameCollectItem#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.

Guess you like

Origin blog.csdn.net/jdsjlzx/article/details/134180291