本篇文章已受权微信公众号 guolin_blog (郭霖)独家发布android
此次就来梳理一下 Android 的屏幕刷新机制,把我这段时间由于研究动画而梳理出来的一些关于屏幕刷新方面的知识点分享出来,能力有限,有错的地方还望指点一下。另外,内容有点多,毕竟要讲清楚不容易,因此慢慢看哈。缓存
阅读源码仍是得带着问题或目的性的去阅读,这样阅读过程当中比较有条理性,不会跟偏或太深刻,因此,仍是先来几个问题吧:性能优化
大伙都清楚,Android 每隔 16.6ms 会刷新一次屏幕。微信
Q1:可是大伙想过没有,这个 16.6ms 刷新一次屏幕究竟是什么意思呢?是指每隔 16.6ms 调用 onDraw() 绘制一次么?app
Q2:若是界面一直保持没变的话,那么还会每隔 16.6ms 刷新一次屏幕么?框架
Q3:界面的显示其实就是一个 Activity 的 View 树里全部的 View 都进行测量、布局、绘制操做以后的结果呈现,那么若是这部分工做都完成后,屏幕会立刻就刷新么?异步
Q4:网上都说避免丢帧的方法之一是保证每次绘制界面的操做要在 16.6ms 内完成,但若是这个 16.6ms 是一个固定的频率的话,请求绘制的操做在代码里被调用的时机是不肯定的啊,那么若是某次用户点击屏幕致使的界面刷新操做是在某一个 16.6ms 帧快结束的时候,那么即便此次绘制操做小于 16.6 ms,按道理不也会形成丢帧么?这又该如何理解?oop
Q5:大伙都清楚,主线程耗时的操做会致使丢帧,可是耗时的操做为何会致使丢帧?它是如何致使丢帧发生的?源码分析
本篇主要就是搞清楚这几个问题,分析的源码基本只涉及 ViewRootImpl 和 Choreographer 这两个类。布局
ps:本篇分析的源码均是 android-25 版本,版本不同,源码可能会有些许差别,大伙过的时候注意一下。
首先,先来过一下一些基本概念,摘抄自网上文章android屏幕刷新显示机制:
在一个典型的显示系统中,通常包括CPU、GPU、display三个部分, CPU负责计算数据,把计算好数据交给GPU,GPU会对图形数据进行渲染,渲染好后放到buffer里存起来,而后display(有的文章也叫屏幕或者显示器)负责把buffer里的数据呈现到屏幕上。
显示过程,简单的说就是CPU/GPU准备好数据,存入buffer,display每隔一段时间去buffer里取数据,而后显示出来。display读取的频率是固定的,好比每一个16ms读一次,可是CPU/GPU写数据是彻底无规律的。
上述内容归纳一下,大致意思就是说,屏幕的刷新包括三个步骤:CPU 计算屏幕数据、GPU 进一步处理和缓存、最后 display 再将缓存中(buffer)的屏幕数据显示出来。
(ps:开发过程当中应该接触不到 GPU、display 这些层面的东西,因此我把这部分工做都称做底层的工做了,下文出现的底层指的就是除了 CPU 计算屏幕数据以外的工做。)
对于 Android 而言,第一个步骤:CPU 计算屏幕数据指的也就是 View 树的绘制过程,也就是 Activity 对应的视图树从根布局 DecorView 开始层层遍历每一个 View,分别执行测量、布局、绘制三个操做的过程。
也就是说,咱们常说的 Android 每隔 16.6ms 刷新一次屏幕实际上是指:底层以固定的频率,好比每 16.6ms 将 buffer 里的屏幕数据显示出来。
若是还不清楚,那再看一张网上很常见的图(摘自上面同一篇文章):
结合这张图,再来说讲 16.6 ms 屏幕刷新一次的意思。
Display 这一行能够理解成屏幕,因此能够看到,底层是以固定的频率发出 VSync 信号的,而这个固定频率就是咱们常说的每 16.6ms 发送一个 VSync 信号,至于什么叫 VSync 信号,咱们能够不用深刻去了解,只要清楚这个信号就是屏幕刷新的信号就能够了。
继续看图,Display 黄色的这一行里有一些数字:0, 1, 2, 3, 4
,能够看到每次屏幕刷新信号到了的时候,数字就会变化,因此这些数字其实能够理解成每一帧屏幕显示的画面。也就是说,屏幕每一帧的画面能够持续 16.6ms,当过了 16.6ms,底层就会发出一个屏幕刷新信号,而屏幕就会去显示下一帧的画面。
以上都是一些基本概念,也都是底层的工做,咱们了解一下就能够了。接下去就仍是看这图,而后讲讲咱们 app 层该干的事了:
继续看图,CPU 蓝色的这行,上面也说过了,CPU 这块的耗时其实就是咱们 app 绘制当前 View 树的时间,而这段时间就跟咱们本身写的代码有关系了,若是你的布局很复杂,层次嵌套不少,每一帧内须要刷新的 View 又不少时,那么每一帧的绘制耗时天然就会多一点。
继续看图,CPU 蓝色这行里也有一些数字,其实这些数字跟 Display 黄色的那一行里的数字是对应的,在 Display 里咱们解释过这些数字表示的是每一帧的画面,那么在 CPU 这一行里,其实就是在计算对应帧的画面数据,也叫屏幕数据。也就是说,在当前帧内,CPU 是在计算下一帧的屏幕画面数据,当屏幕刷新信号到的时候,屏幕就去将 CPU 计算的屏幕画面数据显示出来;同时 CPU 也接收到屏幕刷新信号,因此也开始去计算下一帧的屏幕画面数据。
CPU 跟 Display 是不一样的硬件,它们是能够并行工做的。要理解的一点是,咱们写的代码,只是控制让 CPU 在接收到屏幕刷新信号的时候开始去计算下一帧的画面工做。而底层在每一次屏幕刷新信号来的时候都会去切换这一帧的画面,这点咱们是控制不了的,是底层的工做机制。之因此要讲这点,是由于,当咱们的 app 界面没有必要再刷新时(好比用户不操做了,当前界面也没动画),这个时候,咱们 app 是接收不到屏幕刷新信号的,因此也就不会让 CPU 去计算下一帧画面数据,可是底层仍然会以固定的频率来切换每一帧的画面,只是它后面切换的每一帧画面都同样,因此给咱们的感受就是屏幕没刷新。
因此,我以为上面那张图还能够再继续延深几帧的长度,这样就更容易理解了:
我在那张图的基础上延长了几帧,我想这样应该能够更容易理解点。
看我画的这张图,前三帧跟原图同样,从第三帧以后,由于咱们的 app 界面不须要刷新了(用户不操做了,界面也没有动画),那么这以后咱们 app 就不会再接收到屏幕刷新信号了,因此也就不会再让 CPU 去绘制视图树来计算下一帧画面了。可是,底层仍是会每隔 16.6ms 发出一个屏幕刷新信号,只是咱们 app 不会接收到而已,Display 仍是会在每个屏幕刷新信号到的时候去显示下一帧画面,只是下一帧画面一直是第4帧的内容而已。
好了,到这里 Q1,Q2,Q3 均可以先回答一半了,那么咱们就先稍微来梳理一下:
咱们常说的 Android 每隔 16.6 ms 刷新一次屏幕实际上是指底层会以这个固定频率来切换每一帧的画面。
这个每一帧的画面也就是咱们的 app 绘制视图树(View 树)计算而来的,这个工做是交由 CPU 处理,耗时的长短取决于咱们写的代码:布局复不复杂,层次深不深,同一帧内刷新的 View 的数量多很少。
CPU 绘制视图树来计算下一帧画面数据的工做是在屏幕刷新信号来的时候才开始工做的,而当这个工做处理完毕后,也就是下一帧的画面数据已经所有计算完毕,也不会立刻显示到屏幕上,而是会等下一个屏幕刷新信号来的时候再交由底层将计算完毕的屏幕画面数据显示出来。
当咱们的 app 界面不须要刷新时(用户无操做,界面无动画),app 就接收不到屏幕刷新信号因此也就不会让 CPU 再去绘制视图树计算画面数据工做,可是底层仍然会每隔 16.6 ms 切换下一帧的画面,只是这个下一帧画面一直是相同的内容。
这部分虽说是一些基本概念,但其实也包含了一些结论了,因此可能大伙看着会有些困惑:**为何界面不刷新时 app 就接收不到屏幕刷新信号了?为何绘制视图树计算下一帧画面的工做会是在屏幕刷新信号来的时候才开始的?**等等。
emmm,有这些困惑很棒,这样,咱们下面一块儿过源码时,大伙就更有目的性了,这样过源码我以为效率是比较高一点的。继续看下去,跟着过完源码,你就清楚为何了。好了,那咱们下面就开始过源码了。
阅读源码从哪开始看起一直都是个头疼的问题,因此找一个合适的切入点来跟的话,整个梳理的过程可能会顺畅一点。本篇是研究屏幕的刷新,那么建议就是从某个会致使屏幕刷新的方法入手,好比 View#invalidate()
。
View#invalidate()
是请求重绘的一个操做,因此咱们切入点能够从这个方法开始一步步跟下去。咱们在上一篇博客View 动画 Animation 运行原理解析已经分析过 View#invalidate()
这个方法了。
想再过一遍的能够再去看看,咱们这里就直接说结论了。咱们跟着 invalidate()
一步步往下走的时候,发现最后跟到了 ViewRootImpl#scheduleTraversals()
就中止了。而 ViewRootImpl 就是今天咱们要介绍的重点对象了。
大伙都清楚,Android 设备呈现到界面上的大多数状况下都是一个 Activity,真正承载视图的是一个 Window,每一个 Window 都有一个 DecorView,咱们调用 setContentView()
实际上是将咱们本身写的布局文件添加到以 DecorView 为根布局的一个 ViewGroup 里,构成一颗 View 树。
这些大伙都清楚,每一个 Activity 对应一颗以 DecorView 为根布局的 View 树,但其实 DecorView 还有 mParent,并且就是 ViewRootImpl,并且每一个界面上的 View 的刷新,绘制,点击事件的分发其实都是由 ViewRootImpl 做为发起者的,由 ViewRootImpl 控制这些操做从 DecorView 开始遍历 View 树去分发处理。
在上一篇动画分析的博客里,分析 View#invalidate()
时,也能够看到内部实际上是有一个 do{}while() 循环来不断寻找 mParent,因此最终才会走到 ViewRootImpl 里去,那么可能大伙就会疑问了,为何 DecorView 的 mParent 会是 ViewRootImpl 呢?换个问法也就是,在何时将 DevorView 和 ViewRootImpl 绑定起来?
Activity 的启动是在 ActivityThread 里完成的,handleLaunchActivity()
会依次间接的执行到 Activity 的 onCreate()
, onStart()
, onResume()
。在执行完这些后 ActivityThread 会调用 WindowManager#addView()
,而这个 addView()
最终实际上是调用了 WindowManagerGlobal 的 addView()
方法,咱们就从这里开始看:
WindowManager 维护着全部 Activity 的 DecorView 和 ViewRootImpl。这里初始化了一个 ViewRootImpl,而后调用了它的 setView()
方法,将 DevorView 做为参数传递了进去。因此看看 ViewRootImpl 中的 setView()
作了什么:
在 setView()
方法里调用了 DecorView 的 assignParent()
方法,因此去看看 View 的这个方法:
参数是 ViewParent,而 ViewRootImpl 是实现了 ViewParent 接口的,因此在这里就将 DecorView 和 ViewRootImpl 绑定起来了。每一个Activity 的根布局都是 DecorView,而 DecorView 的 parent 又是 ViewRootImpl,因此在子 View 里执行 invalidate()
之类的操做,循环找 parent 时,最后都会走到 ViewRootImpl 里来。
跟界面刷新相关的方法里应该都会有一个循环找 parent 的方法,或者是不断调用 parent 的方法,这样最终才都会走到 ViewRootImpl 里,也就是说实际上 View 的刷新都是由 ViewRootImpl 来控制的。
即便是界面上一个小小的 View 发起了重绘请求时,都要层层走到 ViewRootImpl,由它来发起重绘请求,而后再由它来开始遍历 View 树,一直遍历到这个须要重绘的 View 再调用它的 onDraw()
方法进行绘制。
咱们从新看回 ViewRootImpl 的 setView()
这个方法,这个方法里还调用了一个 requestLayout()
方法:
这里调用了一个 scheduleTraversals()
,还记得当 View 发起重绘操做 invalidate()
时,最后也调用了 scheduleTraversals()
这个方法么。其实这个方法就是屏幕刷新的关键,它是安排一次绘制 View 树的任务等待执行,具体后面再说。
也就是说,其实打开一个 Activity,当它的 onCreate---onResume 生命周期都走完后,才将它的 DecoView 与新建的一个 ViewRootImpl 对象绑定起来,同时开始安排一次遍历 View 任务也就是绘制 View 树的操做等待执行,而后将 DecoView 的 parent 设置成 ViewRootImpl 对象。
这也就是为何在 onCreate---onResume
里获取不到 View 宽高的缘由,由于在这个时刻 ViewRootImpl 甚至都还没建立,更不用说是否已经执行过测量操做了。
还能够获得一点信息是,一个 Activity 界面的绘制,实际上是在 onResume()
以后才开始的。
到这里,咱们梳理清楚了,调用一个 View 的 invalidate()
请求重绘操做,内部原来是要层层通知到 ViewRootImpl 的 scheduleTraversals()
里去。并且打开一个新的 Activity,它的界面绘制原来是在 onResume()
以后也层层通知到 ViewRootImpl 的 scheduleTraversals()
里去。虽然其余关于 View 的刷新操做,好比 requestLayout()
等等之类的方法咱们尚未去看,但咱们已经能够大胆猜想,这些跟 View 刷新有关的操做最终也都会层层走到 ViewRootImpl 中的 scheduleTraversals()
方法里去的。
那么这个方法究竟干了些什么,咱们就要好好来分析了:
mTraversalScheduled 这个 boolean 变量的做用等会再来看,先看看 mChoreographer.postCallback()
这个方法,传入了三个参数,第二个参数是一个 Runnable 对象,先来看看这个 Runnable:
这个 Runnable 作的事很简单,就调用了一个方法,doTraversal()
:
看看这个方法作的事,跟 scheduleTraversals()
正好相反,一个将变量置成 true,这里置成 false,一个是 postSyncBarrier()
,这里是 removeSyncBarrier()
,具体做用等会再说,继续先看看 performTraversals()
,这个方法也是屏幕刷新的关键:
View 的测量、布局、绘制三大流程都是交由 ViewRootImpl 发起,并且还都是在 performTraversals()
方法中发起的,因此这个方法的逻辑很复杂,由于每次都须要根据相应状态判断是否须要三个流程都走,有时可能只须要执行 performDraw()
绘制流程,有时可能只执行 performMeasure()
测量和 performLayout()
布局流程(通常测量和布局流程是一块儿执行的)。无论哪一个流程都会遍历一次 View 树,因此其实界面的绘制是须要遍历不少次的,若是页面层次太过复杂,每一帧须要刷新的 View 又不少时,耗时就会长一点。
固然,测量、布局、绘制这些流程在遍历时并不必定会把整颗 View 树都遍历一遍,ViewGroup 在传递这些流程时,还会再根据相应状态判断是否须要继续往下传递。
了解了 performTraversals()
是刷新界面的源头后,接下去就须要了解下它是何时执行的,和 scheduleTraversals()
又是什么关系?
performTraversals()
是在 doTraversal()
中被调用的,而 doTraversal()
又被封装到一个 Runnable 里,那么关键就是这个 Runnable 何时被执行了?
scheduleTraversals()
里调用了 Choreographer 的 postCallback()
将 Runnable 做为参数传了进去,因此跟进去看看:
由于 postCallback()
调用 postCallbackDelayed()
时传了 delay = 0 进去,因此在 postCallbackDelayedInternal()
里面会先根据当前时间戳将这个 Runnable 保存到一个 mCallbackQueue 队列里,这个队列跟 MessageQueue 很类似,里面待执行的任务都是根据一个时间戳来排序。而后走了 scheduleFrameLocked()
方法这边,看看作了些什么:
若是代码走了 else 这边来发送一个消息,那么这个消息作的事确定很重要,由于对这个 Message 设置了异步的标志并且用了sendMessageAtFrontOfQueue()
方法,这个方法是将这个 Message 直接放到 MessageQueue 队列里的头部,能够理解成设置了这个 Message 为最高优先级,那么先看看这个 Message 作了些什么:
因此这个 Message 最后作的事就是 scheduleVsyncLocked()
。咱们回到 scheduleFrameLocked()
这个方法里,当走 if 里的代码时,直接调用了 scheduleVsyncLocked()
,当走 else 里的代码时,发了一个最高优先级的 Message,这个 Message 也是执行 scheduleVsyncLocked()
。既然两边最后调用的都是同一个方法,那么为何这么作呢?
关键在于 if 条件里那个方法,个人理解那个方法是用来判断当前是不是在主线程的,咱们知道主线程也是一直在执行着一个个的 Message,那么若是在主线程的话,直接调用这个方法,那么这个方法就能够直接被执行了,若是不是在主线程,那么 post 一个最高优先级的 Message 到主线程去,保证这个方法能够第一时间获得处理。
那么这个方法是干吗的呢,为何须要在最短期内被执行呢,并且只能在主线程?
调用了 native 层的一个方法,那跟到这里就跟不下去了。
那到这里,咱们先来梳理一下:
到这里为止,咱们知道一个 View 发起刷新的操做时,会层层通知到 ViewRootImpl 的 scheduleTraversals() 里去,而后这个方法会将遍历绘制 View 树的操做 performTraversals() 封装到 Runnable 里,传给 Choreographer,以当前的时间戳放进一个 mCallbackQueue 队列里,而后调用了 native 层的一个方法就跟不下去了。因此这个 Runnable 何时会被执行还不清楚。那么,下去的重点就是搞清楚它何时从队列里被拿出来执行了?
接下去只能换种方式继续跟了,既然这个 Runnable 操做被放在一个 mCallbackQueue 队列里,那就从这个队列着手,看看这个队列的取操做在哪被执行了:
还记得咱们说过在 ViewRootImpl 的 scheduleTraversals()
里会将遍历 View 树绘制的操做封装到 Runnable 里,而后调用 Choreographer 的 postCallback()
将这个 Runnable 放进队列里么,而当时调用 postCallback()
时传入了多个参数,这是由于 Choreographer 里有多个队列,而第一个参数 Choreographer.CALLBACK_TRAVERSAL 这个参数是用来区分队列的,能够理解成各个队列的 key 值。
那么这样一来,就找到关键的方法了:doFrame()
,这个方法里会根据一个时间戳去队列里取任务出来执行,而这个任务就是 ViewRootImpl 封装起来的 doTraversal()
操做,而 doTraversal()
会去调用 performTraversals()
开始根据须要测量、布局、绘制整颗 View 树。因此剩下的问题就是 doFrame()
这个方法在哪里被调用了。
有几个调用的地方,但有个地方很关键:
关键的地方来了,这个继承自 DisplayEventReceiver 的 FrameDisplayEventReceiver 类的做用很重要。跟进去看注释,我只能理解它是用来接收底层信号用的。但看了网上的解释后,全部的都理解过来了:
FrameDisplayEventReceiver继承自DisplayEventReceiver接收底层的VSync信号开始处理UI过程。VSync信号由SurfaceFlinger实现并定时发送。FrameDisplayEventReceiver收到信号后,调用onVsync方法组织消息发送到主线程处理。这个消息主要内容就是run方法里面的doFrame了,这里mTimestampNanos是信号到来的时间参数。
也就是说,onVsync()
是底层会回调的,能够理解成每隔 16.6ms 一个帧信号来的时候,底层就会回调这个方法,固然前提是咱们得先注册,这样底层才能找到咱们 app 并回调。当这个方法被回调时,内部发起了一个 Message,注意看代码对这个 Message 设置了 callback 为 this,Handler 在处理消息时会先查看 Message 是否有 callback,有则优先交由 Message 的 callback 处理消息,没有的话再去看看Handler 有没有 callback,若是也没有才会交由 handleMessage()
这个方法执行。
这里这么作的缘由,我猜想可能 onVsync()
是由底层回调的,那么它就不是运行在咱们 app 的主线程上,毕竟上层 app 对底层是隐藏的。但这个 doFrame()
是个 ui 操做,它须要在主线程中执行,因此才经过 Handler 切到主线程中。
还记得咱们前面分析 scheduleTraversals()
方法时,最后跟到了一个 native 层方法就跟不下去了么,如今再回过来想一想这个 native 层方法的做用是什么,应该就比较好猜想了。
英文不大理解,大致上多是说安排接收一个 vsync 信号。而根据咱们的分析,若是这个 vsync 信号发出的话,底层就会回调 DisplayEventReceiver 的 onVsync()
方法。
那若是只是这样的话,就有一点说不通了,首先上层 app 对于这些发送 vsync 信号的底层来讲确定是隐藏的,也就是说底层它根本不知道上层 app 的存在,那么在它的每 16.6ms 的帧信号来的时候,它是怎么找到咱们的 app,并回调它的方法呢?
这就有点相似于观察者模式,或者说发布-订阅模式。既然上层 app 须要知道底层每隔 16.6ms 的帧信号事件,那么它就须要先注册监听才对,这样底层在发信号的时候,直接去找这些观察者通知它们就好了。
这是个人理解,因此,这样一来,scheduleVsync()
这个调用到了 native 层方法的做用大致上就能够理解成注册监听了,这样底层也才找获得上层 app,并在每 16.6ms 刷新信号发出的时候回调上层 app 的 onVsync() 方法。这样一来,应该就说得通了。
还有一点,scheduleVsync()
注册的监听应该只是监听下一个屏幕刷新信号的事件而已,而不是监听全部的屏幕刷新信号。好比说当前监听了第一帧的刷新信号事件,那么当第一帧的刷新信号来的时候,上层 app 就能接收到事件并做出反应。但若是还想监听第二帧的刷新信号,那么只能等上层 app 接收到第一帧的刷新信号以后再去监听下一帧。
虽然如今能力还不足以跟踪到 native 层,这些结论虽然是猜想的,但都通过调试,对注释、代码理解以后梳理出来的结论,跟原理应该不会误差太多,这样子的理解应该是能够的。
本篇内容确实有点多,因此到这里仍是继续来先来梳理一下目前的信息,防止都忘记上面讲了些什么:
咱们知道一个 View 发起刷新的操做时,最终是走到了 ViewRootImpl 的 scheduleTraversals() 里去,而后这个方法会将遍历绘制 View 树的操做 performTraversals() 封装到 Runnable 里,传给 Choreographer,以当前的时间戳放进一个 mCallbackQueue 队列里,而后调用了 native 层的方法向底层注册监听下一个屏幕刷新信号事件。
当下一个屏幕刷新信号发出的时候,若是咱们 app 有对这个事件进行监听,那么底层它就会回调咱们 app 层的 onVsync() 方法来通知。当 onVsync() 被回调时,会发一个 Message 到主线程,将后续的工做切到主线程来执行。
切到主线程的工做就是去 mCallbackQueue 队列里根据时间戳将以前放进去的 Runnable 取出来执行,而这些 Runnable 有一个就是遍历绘制 View 树的操做 performTraversals()。在此次的遍历操做中,就会去绘制那些须要刷新的 View。
因此说,当咱们调用了 invalidate(),requestLayout(),等之类刷新界面的操做时,并非立刻就会执行这些刷新的操做,而是经过 ViewRootImpl 的 scheduleTraversals() 先向底层注册监听下一个屏幕刷新信号事件,而后等下一个屏幕刷新信号来的时候,才会去经过 performTraversals() 遍历绘制 View 树来执行这些刷新操做。
总体上的流程咱们已经梳理出来的,但还有几点问题须要解决。咱们在一个 16.6ms 的一帧内,代码里可能会有多个 View 发起了刷新请求,这是很是常见的场景了,好比某个动画是有多个 View 一块儿完成,好比界面发生了滑动等等。
按照咱们上面梳理的流程,只要 View 发起了刷新请求最终都会走到 ViewRootImpl 中的 scheduleTraversals()
里去,是吧。而这个方法又会封装一个遍历绘制 View 树的操做 performTraversals()
到 Runnable 而后扔到队列里等刷新信号来的时候取出来执行,没错吧。
那若是多个 View 发起了刷新请求,岂不是意味着会有屡次遍历绘制 View 树的操做?
其实,这点不用担忧,还记得咱们在最开始分析 scheduleTraverslas()
的时候先跳过了一些代码么?如今咱们回过来继续看看这些代码:
咱们上面分析的 scheduleTraversals()
干的那一串工做,前提是 mTraversalScheduled 这个 boolean 类型变量等于 false 才会去执行。那这个变量在何时被赋值被 false 了呢:
只有三个被赋值为 false 的地方,一个是上图的 doTraversal()
,还有就是声明时默认为 false,剩下一个是在取消遍历绘制 View 操做 unscheduleTraversals()
里。这两个能够先不去看,就看看 doTraversal()
。还记得这个方法吧,就是在 scheduleTraversals()
中封装到 Runnable 里的那个方法。
也就是说,当咱们调用了一次 scheduleTraversals()
以后,直到下一个屏幕刷新信号来的时候,doTraversal()
被取出来执行。在这期间重复调用 scheduleTraversals()
都会被过滤掉的。那么为何须要这样呢?
其实,想一想就能明白了。View 最终是怎么刷新的呢,就是在执行 performTraversals()
遍历绘制 View 树过程当中层层遍历到须要刷新的 View,而后去绘制它的吧。既然是遍历,那么无论上一帧内有多少个 View 发起了刷新的请求,在这一次的遍历过程当中所有都会去处理的吧。这也是咱们从代码上看到的,每个屏幕刷新信号来的时候,只会去执行一次 performTraversals()
,由于只需遍历一遍,就可以刷新全部的 View 了。
而 performTraversals()
会被执行的前提是调用了 scheduleTraversals()
来向底层注册监听了下一个屏幕刷新信号事件,因此在同一个 16.6ms 的一帧内,只须要第一个发起刷新请求的 View 来走一遍 scheduleTraversals()
干的事就能够了,其余无论还有多少 View 发起了刷新请求,不必再去重复向底层注册监听下一个屏幕刷新信号事件了,反正只要有一次遍历绘制 View 树的操做就能够对它们进行刷新了。
还剩最后一个问题,scheduleTraversals()
里咱们还有一行代码没分析。这个问题是这样的:
咱们清楚主线程实际上是一直在处理 MessageQueue 消息队列里的 Message,每一个操做都是一个 Message,打开 Activity 是一个 Message,遍历绘制 View 树来刷新屏幕也是一个 Message。
并且,上面梳理完咱们也清楚,遍历绘制 View 树的操做是在屏幕刷新信号到的时候,底层回调咱们 app 的 onVsync()
,这个方法再去将遍历绘制 View 树的操做 post 到主线程的 MessageQueue 中去等待执行。主线程同一时间只能处理一个 Message,这些 Message 就确定有前后的问题,那么会不会出现下面这种状况呢:
也就是说,当咱们的 app 接收到屏幕刷新信号时,来不及第一时间就去执行刷新屏幕的操做,这样一来,即便咱们将布局优化得很完全,保证绘制当前 View 树不会超过 16ms,但若是不能第一时间优先处理绘制 View 的工做,那等 16.6 ms 过了,底层须要去切换下一帧的画面了,咱们 app 却还没处理完,这样也照样会出现丢帧了吧。并且这种场景是很是有可能出现的吧,毕竟主线程须要处理的事确定不只仅是刷新屏幕的事而已,那么这个问题是怎么处理的呢?
因此咱们继续回来看 scheduleTraversals()
:
在逻辑走进 Choreographer 前会先往队列里发送一个同步屏障,而当 doTraversal()
被调用时才将同步屏障移除。这个同步屏障又涉及到消息机制了,不深刻了,这里就只给出结论。
这个同步屏障的做用能够理解成拦截同步消息的执行,主线程的 Looper 会一直循环调用 MessageQueue 的 next()
来取出队头的 Message 执行,当 Message 执行完后再去取下一个。当 next()
方法在取 Message 时发现队头是一个同步屏障的消息时,就会去遍历整个队列,只寻找设置了异步标志的消息,若是有找到异步消息,那么就取出这个异步消息来执行,不然就让 next()
方法陷入阻塞状态。若是 next()
方法陷入阻塞状态,那么主线程此时就是处于空闲状态的,也就是没在干任何事。因此,若是队头是一个同步屏障的消息的话,那么在它后面的全部同步消息就都被拦截住了,直到这个同步屏障消息被移除出队列,不然主线程就一直不会去处理同步屏幕后面的同步消息。
而全部消息默认都是同步消息,只有手动设置了异步标志,这个消息才会是异步消息。另外,同步屏障消息只能由内部来发送,这个接口并无公开给咱们使用。
最后,仔细看上面 Choreographer 里全部跟 message 有关的代码,你会发现,都手动设置了异步消息的标志,因此这些操做是不受到同步屏障影响的。这样作的缘由可能就是为了尽量保证上层 app 在接收到屏幕刷新信号时,能够在第一时间执行遍历绘制 View 树的工做。
由于主线程中若是有太多消息要执行,而这些消息又是根据时间戳进行排序,若是不加一个同步屏障的话,那么遍历绘制 View 树的工做就可能被迫延迟执行,由于它也须要排队,那么就有可能出现当一帧都快结束的时候才开始计算屏幕数据,那即便此次的计算少于 16.6ms,也一样会形成丢帧现象。
那么,有了同步屏障消息的控制就能保证每次一接收到屏幕刷新信号就第一时间处理遍历绘制 View 树的工做么?
只能说,同步屏障是尽量去作到,但并不能保证必定能够第一时间处理。由于,同步屏障是在 scheduleTraversals()
被调用时才发送到消息队列里的,也就是说,只有当某个 View 发起了刷新请求时,在这个时刻后面的同步消息才会被拦截掉。若是在 scheduleTraversals()
以前就发送到消息队列里的工做仍然会按顺序依次被取出来执行。
最后,就是上文常常说的一点,全部跟界面刷新相关的操做,其实最终都会走到 ViewRootImpl 中的 scheduleTraversals()
去的。
大伙能够想一想,跟界面刷新有关的操做有哪些,大概就是下面几种场景吧:
在上一篇分析动画的博客里,咱们跟踪了 invalidate()
,确实也是这样,至于其余的我并无一一去验证,大伙有兴趣能够看看,我猜想,这些跟界面刷新有关的方法内部要么就是一个 do{}while() 循环寻找 mParent,要么就是直接不断的调用 mParent 的方法。而一颗 View 树最顶端的 mParent 就是 ViewRootImpl,因此这些跟界面刷新相关的方法,在 ViewRootImpl 确定也是能够找到的:
其实,之前我一直觉得若是界面上某个小小的 View 发起了 invalidate()
重绘之类的操做,那么应该就只是它本身的 onLayout()
, onDraw()
被调用来重绘而已。最后才清楚,原来,即便再小的 View,若是发起了重绘的请求,那么也须要先层层走到 ViewRootImpl 里去,并且还不是立刻就执行重绘操做,而是须要等待下一个屏幕刷新信号来的时候,再从 DecorView 开始层层遍历到这些须要刷新的 View 里去重绘它们。
本篇篇幅确实很长,由于这部份内容要理清楚不容易,要讲清楚更不容易,大伙若是有时间,能够静下心来慢慢看,从头看下来,我相信,多少会有些收获的。若是没时间,那么也能够直接看看总结。
再来一张时序图结尾,大伙想本身过源码时能够跟着时序图来,建议在电脑上阅读:
Q1:Android 每隔 16.6 ms 刷新一次屏幕到底指的是什么意思?是指每隔 16.6ms 调用 onDraw() 绘制一次么?
Q2:若是界面一直保持没变的话,那么还会每隔 16.6ms 刷新一次屏幕么?
答:咱们常说的 Android 每隔 16.6 ms 刷新一次屏幕实际上是指底层会以这个固定频率来切换每一帧的画面,而这个每一帧的画面数据就是咱们 app 在接收到屏幕刷新信号以后去执行遍历绘制 View 树工做所计算出来的屏幕数据。而 app 并非每隔 16.6ms 的屏幕刷新信号均可以接收到,只有当 app 向底层注册监听下一个屏幕刷新信号以后,才能接收到下一个屏幕刷新信号到来的通知。而只有当某个 View 发起了刷新请求时,app 才会去向底层注册监听下一个屏幕刷新信号。
也就是说,只有当界面有刷新的须要时,咱们 app 才会在下一个屏幕刷新信号来时,遍历绘制 View 树来从新计算屏幕数据。若是界面没有刷新的须要,一直保持不变时,咱们 app 就不会去接收每隔 16.6ms 的屏幕刷新信号事件了,但底层仍然会以这个固定频率来切换每一帧的画面,只是后面这些帧的画面都是相同的而已。
Q3:界面的显示其实就是一个 Activity 的 View 树里全部的 View 都进行测量、布局、绘制操做以后的结果呈现,那么若是这部分工做都完成后,屏幕会立刻就刷新么?
答:咱们 app 只负责计算屏幕数据而已,接收到屏幕刷新信号就去计算,计算完毕就计算完毕了。至于屏幕的刷新,这些是由底层以固定的频率来切换屏幕每一帧的画面。因此即便屏幕数据都计算完毕,屏幕会不会立刻刷新就取决于底层是否到了要切换下一帧画面的时机了。
Q4:网上都说避免丢帧的方法之一是保证每次绘制界面的操做要在 16.6ms 内完成,但若是这个 16.6ms 是一个固定的频率的话,请求绘制的操做在代码里被调用的时机是不肯定的啊,那么若是某次用户点击屏幕致使的界面刷新操做是在某一个 16.6ms 帧快结束的时候,那么即便此次绘制操做小于 16.6 ms,按道理不也会形成丢帧么?这又该如何理解?
答:之因此提了这个问题,是由于以前是觉得若是某个 View 发起了刷新请求,好比调用了 invalidte()
,那么它的重绘工做就立刻开始执行了,因此之前在看网上那些介绍屏幕刷新机制的博客时,常常看见下面这张图:
那个时候就是不大理解,为何每一次 CPU 计算的工做都刚恰好是在每个信号到来的那个瞬间开始的呢?毕竟代码里发起刷新屏幕的操做是动态的,不可能每次都刚恰好那么巧。
梳理完屏幕刷新机制后就清楚了,代码里调用了某个 View 发起的刷新请求,这个重绘工做并不会立刻就开始,而是须要等到下一个屏幕刷新信号来的时候才开始,因此如今回过头来看这些图就清楚多了。
Q5:大伙都清楚,主线程耗时的操做会致使丢帧,可是耗时的操做为何会致使丢帧?它是如何致使丢帧发生的?
答:形成丢帧大致上有两类缘由,一是遍历绘制 View 树计算屏幕数据的时间超过了 16.6ms;二是,主线程一直在处理其余耗时的消息,致使遍历绘制 View 树的工做迟迟不能开始,从而超过了 16.6 ms 底层切换下一帧画面的时机。
第一个缘由就是咱们写的布局有问题了,须要进行优化了。而第二个缘由则是咱们常说的避免在主线程中作耗时的任务。
针对第二个缘由,系统已经引入了同步屏障消息的机制,尽量的保证遍历绘制 View 树的工做可以及时进行,但仍没办法彻底避免,因此咱们仍是得尽量避免主线程耗时工做。
其实第二个缘由,能够拿出来细讲的,好比有这种状况, message 不怎么耗时,但数量太多,这一样可能会形成丢帧。若是有使用一些图片框架的,它内部下载图片都是开线程去下载,但当下载完成后须要把图片加载到绑定的 view 上,这个工做就是发了一个 message 切到主线程来作,若是一个界面这种 view 特别多的话,队列里就会有很是多的 message,虽然每一个都 message 并不怎么耗时,但经不起量多啊。后面有时间的话,看看要不要专门整理一篇文章来说卡顿和丢帧的事。
破译Android性能优化中的16ms问题 android屏幕刷新显示机制 Android Choreographer 源码分析