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

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

从UI控件内容更改到被重新绘制到屏幕上,这中间到底经历了什么?另外,连续两次setTextView到底会触发几次UI重绘呢?为什么Android APP的帧率最高是60FPS呢,这就是本文要讨论的内容。


以电影为例,动画至少要达到24FPS,才能保证画面的流畅性,低于这个值,肉眼会感觉到卡顿。在手机上,这个值被调整到60FPS,增加丝滑度,这也是为什么有个(1000/60)16ms的指标,一般而言目前的Android系统最高FPS也就是60,它是通过了一个VSYNC来保证每16ms最多绘制一帧。简而言之:UI必须至少等待16ms的间隔才会绘制下一帧,所以连续两次setTextView只会触发一次重绘。下面来具体看一下UI的重绘流程。


UI刷新流程示意


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

image.png

从我们的代码端来看如下:setText最终调用invalidate申请重绘,最后会通过ViewParent递归到ViewRootImpl的invalidate,请求VSYNC,在请求VSYNC的时候,会添加一个同步栅栏,防止UI线程中同步消息执行,这样做为了加快VSYNC的响应速度,如果不设置,VSYNC到来的时候,正在执行一个同步消息,那么UI更新的Task就会被延迟执行,这是Android的Looper跟MessageQueue决定的。

APP端触发重绘,申请VSYNC流程示意


image.png

等到VSYNC到来后,会移除同步栅栏,并率先开始执行当前帧的处理,调用逻辑如下

VSYNC回来流程示意

image.png

doFrame执行UI绘制的示意图

image.png


UI刷新源码跟踪


同TextView类似,View内容改变一般都会调用invalidate触发视图重绘,这中间经历了什么呢?View会递归的调用父容器的invalidateChild,逐级回溯,最终走到ViewRootImpl的invalidate,如下:

View.java

void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
            boolean fullInvalidate) {
            // Propagate the damage rectangle to the parent view.
            final AttachInfo ai = mAttachInfo;
            final ViewParent p = mParent;
            if (p != null && ai != null && l < r && t < b) {
                final Rect damage = ai.mTmpInvalRect;
                damage.set(l, t, r, b);
                p.invalidateChild(this, damage);
            }

ViewRootImpl.java

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

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

ViewRootImpl.java

// 将UI绘制的mTraversalRunnable加入到下次垂直同步信号到来的等待callback中去
 // mTraversalScheduled用来保证本次Traversals未执行前,不会要求遍历两边,浪费16ms内,不需要绘制两次
void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        // 防止同步栅栏,同步栅栏的意思就是拦截同步消息
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        // postCallback的时候,顺便请求vnsc垂直同步信号scheduleVsyncLocked
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
         <!--添加一个处理触摸事件的回调,防止中间有Touch事件过来-->
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}

Choreographer.java

private void postCallbackDelayedInternal(int callbackType,
        Object action, Object token, long delayMillis) {
    synchronized (mLock) {
        final long now = SystemClock.uptimeMillis();
        final long dueTime = now + delayMillis;
        mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
        if (dueTime <= now) {
        <!--申请VSYNC同步信号-->
            scheduleFrameLocked(now);
        } 
    }
}

scheduleTraversals利用mTraversalScheduled保证,在当前的mTraversalRunnable未被执行前,scheduleTraversals不会再被有效调用,也就是Choreographer.CALLBACK_TRAVERSAL理论上应该只有一个mTraversalRunnable的Task。mChoreographer.postCallback将mTraversalRunnable插入到CallBack之后,会接着调用scheduleFrameLocked请求Vsync同步信号

// mFrameScheduled保证16ms内,只会申请一次垂直同步信号
// scheduleFrameLocked可以被调用多次,但是mFrameScheduled保证下一个vsync到来之前,不会有新的请求发出
// 多余的scheduleFrameLocked调用被无效化
private void scheduleFrameLocked(long now) {
    if (!mFrameScheduled) {
        mFrameScheduled = true;
        if (USE_VSYNC) {
            if (isRunningOnLooperThreadLocked()) {
                scheduleVsyncLocked();
            } else {
                // 因为invalid已经有了同步栅栏,所以必须mFrameScheduled,消息才能被UI线程执行
                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
                msg.setAsynchronous(true);
                mHandler.sendMessageAtFrontOfQueue(msg);
            }
        }  
    }
}

