app卡顿系列二 :屏幕刷新机制学习

1,835 阅读4分钟

「这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战」。

Choreographer的工作过程

谷歌在4.1之后引入了Choreographer,Choreographer的中文翻译是编舞,编导。对于屏幕刷新而言Choreographer的工作职责是发起并接受底层的VSync信号。工作流程如图

Choreographer工作流程.png

ViewRootImpl#requestLayout

view的requestLayout最终会调用ViewRootImpl#requestLayout,DecorView的parent是ViewRootImpl

 @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            //检查是不是在主线程发起ui更新请求
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }
​
​
 void scheduleTraversals() {
        if (!mTraversalScheduled) {
            //这个标记防止多次重复调用该方法,在接收并处理VSync或者取消本次更新ui请求的时候会被重置
            mTraversalScheduled = true;
            //发起一个同步屏障消息
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            //发送消息监听垂直同步信息
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            //发送一个消息通知
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

小结:ViewRootImpl在向Choreographer 请求更新ui的时候主要的工作是

  1. 检测当前发起更新ui的线程.
  2. 给消息队列发送一个同步屏障。当消息队列中存在同步屏障的时候,会优先处理异步消息,而更新ui的消息就是异步消息。

Choreographer

Choreographer#postCallback

 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) {
        if (action == null) {
            throw new IllegalArgumentException("action must not be null");
        }
        if (callbackType < 0 || callbackType > CALLBACK_LAST) {
            throw new IllegalArgumentException("callbackType is invalid");
        }
​
        postCallbackDelayedInternal(callbackType, action, token, delayMillis);
    }
​
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;
            //将对应类型的callbackType 添加到队列中
            mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
            //一般而言都是走这个位置,但是在TextView中会发送延迟消息。
            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);
            }
        }
    }

Choreographer#scheduleFrameLocked

 private void scheduleFrameLocked(long now) {
        if (!mFrameScheduled) {
            mFrameScheduled = true;
            if (USE_VSYNC) {//这个是一个配置项是否启用垂直同步,一般是启用的
                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()) {//如果是在主线程,直接调用不在主线程的话发送handler消息调用scheduleVsyncLocked
                    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);
            }
        }
    }

Choreographer#scheduleVsyncLocked

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

FrameDisplayEventReceiver

mDisplayEventReceiver是FrameDisplayEventReceiver的实例对象,它的scheduleVsync方法在FrameDisplayEventReceiver的父类DisplayEventReceiver

DisplayEventReceiver#scheduleVsync

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

nativeScheduleVsync的底层实现主要是重置是否分发垂直同步信号的标记,等待接收下一次垂直同步信息,在垂直同步信息分发之后会重置该标记,因此VSync信息不会每次都被应用接收到,每一次接收都对应着一次请求,不请求就不接收。

DisplayEventReceiver#dispatchVsync

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

DisplayEventReceiver#dispatchVsync负责接收底层调用的垂直同步信息然后调用onVsync 。

FrameDisplayEventReceiver

在DisplayEventReceiver中onVsync为空实现,具体的逻辑在它的子类FrameDisplayEventReceiver

 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) {
            // Ignore vsync from secondary display.
            // This can be problematic because the call to scheduleVsync() is a one-shot.
            // We need to ensure that we will still receive the vsync from the primary
            // display which is the one we really care about.  Ideally we should schedule
            // vsync for a particular display.
            // At this time Surface Flinger won't send us vsyncs for secondary displays
            // but that could change in the future so let's log a message to help us remember
            // that we need to fix this.
            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;
            }
​
            // Post the vsync event to the Handler.
            // The idea is to prevent incoming vsync events from completely starving
            // the message queue.  If there are no messages in the queue with timestamps
            // earlier than the frame time, then the vsync event will be processed immediately.
            // Otherwise, messages that predate the vsync event will be handled first.
            long now = System.nanoTime();
            if (timestampNanos > now) {//更正时间戳,并打印日志提醒开发者检测 图形系统的时间是否有问题
                Log.w(TAG, "Frame time is " + ((timestampNanos - now) * 0.000001f)
                        + " ms in the future!  Check that graphics HAL is generating vsync "
                        + "timestamps using the correct timebase.");
                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;
            //第二个参数是this 最终会执行自身的run
            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);
        }
    }

