前言:在平时开发项目的时候我们都知道生成一个view加入到window中,或者对显示的view调用其属性改变的方法亦或者启动在某个view上配置的动画就会让屏幕进行刷新达到自己想要的ui效果。但是咱们的代码是怎么触发屏幕刷新或者说系统是用怎样的机制去刷新屏幕改变的view属性的很多人还是不太清楚。当然最开始我也是没有系统的梳理过,前段时间经过对一些文章博客的阅读和系统源码的梳理我才有了较为清晰的感知,在此也写出来仅供大家参考。
此篇文章借鉴了很多其他文章的内容作为本人偷懒的途径,再次向大神们表示万分感谢!!
友情提示:本篇文章确实很长,因为这部分内容要理清楚不容易,小伙伴如果有时间,可以静下心来慢慢看。如果没时间,那么也可以直接看看最后面的总结。
问题导向
我也知道阅读梳理源码是很枯燥和容易跑偏的,所以这里咱们带着几个问题来梳理源码,这样会比较有条理性,不会跟偏或太深入,当然了也是怕大家看着看着就烦了。
大伙都清楚,Android 每隔 16.6ms 会刷新一次屏幕。但是下面这几个都是很常见的容易迷惑的问题:
Q1:这个 16.6ms 刷新一次屏幕到底是什么意思呢?是指每隔 16.6ms 调用 onDraw() 绘制一次么?
Q2:如果界面一直保持没变的话,那么还会每隔 16.6ms 刷新一次屏幕么?
Q3:界面的显示其实就是一个 Activity 的 View 树里所有的 View 都进行测量、布局、绘制操作之后的结果呈现,那么如果这部分工作都完成后,屏幕会马上就刷新么?
Q4:网上都说避免丢帧的方法之一是保证每次绘制界面的操作要在 16.6ms 内完成,但如果这个 16.6ms 是一个固定的频率的话,请求绘制的操作在代码里被调用的时机是不确定的啊,那么如果某次用户点击屏幕导致的界面刷新操作是在某一个 16.6ms 帧快结束的时候,那么即使这次绘制操作小于 16.6 ms,按道理不也会造成丢帧么?这又该如何理解?
Q5:大伙都清楚,主线程耗时的操作会导致丢帧,但是耗时的操作为什么会导致丢帧?它是如何导致丢帧发生的?
下面的文章内容主要就是以奔着搞清楚这几个问题为导向的,内容有些多,请对面的你耐心的看下去吧。
源码分析
友情提示:由于各系统版本的不一致,源码可能会有些许差异,小伙伴梳理的时候注意一下。
基本概念
在最开始,咱们先来过一下涉及的一些基本概念:
在一个典型的显示系统中,一般包括CPU、GPU、display三个部分, CPU负责计算数据,把计算好数据交给GPU,GPU会对图形数据进行渲染,渲染好后放到buffer里存起来,然后display(有的文章也叫屏幕或者显示器)负责把buffer里的数据呈现到屏幕上。
显示过程,简单的说就是CPU/GPU准备好数据,存入buffer,display每隔一段时间去buffer里取数据,然后显示出来。display读取的频率是固定的,比如每个16ms读一次,但是CPU/GPU写数据是完全无规律的。
上述内容概括一下就是说,屏幕的刷新包括三个步骤:CPU 计算屏幕数据、GPU 进一步处理和缓存、最后 display 再将缓存中(buffer)的屏幕数据显示出来。
友情提示:在咱们的开发过程中应该接触不到 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 就接收不到屏幕刷新信号了?为什么绘制视图树计算下一帧画面的工作会是在屏幕刷新信号来的时候才开始的?等等。
good,有了这些困惑很好,这样,我们下面一起过源码时,大伙就更有目的性了,这样过源码我觉得效率是比较高一点的。继续看下去,跟着过完源码,你就清楚为什么了。好了,那我们下面就开始过源码了。
ViewRootImpl 与 DecorView 的绑定
阅读源码从哪开始看起一直都是个头疼的问题,所以找一个合适的切入点来跟的话,整个梳理的过程可能会顺畅一点。本篇是研究屏幕的刷新,那么建议就是从某个会导致屏幕刷新的方法入手,比如 View#invalidate()
。View#invalidate()
是请求重绘的一个操作,所以我们切入点可以从这个方法开始一步步跟下去。我们跟着 invalidate()
一步步往下走的时候,发现最后跟到了