美文网首页
Choreographer与UI刷新

Choreographer与UI刷新

作者: feifei_fly | 来源:发表于2024-01-06 14:52 被阅读0次

一、View 刷新流程

以Textview为例 ,当我们通过setText改变TextView内容后,UI界面不会立刻改变,APP端会先向VSYNC服务请求,等到下一次VSYNC信号触发后,APP端的UI才真的开始刷新

image
  • setText最终调用invalidate申请重绘,最后会通过ViewParent递归到ViewRootImpl的invalidate
  • ViewRootImpl 调用scheduleTraversal, 插入一个同步屏障消息,然后通过Choreographer申请Vsync信号
  • Choreographer 收到同步信号后,将FrameDisplayEventReceiver封装成一个异步消息,发送给主线程
  • 该异步消息执行时,移除同步屏障 ,最终调用ViewRootImpl.doTravesal 执行view的测量、布局和绘制

二、源码分析:

ViewRootImpl.java

void invalidate() {
    mDirty.set(0, 0, mWidth, mHeight);
    if (!mWillDrawSoon) {
        scheduleTraversals();
    }
}

ViewRootImpl会调用scheduleTraversals准备重绘,但是,重绘一般不会立即执行。而是往Choreographer的Choreographer.CALLBACK_TRAVERSAL队列中添加了一个mTraversalRunnable,同时申请VSYNC,这个mTraversalRunnable要一直等到申请的VSYNC到来后才会被执行。

void scheduleTraversals() {

        if (!mTraversalScheduled) { // (1) mTraversalScheduled 变量 保证一次Vsync信号周期内,仅会执行一次mTraversalRunnable,一次doTravalsal
            mTraversalScheduled = true;
            // (2) 添加同步屏障
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            // (3) 注册Vsync信号 和m TraversalRunnable 回调
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
           
        }
    }

Choreographer.java


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

    // mFrameScheduled保证16ms内,只会申请一次垂直同步信号
    // scheduleFrameLocked可以被调用多次,但是mFrameScheduled保证下一个vsync到来之前,不会有新的请求发出
    private void scheduleFrameLocked(long now) {
        if (!mFrameScheduled) {
            mFrameScheduled = true;
            if (USE_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()) {
                    // 申请Vsync 信号
                    scheduleVsyncLocked();
                } else {
                    Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
                    msg.setAsynchronous(true);
                    mHandler.sendMessageAtFrontOfQueue(msg);
                }
            } 
        }
    }

scheduleFrameLocked跟上一个scheduleTraversals类似,也采用了利用mFrameScheduled来保证:在当前申请的VSYNC到来之前,不会再去请求新的VSYNC。

再VSYNC到来之后,Choreographer利用Handler将FrameDisplayEventReceiver封装成一个异步Message,发送到UI线程的MessageQueue

2.1、VSync信号的申请

FrameDisplayEventReceiver 集成自DisplayEventReceiver,并且本身一个Runnable

 private final class FrameDisplayEventReceiver extends DisplayEventReceiver
            implements Runnable {
                
            }

DisplayEventReceiver.java

    /**
     * Schedules a single vertical sync pulse to be delivered when the next
     * display frame begins.
     */
    @UnsupportedAppUsage
    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);
        }
    }

2.2、VSync信号的接收

FrameDisplayEventReceiver.scheduleVsync()通过native方法注册Vysnc信号,当Vysnc发出时 会执行FrameDisplayEventReceiver.onVsync()

onVsync()回调中,将FrameDisplayEventReceiver封装成一个异步消息,发送到主线程.

 private final class FrameDisplayEventReceiver extends DisplayEventReceiver
            implements Runnable {

        private boolean mHavePendingVsync;
        private long mTimestampNanos;
        private int mFrame;

        public FrameDisplayEventReceiver(Looper looper) {
            super(looper);
        }

        @Override
        public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
            @Override
        public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {

            long now = System.nanoTime();
            if (timestampNanos > now) {
            <!--正常情况,timestampNanos不应该大于now,一般是上传vsync的机制出了问题-->
                timestampNanos = now;
            }
            <!--如果上一个vsync同步信号没执行,那就不应该相应下一个(可能是其他线程通过某种方式请求的)-->
              if (mHavePendingVsync) {
                Log.w(TAG, "Already have a pending vsync event.  There should only be "
                        + "one at a time.");
            } else {
                mHavePendingVsync = true;
            }
            <!--timestampNanos其实是本次vsync产生的时间,从服务端发过来-->
            mTimestampNanos = timestampNanos;
            mFrame = frame;
            Message msg = Message.obtain(mHandler, this);
            <!--由于已经存在同步栅栏,所以VSYNC到来的Message需要作为异步消息发送过去-->
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
        }

        @Override
        public void run() {
            mHavePendingVsync = false;
            <!--这里的mTimestampNanos其实就是本次Vynsc同步信号到来的时候,但是执行这个消息的时候,可能延迟了-->
            doFrame(mTimestampNanos, mFrame);
        }
    }

之所以封装成异步Message,是因为前面添加了一个同步栅栏,同步消息不会被执行。UI线程被唤起,取出该消息,最终调用doFrame进行UI刷新重绘。