可以看到底层的垂直同步信号与doFrame会经过handler将消息的处理切换到主线程,如果此时主线程正在执行耗时操作,虽然有同步屏障保证,但是也有可能造成丢帧。

Choreographer

doFrame是Choreographer的一个内部方法

Choreographer#doFrame

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();
            //doframe  调用过慢 虽然有同步屏障来保证异步消息先执行,发生丢帧
            final long jitterNanos = startNanos - frameTimeNanos;
            //mFrameIntervalNanos  是根据屏幕刷新频率计算出来的,Android 的屏幕刷新频率一般是60 但是也可能有其它的
            if (jitterNanos >= mFrameIntervalNanos) {
                final long skippedFrames = jitterNanos / mFrameIntervalNanos;
                //这个可以检测到发起ui更新之后的耗时操作,但是先由耗时操作在进行ui更新的话不一定能够检测   还有这个应该没办法检测三大流程的耗时
                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;
            }
​
            if (mFPSDivisor > 1) {
                long timeSinceVsync = frameTimeNanos - mLastFrameTimeNanos;
                if (timeSinceVsync < (mFrameIntervalNanos * mFPSDivisor) && timeSinceVsync > 0) {
                    scheduleVsyncLocked();
                    return;
                }
            }
​
            mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);
            mFrameScheduled = false;
            mLastFrameTimeNanos = frameTimeNanos;
        }
​
        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();
            //requestLayout 传递的type
            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.");
        }
    }

计算丢失多少帧的算法是:偏差时间/每帧时间 每帧的时间是根据屏幕刷新频率计算而来的,对于Android设备而言大不部分是60 但是也有其他的

Choreographer#doCallbacks

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;
            if (callbackType == Choreographer.CALLBACK_COMMIT) {
                final long jitterNanos = now - frameTimeNanos;
                Trace.traceCounter(Trace.TRACE_TAG_VIEW, "jitterNanos", (int) jitterNanos);
                if (jitterNanos >= 2 * mFrameIntervalNanos) {
                    final long lastFrameOffset = jitterNanos % mFrameIntervalNanos
                            + mFrameIntervalNanos;
                    if (DEBUG_JANK) {
                        Log.d(TAG, "Commit callback delayed by " + (jitterNanos * 0.000001f)
                                + " ms which is more than twice the frame interval of "
                                + (mFrameIntervalNanos * 0.000001f) + " ms!  "
                                + "Setting frame time to " + (lastFrameOffset * 0.000001f)
                                + " ms in the past.");
                        mDebugPrintNextFrameTimeDelta = true;
                    }
                    frameTimeNanos = now - lastFrameOffset;
                    mLastFrameTimeNanos = frameTimeNanos;
                }
            }
        }
        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);
        }
    }

ViewRootImpl$TraversalRunnable

在requestLayout的过程中分装的CallbackRecord最终会调用到ViewRootImpl$TraversalRunnable

final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }

ViewRootImpl#doTraversal

void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        //移除同步屏障
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
​
        if (mProfile) {
            Debug.startMethodTracing("ViewAncestor");
        }
        //开始View布局流程
        performTraversals();
​
        if (mProfile) {
            Debug.stopMethodTracing();
            mProfile = false;
        }
    }
}

Choreographer对上承接ViewRootImpl 发起的屏幕刷新请求,对下监听垂直同步信号,调用ViewRootImpl 进行ui更新。

小结:app卡顿原因

外部:

内存紧张,频繁gc导致ui线程被暂停

其它线程/进程 频繁抢占CPU app 因为得不到cpu而产生卡顿现象

内部:

消息队列中存在耗时的操作,导致doframe不能及时执行卡顿

view布局流程耗时导致cpu不能及时准备好GPU所需要的数据

消息队列中有其它的异步消息,导致doframe不能及时执行(我们在app开发中慎用异步消息)。