Choreographer may be unfamiliar to some students, however, Choreographer is actually very popular. The start of the drawing flow of a View depends on Choreographer, which translates as “Choreographer”. Now let’s go into details about what its specific role is.

  • The demo address
  • Android Knowledge System

1. Pre-knowledge

Before going on to Choreographer, there must be some pre-knowledge to help you learn.

The refresh rate

The refresh rate represents the number of times the screen refreshes in a second, which is expressed in Hertz and depends on fixed parameters of the hardware. This value is typically 60Hz, or a screen refresh every 16.66ms.

Frame rate

Frame rate represents the number of frames a GPU can draw in a second, say 30FPS/60FPS. In this case, the high frame rate is always good.

VSYNC

The refresh rate and frame rate need to work together to get the content of the application onto the screen, the GPU takes the image data to draw, and then the hardware takes care of rendering the content onto the screen, which happens over and over again throughout the life of the application.

Refresh rates and frame rates don’t always keep the same pace:

  • If the frame rate is actually faster than the refresh rate

Then there will be some visual problems, as you can see in the figure below, when the frame rate is 100fps and the refresh rate is only 75Hz, not all the images rendered by the GPU will be displayed.

Inconsistent refresh rate and frame rate can cause screen tearing effects. When the GPU is writing frame data, starting at the top, the new frame overwrites the previous frame and immediately outputs a line of content. When the screen starts to refresh, it doesn’t actually know what state the buffer is in (whether a frame in the buffer has been drawn, or if not, some of it is from this frame and some of it is from the previous frame), so the frames it grabs from the GPU may not be completely complete.

Currently Android’s double buffering (or triple buffering, quadruple buffering) is very efficient, when the GPU writes a frame to a post-buffered memory, and the sub-region of memory called the frame buffer starts filling the post-buffering when the next frame is written, while the frame buffer stays the same. When the screen is refreshed, it will use the frame buffer (which has been drawn beforehand) instead of the post-buffer that is being drawn, which is what VSYNC does.

  • A condition in which the screen refresh rate is faster than the frame rate

If the screen refresh rate is faster than the frame rate, the screen will display the same image in two frames. At this point, the user will obviously notice that the animation is getting stuck or dropping frames, and then it will start flowing again. This is often referred to as flickering, frame skipping, and lag.

VSYNC is designed to solve the “screen tear” problem caused by inconsistent screen refresh rate and GPU frame rate.

FPS

FPS: The number of frames displayed Per Second, also called Frame rate. The average FPS on An Android device is 60FPS, which means 60 refresh times per second, or 60 frames, and each frame is only 1000/60=16.67ms at most. Once a frame has been drawn for longer than the limit, a frame drop occurs and the user sees the same picture in two consecutive frames. That’s when the screen refresh rate is faster than the frame rate.

2. ViewRootImpl.setView()

When viewrootimpl.setView () is called: ActivityThread.handleResumeActivity()->WindowManagerImpl.addView()->WindowManagerGlobal.addView()->ViewRootImpl.setView( )