void doFrame(long frameTimeNanos, int frame) {
    final long startNanos;
    synchronized (mLock) {
    <!--做了很多东西,都是为了保证一次16ms有一次垂直同步信号,有一次input 、刷新、重绘-->
        if (!mFrameScheduled) {
            return; // no work to do
        }
       long intendedFrameTimeNanos = frameTimeNanos;
        startNanos = System.nanoTime();
        final long jitterNanos = startNanos - frameTimeNanos;
        <!--检查是否因为延迟执行掉帧,每大于16ms,就多掉一帧-->
        if (jitterNanos >= mFrameIntervalNanos) {
            final long skippedFrames = jitterNanos / mFrameIntervalNanos;
            <!--跳帧,其实就是上一次请求刷新被延迟的时间,但是这里skippedFrames为0不代表没有掉帧-->
            if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
            <!--skippedFrames很大一定掉帧,但是为 0,去并非没掉帧-->
                Log.i(TAG, "Skipped " + skippedFrames + " frames!  "
                        + "The application may be doing too much work on its main thread.");
            }
            final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
                <!--开始doFrame的真正有效时间戳-->
            frameTimeNanos = startNanos - lastFrameOffset;
        }

        if (frameTimeNanos < mLastFrameTimeNanos) {
            <!--这种情况一般是生成vsync的机制出现了问题,那就再申请一次-->
            scheduleVsyncLocked();
            return;
        }
          <!--intendedFrameTimeNanos是本来要绘制的时间戳,frameTimeNanos是真正的,可以在渲染工具中标识延迟VSYNC多少-->
        mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);
        <!--移除mFrameScheduled判断,说明处理开始了,-->
        mFrameScheduled = false;
        <!--更新mLastFrameTimeNanos-->
        mLastFrameTimeNanos = frameTimeNanos;
    }

    try {
         <!--真正开始处理业务-->
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
        <!--处理打包的move事件-->
        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 {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}

doFrame也采用了一个mFrameScheduled变量保证每次VSYNC中,只执行一次,doFrame里除了UI重绘,其实还处理了很多其他的事

  • 检测VSYNC被延迟多久执行,掉了多少帧,
  • 处理Touch事件(一般是MOVE)
  • 处理动画,
  • 处理UI。

doFrame在处理Choreographer.CALLBACK_TRAVERSAL的回调时(mTraversalRunnable),才是真正的开始处理View重绘:

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

回到ViewRootImpl调用doTraversal进行View树遍历,

// 这里是真正执行了,
void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        <!--移除同步栅栏,只有重绘才设置了栅栏,说明重绘的优先级还是挺高的,所有的同步消息必须让步-->
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
        performTraversals();
    }
}

doTraversal会先将栅栏移除,然后处理performTraversals,进行测量、布局、绘制,提交当前帧给SurfaceFlinger进行图层合成显示。以上多个boolean变量保证了每16ms最多执行一次UI重绘,这也是目前Android存在60FPS上限的原因。

VSYNC同步信号需要用户主动去请求才会收到,并且是单次有效。

三、同步屏障泄露问题

Barrier消息像一道栅栏,将消息队列里的普通消息先拦住,多数情况下是正常,但一旦异常,则很容易发生ANR

  • 正常情况:渲染刷新类先优先执行,等执行完以后,撤掉栅栏,普通消息(包括会导致ANR的消息)得以继续执行
  • 异常情况:Barrier存在泄漏,导致无法释放栅栏,普通消息卡住不动,UI假死,如果期间有Server或者Provider等消息超时,就会引发ANR
    SyncBarrier产生的问题往往是异步刷新导致的,比如:子线程触发invalidate()UI频繁更新,自定义View写法不对,surfaceview异步刷新等等

    // scheduleTraversals 中添加同步屏障,mTraversalBarrier 记录同步屏障ID
 
   @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

    void unscheduleTraversals() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
            mChoreographer.removeCallbacks(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        }
    }

scheduleTraversals在多个子线程中同时调用,可能多次进入mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();那么就会发送多个Barrier,但是在移除掉的时候,只移除当前成员变量mTraversalBarrier一个Barrier,多余的就会导致泄露

四、参考文章

Android VSYNC (Choreographer)与UI刷新原理分析

Choreographer原理及应用

ANR监听方案之SyncBarrier

相关文章

  • Android VSYNC (Choreographer)与UI

    从UI控件内容更改到被重新绘制到屏幕上,这中间到底经历了什么?另外,连续两次setTextView到底会触发几次U...

  • Android VSYNC (Choreographer)与UI

    从UI控件内容更改到被重新绘制到屏幕上,这中间到底经历了什么?另外,连续两次setTextView到底会触发几次U...

  • Android卡顿检查

    一:常见方案 1 通过UI 线程looper 2 通过Choreographer 1.1 通过UI 线程loope...

  • TinyDancer 分析

      上一篇 Animator 有提到过 Animator 界面刷新逻辑主要是通过 Choreographer 来完...

  • Android硬件加速过程分析

    绘制基础 安卓绘制框架 Choreographer绘制调度器。用于控制各个Surface的刷新步调,让所以Surf...

  • 多线程pThread-NSThread-GCD-NSOperat

    1、主线程(UI线程) ——顾名思义就是:与UI有关,处理UI事件的线程。 1.1、主线程的主要作用: 显示\刷新...

  • block中刷新UI

    block回调中刷新ui,日志提示: 意思是:需要在主线程中去刷新UI

  • Choreographer

    他是控制应用进程绘制时机的,会在收到Vsync信号后调用应用进程的刷新 Choreographer创建,他是跟Vi...

  • iOS NSThread

    NSQualityOfServiceUserInteractive与用户交互的任务,这些任务通常跟UI级别的刷新相...

  • iOS开发常用之目录

    目录 UI下拉刷新模糊效果自动版式富文本图表表相关与的TabBar隐藏与显示HUD与吐司对话框其他UI 动画侧滑与...

网友评论

      本文标题:Choreographer与UI刷新

      本文链接:https://www.haomeiwen.com/subject/kyewndtx.html