A brief analysis of the principle of Android fluency detection

Android will optimize the "fluency of UI" problem in different versions, but until the Android 4.1 version has been effectively optimized, this is Project Butter.

Project Butter joins three core elements: VSYNC, Triple Buffer and Choreographer. Among them, VSYNC is the core of understanding Project Buffer. VSYNC is the abbreviation of Vertical Synchronization, which is "Vertical Synchronization"

  • VSYNC: generate an interrupt signal
  • Triple Buffer: When the double buffer is not enough, the system can allocate a third buffer
  • Choreographer: This is used to receive a VSYNC signal to coordinate UI updates uniformly

To detect application stuck, the Android system sends a VSYNC signal every 16.6ms to notify the interface to perform input, animation, drawing and other actions. The cycle of each synchronization is 16.6ms, which represents the refresh rate of one frame. In theory, twice The time period of the callback should be 16.6ms. If it exceeds 16.6ms, we consider that a freeze has occurred, and the time period between two callbacks is used to determine whether a freeze has occurred. The principle of this scheme is to set its FrameCallback function through the Choreographer class. , when each frame is rendered, the callback FrameCallback will be triggered, and FrameCallback will call back the void doFrame (long frameTimeNanos) function. An interface rendering will call back the doFrame method. If the interval between two doFrames is greater than 16.6ms, it means that a freeze has occurred.

The fluency of the monitoring application is generally registered through the postFrameCallback method of the Choreographer class to register a VSYNC callback event

public static void start(final Builder builder) {
        Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
            long lastFrameTimeNanos = 0;
            long currentFrameTimeNanos = 0;

            @Override
            public void doFrame(long frameTimeNanos) {
                if (lastFrameTimeNanos == 0) {
                    lastFrameTimeNanos = frameTimeNanos;
                    LogMonitor.getInstance().setFrequency(builder.frame * 17 / 2);
                    if (builder.targetPackageName != null) {
                        LogMonitor.getInstance().setTargetPackageName(builder.targetPackageName);
                    }
                    LogMonitor.getInstance().setDumpListener(builder.onDumpListener);
                }
                currentFrameTimeNanos = frameTimeNanos;
                skipFrameCount = skipFrameCount(lastFrameTimeNanos, currentFrameTimeNanos, deviceRefreshRateMs);
                LogMonitor.getInstance().setFrame(skipFrameCount);
                if (LogMonitor.getInstance().isMonitor()) {
                    LogMonitor.getInstance().removeMonitor();
                }
                LogMonitor.getInstance().startMonitor();
                lastFrameTimeNanos = currentFrameTimeNanos;
                Choreographer.getInstance().postFrameCallback(this);
            }
        });
    }

The source code of postFrameCallback(FrameCallback callback)

 public void postFrameCallback(FrameCallback callback) {
        postFrameCallbackDelayed(callback, 0);
    }
......
 public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) {
        if (callback == null) {
            throw new IllegalArgumentException("callback must not be null");
        }

        postCallbackDelayedInternal(CALLBACK_ANIMATION,
                callback, FRAME_CALLBACK_TOKEN, delayMillis);
    }

postFrameCallback finally calls the postCallbackDelayedInternal() method, which we are tracking

postCallbackDelayedInternal()
 private void postCallbackDelayedInternal(int callbackType,
            Object action, Object token, long delayMillis) {
        if (DEBUG_FRAMES) {
            Log.d(TAG, "PostCallback: type=" + callbackType
                    + ", action=" + action + ", token=" + token
                    + ", delayMillis=" + delayMillis);
        }

        synchronized (mLock) {
            final long now = SystemClock.uptimeMillis();
            final long dueTime = now + delayMillis;
            mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

            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);
            }
        }
    }

In the postCallbackDelayedInternal method, it is preferred to add the callback interface we registered to the mCallbackQueues queue through mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);. Each type of callback is sorted and stored in a singly linked list according to the set execution time (dueTime) order.

Then judge whether the execution time is the current time, if it is to directly call scheduleFrameLocked(now); otherwise, send a message MSG_DO_SCHEDULE_CALLBACK, in fact, send the information and finally call the scheduleFrameLocked(now) method, so let's look directly at the code of this method