/**
* We have one child
*/
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
        if (mView == null) { mView = view; .// Note 1 Start three processes (measurement, layout, drawing)requestLayout(); .// Note 2 Add a View to WindowManagerService, which uses Binder to communicate across processes by calling session.addtodisplay ()
            // Add Window to the screenres = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes, getHostVisibility(), mDisplay.getDisplayId(), mWinFrame, mAttachInfo.mContentInsets, mAttachInfo.mStableInsets, mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel); . }}}Copy the code

From ViewRootImpl. RequestLayout (), as well as the View of drawing process for the first time

@Override
public void requestLayout(a) {
    if(! mHandlingLayoutInLayoutRequest) { checkThread(); mLayoutRequested =true; scheduleTraversals(); }}Copy the code

RequestLayout () will go to the scheduleTraversals() method, which is very important and is explained separately below.

Choreographer 3

It’s finally time for Choreographer

//ViewRootImpl.java
final class TraversalRunnable implements Runnable {
    @Override
    public void run(a) { doTraversal(); }}final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

void scheduleTraversals(a) {
    // Comment 1 indicates whether it has started. If it has started, it will no longer enter
    if(! mTraversalScheduled) { mTraversalScheduled =true;
        // Comment 2 Synchronization barrier to ensure priority for drawn messages (which are asynchronous)
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        // Comment 3 Listen for the VSYNC signal and the next time the VSYNC signal comes, execute the traversalrunnable given to it
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); . }}void doTraversal(a) {
    if (mTraversalScheduled) {
        // The tag is complete
        mTraversalScheduled = false;
        // Remove the synchronization barrier
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
        // Start three process measure Layout drawperformTraversals(); . }}Copy the code
  1. It does not make sense to call scheduleTraversals multiple times during a single VSYNC signal, so a flag bit is used to mark this
  2. A barrier message is sent so that synchronous messages cannot be executed, only asynchronous messages can be executed, and drawn messages are asynchronous, guaranteeing the priority of drawn messages. Drawing tasks are definitely higher than other synchronization tasks. For more details on Handler synchronization barriers, read my previous article Handler synchronization barriers
  3. With Choreographer, its postCallback method was called, which I don’t know what to do with, but I’ll cover it later

Choreographer initialization

First we need to know what mChoreographer is and where initialization takes place. In the ViewRootImpl constructor, I see its initialization.

public ViewRootImpl(Context context, Display display) {
    mContext = context;
    Binder agent IWindowSession, communicates with WMS
    mWindowSession = WindowManagerGlobal.getWindowSession();
    mDisplay = display;
    / / initialize the current thread Generally is the main thread, the general is in WindowManagerGlobal addView () call
    mThread = Thread.currentThread();
    mWidth = -1;
    mHeight = -1;
    / / Binder agent IWindow
    mWindow = new W(this);
    // Currently not visible
    mViewVisibility = View.GONE;
    mFirst = true; // true for the first time the view is added
    mAdded = false; .// Initialize Choreographer from the getInstance() method name, which looks like a singletonmChoreographer = Choreographer.getInstance(); . }Copy the code

Initialize Choreographer in the constructor of ViewRootImpl, using Choreographer’s getInstance method, which looks like a singleton.

//Choreographer.java
/**
 * Gets the choreographer for the calling thread.  Must be called from
 * a thread that already has a {@linkAndroid.os. Looper} Associated with it. * Gets singleton Choreographer on the current thread. Before getting to it, ensure that the thread has initialized Looper *@return The choreographer for this thread.
 * @throws IllegalStateException if the thread does not have a looper.
 */
public static Choreographer getInstance(a) {
    return sThreadInstance.get();
}

// Thread local storage for the choreographer.
// Threads are private
private static final ThreadLocal<Choreographer> sThreadInstance =
        new ThreadLocal<Choreographer>() {
    @Override
    protected Choreographer initialValue(a) {
        // Fetch Looper from ThreadLocalMap of the current thread
        Looper looper = Looper.myLooper();
        if (looper == null) {
            throw new IllegalStateException("The current thread must have a looper!");
        }
        / / initialization
        Choreographer choreographer = new Choreographer(looper, VSYNC_SOURCE_APP);
        if (looper == Looper.getMainLooper()) {
            mMainInstance = choreographer;
        }
        returnchoreographer; }};Copy the code

As you can see from the code above, the implementation of getInstance() is not really a singleton, but an in-thread singleton. The idea is to use ThreadLocal to privatize data threads. For those of you who don’t know, see everything you need to know about the Handler mechanism.

In ThreadLocal’s initialValue(), we fetch the private data Looper that has already been initialized by the current thread. If the current thread has not initialized Looper, we will throw an IllegalStateException.

The initialization is usually done in the main thread, and the Looper in the main thread is already initialized, so there is no exception thrown. When was the main thread Looper initialized by the way? Let’s take a look at the application process creation process:

  1. AMS creates the application Process by calling process.start ()
  2. In Process. The start () through ZygoteProcess zygoteSendArgsAndGetResult and Zygote Process (Zygote is who? It is the incubation process master, create early use zygoteServer. RegisterServerSocketFromEnv create zygote communication server; It is also started with a call to forkSystemServersystem_server; Then is zygoteServer runSelectLoop into circulation mode) to establish a Socket connection, and will create process parameters needed to send to Zygote Socket server
  3. Zygote process Socket server (ZygoteServer) after receiving the parameter to invoke ZygoteConnection. ProcessOneCommand () processing parameters, and the process of the fork
  4. FindStaticMain () of RuntimeInit then finds the Main method of the ActivityThread class and executes it

Presumably analysis to here, we are already very familiar with it

//ActivityThread.java
public static void main(String[] args) {...// Initialize the main thread Looper
    Looper.prepareMainLooper();
    
    // Create ActivityThread and call Attach
    ActivityThread thread = new ActivityThread();
    thread.attach(false, startSeq);

    if (sMainThreadHandler == null) {
        sMainThreadHandler = thread.getHandler();
    }
    
    // Main thread is in loop
    Looper.loop();
    
    // The main thread loop cannot exit
    throw new RuntimeException("Main thread loop unexpectedly exited");
}
Copy the code

As soon as the application starts, the main thread Looper is initialized first, indicating its importance in Android. It is initialized by storing a Looper in a ThreadLocal and then storing that ThreadLocal in the current thread’s ThreadLocalMap to privatize the thread.

Back to Choreographer’s constructor

//Choreographer.java
private Choreographer(Looper looper, int vsyncSource) {
    // Pass in Looper
    mLooper = looper;
    //FrameHandler initializes the passed Looper
    mHandler = new FrameHandler(looper);
    // USE_VSYNC defaults to true after Android 4.1,
    / / FrameDisplayEventReceiver is used to receive VSYNC signal
    mDisplayEventReceiver = USE_VSYNC
            ? new FrameDisplayEventReceiver(looper, vsyncSource)
            : null;
    mLastFrameTimeNanos = Long.MIN_VALUE;
    
    // the time of one frame, 60FPS is 16.66ms
    mFrameIntervalNanos = (long) (1000000000 / getRefreshRate());
    
    // Callback queue
    mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
    for (int i = 0; i <= CALLBACK_LAST; i++) {
        mCallbackQueues[i] = new CallbackQueue();
    }
    // b/68769804: For low FPS experiments.
    setFPSDivisor(SystemProperties.getInt(ThreadedRenderer.DEBUG_FPS_DIVISOR, 1));
}
Copy the code

Choreographer is almost finished building, there are some new things in the constructor, more on that later.

Choreographer Process principles

Now let’s talk about Choreographer postCallback(), where ViewRootImpl is used

//Choreographer.java
//ViewRootImpl is used with this
public void postCallback(int callbackType, Runnable action, Object token) {
    postCallbackDelayed(callbackType, action, token, 0);
}
public void postCallbackDelayed(int callbackType,
        Runnable action, Object token, long delayMillis) {... postCallbackDelayedInternal(callbackType, action, token, delayMillis); }private final CallbackQueue[] mCallbackQueues;
private void postCallbackDelayedInternal(int callbackType,
        Object action, Object token, long delayMillis) {
    synchronized (mLock) {
        final long now = SystemClock.uptimeMillis();
        final long dueTime = now + delayMillis;
        // Store mTraversalRunnable in the queue at callbackType in the mCallbackQueues array
        mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
        
        // The delayMillis passed in is 0, where dueTime is now
        if (dueTime <= now) {
            scheduleFrameLocked(now);
        } else {
            Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
            msg.arg1 = callbackType;
            msg.setAsynchronous(true); mHandler.sendMessageAtTime(msg, dueTime); }}}Copy the code

There are two key points. The first is to save the mTraversalRunnable for later invocation and the second is to execute the scheduleFrameLocked method

//Choreographer.java
private void scheduleFrameLocked(long now) {
    if(! mFrameScheduled) { mFrameScheduled =true;
        if (USE_VSYNC) {
            / / go here

            // Apply VSYNC directly if the current thread is the thread that starts Choreographer, otherwise send an asynchronous message immediately to apply VSYNC to the thread that starts Choreographer
            if (isRunningOnLooperThreadLocked()) {
                scheduleVsyncLocked();
            } else {
                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
                msg.setAsynchronous(true); mHandler.sendMessageAtFrontOfQueue(msg); }}else {
            // VSYNC is not enabled. It is enabled by default after Android 4.1
            final long nextFrameTime = Math.max(
                    mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now);
            if (DEBUG_FRAMES) {
                Log.d(TAG, "Scheduling next frame in " + (nextFrameTime - now) + " ms.");
            }
            Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
            msg.setAsynchronous(true); mHandler.sendMessageAtTime(msg, nextFrameTime); }}}Copy the code

ScheduleVsyncLocked () is called to listen for VSYNC signals, which are sent by the hardware, and only draw when they arrive.

//Choreographer.java
private final FrameDisplayEventReceiver mDisplayEventReceiver;

private void scheduleVsyncLocked(a) {
    mDisplayEventReceiver.scheduleVsync();
}

private final class FrameDisplayEventReceiver extends DisplayEventReceiver implements Runnable {... }//DisplayEventReceiver.java
public void scheduleVsync(a) {
    if (mReceiverPtr == 0) {
        Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event "
                + "receiver has already been disposed.");
    } else {
        // Register to listen for VSYNC signals and call back to the dispatchVsync() methodnativeScheduleVsync(mReceiverPtr); }}Copy the code

MDisplayEventReceiver is a FrameDisplayEventReceiver FrameDisplayEventReceiver inherited from DisplayEventReceiver. In DisplayEventReceiver, there is a method called scheduleVsync(), which is used to register and listen to VSYNC signals. It is a native method.

When a VSYNC signal arrives, the Native layer calls back to the dispatchVsync method of the DisplayEventReceiver

//DisplayEventReceiver.java
// Called from native code.
@SuppressWarnings("unused")
private void dispatchVsync(long timestampNanos, int builtInDisplayId, int frame) {
    onVsync(timestampNanos, builtInDisplayId, frame);
}
public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {}Copy the code

When I received the VSYNC signal, the callback dispatchVsync method, reached the onVsync method, the method by subclasses FrameDisplayEventReceiver fu wrote

//FrameDisplayEventReceiver.java  
// It is Choreographer's inner class
private final class FrameDisplayEventReceiver extends DisplayEventReceiver
        implements Runnable {
    @Override
    public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
        if(builtInDisplayId ! = SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN) { Log.d(TAG,"Received vsync from secondary display, but we don't support "
                    + "this case yet. Choreographer needs a way to explicitly request "
                    + "vsync for a specific display to ensure it doesn't lose track "
                    + "of its scheduled vsync.");
            scheduleVsync();
            return;
        }
        
        //timestampNanos is the timestamp of the VSYNC callback in nanoseconds
        long now = System.nanoTime();
        if (timestampNanos > now) {
            timestampNanos = now;
        }

        if (mHavePendingVsync) {
            Log.w(TAG, "Already have a pending vsync event. There should only be "
                    + "one at a time.");
        } else {
            mHavePendingVsync = true;
        }

        mTimestampNanos = timestampNanos;
        mFrame = frame;
        // I am a Runnable and pass myself in
        Message msg = Message.obtain(mHandler, this);
        // Async message to ensure priority
        msg.setAsynchronous(true);
        mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
    }
    
    @Override
    public void run(a) {... doFrame(mTimestampNanos, mFrame); }}Copy the code

In the onVsync() method, the main thing is to send a message (presumably to switch threads) and then execute the run method. Within the Run method, Choreographer’s doFrame method is called. This method is a little long, so let’s get it straight.

//Choreographer.java

FrameTimeNanos is the time when the VSYNC signal is called back
void doFrame(long frameTimeNanos, int frame) {
    final long startNanos;
    synchronized (mLock) {
        if(! mFrameScheduled) {return; // no work to do}...long intendedFrameTimeNanos = frameTimeNanos;
        startNanos = System.nanoTime();
        // The jitterNanos is the difference between the current time and the time when the VSYNC signal came in. If Looper has a lot of asynchronous messages waiting to be processed (or if the previous asynchronous message processing is particularly time-consuming and the current message was sent for a long time to be executed), the processing may take a long time to get there
        final long jitterNanos = startNanos - frameTimeNanos;
        
        //mFrameIntervalNanos is the time between frames, which is 16.67ms on mobile phones
        if (jitterNanos >= mFrameIntervalNanos) {
            final long skippedFrames = jitterNanos / mFrameIntervalNanos;
            // The main thread is doing too many time-consuming operations or drawing too slowly
            // If the number of frames dropped exceeds 30, the corresponding log is output
            if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
                Log.i(TAG, "Skipped " + skippedFrames + " frames! "
                        + "The application may be doing too much work on its main thread.");
            }
            final longlastFrameOffset = jitterNanos % mFrameIntervalNanos; frameTimeNanos = startNanos - lastFrameOffset; }... }try {
        AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);

        mFrameInfo.markInputHandlingStart();
        doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);

        mFrameInfo.markAnimationsStart();
        doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
        
        // Perform the callback
        mFrameInfo.markPerformTraversalsStart();
        doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);

        doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
    } finally{ AnimationUtils.unlockAnimationClock(); }}Copy the code