scheduleFrameLocked跟上一个scheduleTraversals类似,也采用了利用mFrameScheduled来保证:在当前申请的VSYNC到来之前,不会再去请求新的VSYNC,因为16ms内申请两个VSYNC没意义。再VSYNC到来之后,Choreographer利用Handler将FrameDisplayEventReceiver封装成一个异步Message,发送到UI线程的MessageQueue,

  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) {
            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也采用了一个boolean遍历mFrameScheduled保证每次VSYNC中,只执行一次,可以看到,为了保证16ms只执行一次重绘,加了好多次层保障。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同步信号需要用户主动去请求才会收到,并且是单次有效。


UI局部重绘


某一个View重绘刷新,并不会导致所有View都进行一次measure、layout、draw,只是这个待刷新View链路需要调整,剩余的View可能不需要浪费精力再来一遍,反应再APP侧就是:不需要再次调用所有ViewupdateDisplayListIfDirty构建RenderNode渲染Op树,如下

View.java

public RenderNode updateDisplayListIfDirty() {
        final RenderNode renderNode = mRenderNode;
          ...
        if ((mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0
                || !renderNode.isValid()
                || (mRecreateDisplayList)) {
           <!--失效了,需要重绘-->
        } else {
        <!--依旧有效,无需重绘-->
            mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
            mPrivateFlags &= ~PFLAG_DIRTY_MASK;
        }
        return renderNode;
    }


总结


  • android最高60FPS,是VSYNC及决定的,每16ms最多一帧
  • VSYNC要客户端主动申请,才会有
  • 有VSYNC到来才会刷新
  • UI没更改,不会请求VSYNC也就不会刷新
  • UI局部重绘其实只是省去了再次构建硬件加速用的DrawOp树(复用上衣帧的)


目录
相关文章
|
2月前
|
搜索推荐 Android开发 开发者
探索安卓开发中的自定义视图:打造个性化UI组件
【10月更文挑战第39天】在安卓开发的世界中,自定义视图是实现独特界面设计的关键。本文将引导你理解自定义视图的概念、创建流程,以及如何通过它们增强应用的用户体验。我们将从基础出发,逐步深入,最终让你能够自信地设计和实现专属的UI组件。
|
2月前
|
缓存 Java 数据库
Android的ANR原理
【10月更文挑战第18天】了解 ANR 的原理对于开发高质量的 Android 应用至关重要。通过合理的设计和优化,可以有效避免 ANR 的发生,提升应用的性能和用户体验。
127 56
|
29天前
|
XML 搜索推荐 前端开发
安卓开发中的自定义视图:打造个性化UI组件
在安卓应用开发中,自定义视图是一种强大的工具,它允许开发者创造独一无二的用户界面元素,从而提升应用的外观和用户体验。本文将通过一个简单的自定义视图示例,引导你了解如何在安卓项目中实现自定义组件,并探讨其背后的技术原理。我们将从基础的View类讲起,逐步深入到绘图、事件处理以及性能优化等方面。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的见解和技巧。
|
2月前
|
XML 前端开发 Android开发
Android:UI:Drawable:View/ImageView与Drawable
通过本文的介绍,我们详细探讨了Android中Drawable、View和ImageView的使用方法及其相互关系。Drawable作为图像和图形的抽象表示,提供了丰富的子类和自定义能力,使得开发者能够灵活地实现各种UI效果。View和ImageView则通过使用Drawable实现了各种图像和图形的显示需求。希望本文能为您在Android开发中使用Drawable提供有价值的参考和指导。
45 2
|
3月前
|
XML 前端开发 Android开发
Android View的绘制流程和原理详细解说
Android View的绘制流程和原理详细解说
56 3
|
4月前
|
ARouter 测试技术 API
Android经典面试题之组件化原理、优缺点、实现方法?
本文介绍了组件化在Android开发中的应用,详细阐述了其原理、优缺点及实现方式,包括模块化、接口编程、依赖注入、路由机制等内容,并提供了具体代码示例。
54 2
|
4月前
|
XML Android开发 UED
💥Android UI设计新风尚!掌握Material Design精髓,让你的界面颜值爆表!🎨
随着移动应用市场的蓬勃发展,用户对界面设计的要求日益提高。为此,掌握由Google推出的Material Design设计语言成为提升应用颜值和用户体验的关键。本文将带你深入了解Material Design的核心原则,如真实感、统一性和创新性,并通过丰富的组件库及示例代码,助你轻松打造美观且一致的应用界面。无论是色彩搭配还是动画效果,Material Design都能为你的Android应用增添无限魅力。
94 1
|
3月前
|
Java 调度 Android开发
Android面试题之Kotlin中async 和 await实现并发的原理和面试总结
本文首发于公众号“AntDream”,详细解析了Kotlin协程中`async`与`await`的原理及其非阻塞特性,并提供了相关面试题及答案。协程作为轻量级线程,由Kotlin运行时库管理,`async`用于启动协程并返回`Deferred`对象,`await`则用于等待该对象完成并获取结果。文章还探讨了协程与传统线程的区别,并展示了如何取消协程任务及正确释放资源。
49 0
|
2月前
|
开发框架 前端开发 Android开发
安卓与iOS开发中的跨平台策略
在移动应用开发的战场上,安卓和iOS两大阵营各据一方。随着技术的演进,跨平台开发框架成为开发者的新宠,旨在实现一次编码、多平台部署的梦想。本文将探讨跨平台开发的优势与挑战,并分享实用的开发技巧,帮助开发者在安卓和iOS的世界中游刃有余。
|
28天前
|
搜索推荐 前端开发 API
探索安卓开发中的自定义视图:打造个性化用户界面
在安卓应用开发的广阔天地中,自定义视图是一块神奇的画布,让开发者能够突破标准控件的限制,绘制出独一无二的用户界面。本文将带你走进自定义视图的世界,从基础概念到实战技巧,逐步揭示如何在安卓平台上创建和运用自定义视图来提升用户体验。无论你是初学者还是有一定经验的开发者,这篇文章都将为你打开新的视野,让你的应用在众多同质化产品中脱颖而出。
53 19