scheduleFrameLocked
   private void scheduleFrameLocked(long now) {
        if (!mFrameScheduled) {
            mFrameScheduled = true;
            if (USE_VSYNC) {//默认为true
                if (DEBUG_FRAMES) {
                    Log.d(TAG, "Scheduling next frame on vsync.");
                }

                // If running on the Looper thread, then schedule the vsync immediately,
                // otherwise post a message to schedule the vsync from the UI thread
                // as soon as possible.
                if (isRunningOnLooperThreadLocked()) { //是否是主线程
                    scheduleVsyncLocked();
                } else {//发消息给主线程
                    Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
                    msg.setAsynchronous(true);
                    mHandler.sendMessageAtFrontOfQueue(msg);
                }
            } else {
                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);
            }
        }
    }

USE_VSYNC is true by default, which means vertical synchronization is enabled by default

private static final boolean USE_VSYNC = SystemProperties.getBoolean(
        "debug.choreographer.vsync", true);

The code of the scheduleVsyncLocked method

private void scheduleVsyncLocked() {
        mDisplayEventReceiver.scheduleVsync();
    }
......
/**
     * Schedules a single vertical sync pulse to be delivered when the next
     * display frame begins.
     */
    public void scheduleVsync() {
        if (mReceiverPtr == 0) {
            Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event "
                    + "receiver has already been disposed.");
        } else {
            nativeScheduleVsync(mReceiverPtr);
        }
    }

Finally, register our callback event with the bottom layer by calling the native method nativeScheduleVsync(mReceiverPtr). At this point, the process of registering the "Vertical Sync" event with the system has ended. All the processes are as follows:

image

Receive VSYNS signal

How does the VSYNC signal synchronize to our code? The entry for the Java layer to receive VSYNC is dispatchVsync(), which means that whenever a VSYNC signal is generated at the bottom of the system, the system will call back this method.

dispatchVsync()
  // Called from native code.
    @SuppressWarnings("unused")
    private void dispatchVsync(long timestampNanos, int builtInDisplayId, int frame) {
        onVsync(timestampNanos, builtInDisplayId, frame);
    }
 private final class FrameDisplayEventReceiver extends DisplayEventReceiver
            implements Runnable {
        private boolean mHavePendingVsync;
        private long mTimestampNanos;
        private int mFrame;

        public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
            super(looper, vsyncSource);
        }

        @Override
        public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
          ......
            mTimestampNanos = timestampNanos;
            mFrame = frame;
            Message msg = Message.obtain(mHandler, this);
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
        }

        @Override
        public void run() {
            mHavePendingVsync = false;
            doFrame(mTimestampNanos, mFrame);
        }
    }

In the onVsync method, Message.obtain(mHandler, this) so msg.callback is this, and finally it will call msg.callback.run(), which is FrameDisplayEventReceiver run(), and enter doFrame() mTimestampNanos, which is the timestampNanos from onVsync Parameter, representing the time to generate VSYNC