DoFrame does two things in general, one is to probably send a log to the developer to warn of a lag, and the other is to perform a callback.

When the VSYNC signal comes, it records the time point, which is called frameTimeNanos here. DoFrame (), on the other hand, is executed through the Looper message loop, which means that the execution of the previous message will be blocked. That’s a long time, and this is dealing with interface drawing, and if you don’t draw in real time for a long time, you’re going to drop frames. Log is also typed in the source code when frame 30 is dropped.

Let’s look at the process of executing the callback

//Choreographer.java
void doCallbacks(int callbackType, long frameTimeNanos) {
    CallbackRecord callbacks;
    synchronized (mLock) {
        final long now = System.nanoTime();
        // Retrieve the corresponding CallbackRecord according to callbackType
        callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(
                now / TimeUtils.NANOS_PER_MS);
        if (callbacks == null) {
            return;
        }
        mCallbacksRunning = true; . }try {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, CALLBACK_TRACE_TITLES[callbackType]);
        for(CallbackRecord c = callbacks; c ! =null; c = c.next) {
            //c.run(frameTimeNanos); }}finally{... }}private static final class CallbackRecord {
    public CallbackRecord next;
    public long dueTime;
    public Object action; // Runnable or FrameCallback
    public Object token;

    public void run(long frameTimeNanos) {
        if (token == FRAME_CALLBACK_TOKEN) {
            ((FrameCallback)action).doFrame(frameTimeNanos);
        } else {
            // It will go here, because when ViewRootImpl scheduleTraversals, the token passed by postCallback is null.((Runnable)action).run(); }}}Copy the code

Find the CallbackRecord corresponding to the callbackType in the mCallbackQueues array and execute the Run method for all the elements in the queue. Then when the scheduleTraversals of the ViewRootImpl is performed, the mTraversalRunnable passed by postCallback is a Runnable. To review:

//ViewRootImpl.java
final class TraversalRunnable implements Runnable {
    @Override
    public void run(a) { doTraversal(); }}Copy the code

From doTraversal() the three traversal processes of View (measure, layout and draw) will be completed. Choreographer’s mission is almost complete.

This is Choreographer’s workflow. A quick summary:

  1. From ActivityThread handleResumeActivity began,ActivityThread. HandleResumeActivity () - > WindowManagerImpl. AddView () - > WindowManagerGlobal. AddView () - > initialize ViewRootImpl - > initialize C horeographer->ViewRootImpl.setView()
  2. It’s called in the setView of view PluginrequestLayout()->scheduleTraversals(), and then a synchronization barrier
  3. Submit a task mTraversalRunnable through Choreographer thread singleton postCallback() which is used to perform the three main processes of View (measure, Layout, draw)
  4. Choreographer. PostCallback () through internal DisplayEventReceiver. NativeScheduleVsync () to the underlying system registry VSYNC signal monitoring, when VSYNC signal comes, Will callback DisplayEventReceiver dispatchVsync (), will eventually notice FrameDisplayEventReceiver. OnVsync () method.
  5. Take the previously passed task mTraversalRunnable in onVsync() and execute the run method to begin drawing the process.

4. The application

Now that we have understood how Choreographer works, let’s put the basics of Choreographer to practical use. It helps us detect the FPS of the application.

Detection of FPS

With the above analysis, we know that Choreographer is internally listening for VSYNC signals and sends an asynchronous message to Looper when a VSYNC signal comes up, which will notify an external observer (the above observer is ViewRootImpl) when the message is executed. Notify ViewRootImpl that it is ready to start drawing. Every time Choreographer calls back, it tells ViewRootImpl to draw, and we just need to count the number of callbacks within a second to know the FPS.

Choreographer is a thread singleton anyway, I get an instance of it in the main thread call and then register an observer with postCallback imitating ViewRootImpl. I wrote this idea in code and found that postCallback was a hide method. / speechless

However, as a bonus, Choreographer provides another postFrameCallback method. I looked at the source code and found that it is not very different from postCallback except that the observer type registered is CALLBACK_ANIMATION, but this does not affect its callback

//Choreographer.java
public void postFrameCallback(FrameCallback callback) {
    postFrameCallbackDelayed(callback, 0);
}
public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) {
    postCallbackDelayedInternal(CALLBACK_ANIMATION,
            callback, FRAME_CALLBACK_TOKEN, delayMillis);
}
Copy the code

Go to the code, show me the code

object FpsMonitor {

    private const val FPS_INTERVAL_TIME = 1000L

    /** * The number of callbacks executed within 1 second is FPS */
    private var count = 0
    private val mMonitorListeners = mutableListOf<(Int) -> Unit>()

    @Volatile
    private var isStartMonitor = false
    private val monitorFrameCallback by lazy { MonitorFrameCallback() }
    private val mainHandler by lazy { Handler(Looper.getMainLooper()) }

    fun startMonitor(listener: (Int) -> Unit) {
        mMonitorListeners.add(listener)
        if (isStartMonitor) {
            return
        }
        isStartMonitor = true
        Choreographer.getInstance().postFrameCallback(monitorFrameCallback)
        // Count times after 1 second
        mainHandler.postDelayed(monitorFrameCallback, FPS_INTERVAL_TIME)
    }

    fun stopMonitor(a) {
        isStartMonitor = false
        count = 0
        Choreographer.getInstance().removeFrameCallback(monitorFrameCallback)
        mainHandler.removeCallbacks(monitorFrameCallback)
    }

    class MonitorFrameCallback : Choreographer.FrameCallback.Runnable {

        // The VSYNC signal arrives and the current asynchronous message is processed
        override fun doFrame(frameTimeNanos: Long) {
            // Times +1 in 1 second
            count++
            // Continue to listen for VSYNC signals next time
            Choreographer.getInstance().postFrameCallback(this)}override fun run(a) {
            // Pass the count outside
            mMonitorListeners.forEach {
                it.invoke(count)
            }
            count = 0
            // Continue sending delayed messages and wait 1 second to count the count count
            mainHandler.postDelayed(this, FPS_INTERVAL_TIME)
        }
    }

}
Copy the code

The FPS is obtained by recording the number of Choreographer callbacks per second.

Monitoring the caton

Choreographer can also be used for caton detection in addition to FPS monitoring.

Choreographer Fluency monitoring

By setting Choreographer’s FrameCallback, it is possible to record the time when each frame is rendered so that when the next frame is processed we can tell if the last frame dropped during rendering based on the time difference. In Android, every VSYNC signal will inform the interface to redraw and render. Each synchronization cycle is 16.6ms, representing the refresh frequency of a frame. DoFrame () is called every time it is necessary to start rendering. If the time difference between two doframes () is greater than 16.6ms, then the UI is stuck and frames have been dropped. Divide the time difference by 16.6 to find how many frames have been dropped.

Show me the code:

object ChoreographerMonitor {
    @Volatile
    private var isStart = false
    private val monitorFrameCallback by lazy { MonitorFrameCallback() }
    private var mListener: (Int) -> Unit = {}
    private var mLastTime = 0L

    fun startMonitor(listener: (Int) -> Unit) {
        if (isStart) {
            return
        }
        mListener = listener
        Choreographer.getInstance().postFrameCallback(monitorFrameCallback)
        isStart = true
    }

    fun stopMonitor(a) {
        isStart = false
        Choreographer.getInstance().removeFrameCallback { monitorFrameCallback }
    }

    class MonitorFrameCallback : Choreographer.FrameCallback {

        private val refreshRate by lazy {
            // calculate the refreshRate and assign it to the refreshRate
            if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { App.getAppContext().display? .refreshRate ? :16.6 f
            } else {
                val windowManager =
                    App.getAppContext().getSystemService(Context.WINDOW_SERVICE) as WindowManager
                windowManager.defaultDisplay.refreshRate
            }
        }

        override fun doFrame(frameTimeNanos: Long) {
            mLastTime = if (mLastTime == 0L) {
                frameTimeNanos
            } else {
                // The frameTimeNanos is in nanoseconds, where the time difference is calculated and then converted to milliseconds
                val time = (frameTimeNanos - mLastTime) / 1000000
                // How many frames have been skipped
                val frames = (time / (1000f / refreshRate)).toInt()
                if (frames > 1) {
                    mListener.invoke(frames)
                }
                frameTimeNanos
            }
            Choreographer.getInstance().postFrameCallback(this)}}}Copy the code

Because the postFrameCallback() method can only listen for VSYNC once, doFrame() must be called again to listen for the next VSYNC signal.

This scheme is suitable for monitoring app frame drop in the online environment to calculate the smoothness of app in certain scenes, and then optimize the performance accordingly.

Looper string matches caton detection

Here’s another way to do the stuck detection (this is the main thread, and child threads generally don’t care about the stuck) –Looper. Let’s start with the loop code:

//Looper.java
private Printer mLogging;
public void setMessageLogging(@Nullable Printer printer) {
    mLogging = printer;
}

public static void loop(a) {
    final Looper me = myLooper();
    for (;;) {
        final Printer logging = me.mLogging;
        if(logging ! =null) {
            logging.println(">>>>> Dispatching to " + msg.target + "" +
                    msg.callback + ":"+ msg.what); }... msg.target.dispatchMessage(msg); .if(logging ! =null) {
            logging.println("<<<<< Finished to " + msg.target + ""+ msg.callback); }}}Copy the code

As you can see from this code, if we set Printer, a log will be printed before and after each message distribution to identify the beginning and end of the event distribution. This point can be used to determine whether there is a holdup by the time interval between Looper printing logs. If there is a holdup, the stack information of the thread at this time is saved to analyze where the holdup occurs. This matching string scheme can get the stack information exactly at the time of the crash.

Now that we know how it works, let’s just whip out a tool

const val TAG = "looper_monitor"

/** * Default timeout threshold */
const val DEFAULT_BLOCK_THRESHOLD_MILLIS = 3000L
const val BEGIN_TAG = ">>>>> Dispatching"
const val END_TAG = "<<<<< Finished"

class LooperPrinter : Printer {

    private var mBeginTime = 0L

    @Volatile
    var mHasEnd = false
    private val collectRunnable by lazy { CollectRunnable() }
    private val handlerThreadWrapper by lazy { HandlerThreadWrapper() }

    override fun println(msg: String?) {
        if (msg.isNullOrEmpty()) {
            return
        }
        log(TAG, "$msg")
        if (msg.startsWith(BEGIN_TAG)) {
            mBeginTime = System.currentTimeMillis()
            mHasEnd = false

            // A separate thread is required to fetch the stack
            handlerThreadWrapper.handler.postDelayed(
                collectRunnable,
                DEFAULT_BLOCK_THRESHOLD_MILLIS
            )
        } else {
            mHasEnd = true
            if (System.currentTimeMillis() - mBeginTime < DEFAULT_BLOCK_THRESHOLD_MILLIS) {
                handlerThreadWrapper.handler.removeCallbacks(collectRunnable)
            }
        }
    }

    fun getMainThreadStackTrace(a): String {
        val stackTrace = Looper.getMainLooper().thread.stackTrace
        return StringBuilder(a).apply {
            for (stackTraceElement in stackTrace) {
                append(stackTraceElement.toString())
                append("\n")
            }
        }.toString()
    }

    inner class CollectRunnable : Runnable {
        override fun run(a) {
            if(! mHasEnd) {// Print the main thread stack
                log(TAG, getMainThreadStackTrace())
            }
        }
    }

    class HandlerThreadWrapper {
        var handler: Handler
        init {
            val handlerThread = HandlerThread("LooperHandlerThread")
            handlerThread.start()
            handler = Handler(handlerThread.looper)
        }
    }

}
Copy the code

There is very little code, and the main idea is to determine whether the text of the println() callback starts or ends. If it is at the beginning, a timer will be set up. After 3 seconds, it will think that it is stuck and output the main thread stack information log. If the message has been distributed within 3 seconds, then it is not stuck and the timer will be cancelled.

I did a click event in the demo and slept for 4 seconds

17987-17987/com.xfhy.allinone D/looper_monitor: >>>>> Dispatching to Handler (android.view.ViewRootImpl$ViewRootHandler) {63ca49} android.view.View$PerformClick@13f525a: 0
17987-18042/com.xfhy.allinone D/looper_monitor: java.lang.Thread.sleep(Native Method)
    java.lang.Thread.sleep(Thread.java:373)
    java.lang.Thread.sleep(Thread.java:314)
    com.xfhy.allinone.performance.caton.CatonDetectionActivity.manufacturingCaton(CatonDetectionActivity.kt:39)
    com.xfhy.allinone.performance.caton.CatonDetectionActivity.access$manufacturingCaton(CatonDetectionActivity.kt:14)
    com.xfhy.allinone.performance.caton.CatonDetectionActivity$onCreate$3.onClick(CatonDetectionActivity.kt:34)
    android.view.View.performClick(View.java:6597)
    android.view.View.performClickInternal(View.java:6574)
    android.view.View.access$3100(View.java:778)
    android.view.View$PerformClick.run(View.java:25885)
    android.os.Handler.handleCallback(Handler.java:873)
    android.os.Handler.dispatchMessage(Handler.java:99)
    android.os.Looper.loop(Looper.java:193)
    android.app.ActivityThread.main(ActivityThread.java:6669)
    java.lang.reflect.Method.invoke(Native Method)
    com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
    com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
17987-17987/com.xfhy.allinone D/looper_monitor: <<<<< Finished to Handler (android.view.ViewRootImpl$ViewRootHandler) {63ca49} android.view.View$PerformClick@13f525a
Copy the code

As you can see, we’ve got the stack information at Caton, which is enough to analyze what happened where. Here’s the manufacturingCaton for sleep() on the CatonDetectionActivity.

The resources

  • Android Performance Patterns: Understanding VSYNC
  • Interviewer: How do you monitor the FPS of your application?
  • Choreographer principle
  • AndroidPerformanceMonitor
  • Comparison of Android Caton performance monitoring schemes