void doFrame(long frameTimeNanos, int frame) {
        final long startNanos;
        synchronized (mLock) {
            if (!mFrameScheduled) {
                return; // no work to do
            }

            if (DEBUG_JANK && mDebugPrintNextFrameTimeDelta) {
                mDebugPrintNextFrameTimeDelta = false;
                Log.d(TAG, "Frame time delta: "
                        + ((frameTimeNanos - mLastFrameTimeNanos) * 0.000001f) + " ms");
            }

            long intendedFrameTimeNanos = frameTimeNanos;
            startNanos = System.nanoTime();//正真开始的时间
            final long jitterNanos = startNanos - frameTimeNanos;
            if (jitterNanos >= mFrameIntervalNanos) {
                // 时间差除以每帧时间间隔,来计算丢掉了几帧。其中mFrameIntervalNanos = (long)(1000000000 / getRefreshRate());一般刷新率为60,时间间隔为16.6ms
                final long skippedFrames = jitterNanos / mFrameIntervalNanos;
                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 long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
                if (DEBUG_JANK) {
                    Log.d(TAG, "Missed vsync by " + (jitterNanos * 0.000001f) + " ms "
                            + "which is more than the frame interval of "
                            + (mFrameIntervalNanos * 0.000001f) + " ms!  "
                            + "Skipping " + skippedFrames + " frames and setting frame "
                            + "time to " + (lastFrameOffset * 0.000001f) + " ms in the past.");
                }
                frameTimeNanos = startNanos - lastFrameOffset;
            }

            if (frameTimeNanos < mLastFrameTimeNanos) {
                if (DEBUG_JANK) {
                    Log.d(TAG, "Frame time appears to be going backwards.  May be due to a "
                            + "previously skipped frame.  Waiting for next vsync.");
                }
                scheduleVsyncLocked();
                return;
            }

            mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);
            mFrameScheduled = false;
            mLastFrameTimeNanos = frameTimeNanos;
        }
       //执行对应的callBack        
        try {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
            AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);

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

            mFrameInfo.markAnimationsStart();
            doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);

            mFrameInfo.markPerformTraversalsStart();
            doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);

            doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
        } finally {
            AnimationUtils.unlockAnimationClock();
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }

        if (DEBUG_FRAMES) {
            final long endNanos = System.nanoTime();
            Log.d(TAG, "Frame " + frame + ": Finished, took "
                    + (endNanos - startNanos) * 0.000001f + " ms, latency "
                    + (startNanos - frameTimeNanos) * 0.000001f + " ms.");
        }
    }

If you usually pay attention to the log information of Caton, then the following log will not be unfamiliar

if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
     Log.i(TAG, "Skipped " + skippedFrames + " frames!  "
                           + "The application may be doing too much work on its main thread.");

The default value of SKIPPED_FRAME_WARNING_LIMIT is 30, which means that when our program freezes more than 30, this log information will be printed. The doFrame() method finally calls doCallbacks() to process UI operations such as user input, animation, and drawing.

doCallbacks() method
void doCallbacks(int callbackType, long frameTimeNanos) {
        CallbackRecord callbacks;
        synchronized (mLock) {
        // We use "now" to determine when callbacks become due because it's possible
            // for earlier processing phases in a frame to post callbacks that should run
            // in a following phase, such as an input event that causes an animation to start.
            final long now = System.nanoTime();
            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) {
                if (DEBUG_FRAMES) {
                    Log.d(TAG, "RunCallback: type=" + callbackType
                            + ", action=" + c.action + ", token=" + c.token
                            + ", latencyMillis=" + (SystemClock.uptimeMillis() - c.dueTime));
                }
                c.run(frameTimeNanos);
            }
        } finally {
            synchronized (mLock) {
                mCallbacksRunning = false;
                do {
                    final CallbackRecord next = callbacks.next;
                    recycleCallbackLocked(callbacks);
                    callbacks = next;
                } while (callbacks != null);
            }
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }

extractDueCallbacksLocked is to extract all CallbackRecords whose execution time is before the current time. CallbackRecord is a linked list, and then traverse the callbacks to execute the run method

  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 {
                ((Runnable)action).run();
            }
        }
    }

If we register the callback event through postFrameCallback(FrameCallback), FrameCallback.doFrame will be called the next time Choreographer doesFrame. Remember that when we registered FrameCallback at the beginning, is it the type that the system encapsulates FrameCallback? It is FRAME_CALLBACK_TOKEN, so it will go here ((FrameCallback) action).doFrame(frameTimeNanos);, why do we need to register one each time, because each "vertical synchronization" will delete the registered event called. If this CallbackRecord is view animation or drawing, it will call ((Runnable)action).run();

The following is the flow chart of receiving VSYNC

image

summary
  1. Choreographer is a thread singleton, and must be bound to a Looper, because there is a Handler inside it that needs to be bound to Looper.
  2. First, we pass the postFrameCallback(FrameCallback callback) method, and finally pass the native method nativeScheduleVsync(mReceiverPtr) and a VSYNC binding.
  3. When the next VSYNC signal comes, call back the interface we bound, and then count the time between the two pins to determine whether the frame is dropped
  4. If the frame drops to get the stuck log, continue to monitor the next VSYNC signal

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325368660&siteId=291194637