屏幕渲染机制
- cpu(gpu sf暂统称cpu)经过各种运算,将数据写入一块内存中,这块内存叫做「帧缓冲」。
- 帧缓冲可以理解为一个M*N矩阵,数据从上到下一行一行保存。
- 屏幕在显示的时候,从上到下逐行扫描,依次显示在屏幕上,我们把这样的一屏数据叫做「一帧」。
- 当一帧数据渲染完后,就开始新一轮扫描,如果CPU「正好」(不正好后面再说)也把下一帧数据写入帧缓冲,那么就会显示下一帧画面,如此循环,我们就看到了不断变化的画面,也就是图像。
过程很简单,执行起来却很难,因为这里有两个问题,撕裂
和卡顿
,为什么?
- 如果当屏幕渲染到一半的时候,cpu已经整理好了下一帧的数据,给到帧缓冲,这时屏幕继续从帧缓冲中一半的位置开始读取数据,这时画面就会显示异常,这种两个图像重合的的问题叫作图像
撕裂
。 - 如果当屏幕渲染完一帧画面,cpu还没整理好下一帧数据给到帧缓冲,这时屏幕继续从帧缓冲中读取数据渲染,显示的还是上一帧的数据,这时用户就会感觉到
卡顿
。
那你就会有个疑问了
- 这cpu怎么这么笨,屏幕都没把数据渲染完,干嘛把数据换了?
- 屏幕怎么这么傻,数据都被换了,怎么不从头读取呢?
这是因为"现代计算机之父"冯·诺依曼提出了计算机的体系结构: 计算机由运算器,存储器,控制器,输入设备和输出设备构成,每部分各司其职。
- 屏幕的作用就是渲染,不管外界怎么变化,它就是一行一行的渲染。
- cpu的任务就是运算,运算好了把数据丢给帧缓冲,继续运算。
那能不能让cpu停下来?
当然可以,但是不划算,因为这样就等于把cpu的长处给扼杀了。
等等,有个问题,你说当两个图像重合的的问题叫作撕裂
,那不是从第二帧开始,时时刻刻都在撕裂
?
- 好吧,我说错了,有一点忘记说了,应该有个时间,在这个时间内渲染完一帧,画面看起来就是连续的,这个时间大约是16ms。
为什么是16ms?
- 这是因为人眼与大脑之间的协作无法感知超过60fps的画面更新。12fps大概类似手动快速翻动书籍的帧率,这明显是可以感知到不够顺滑的。24fps使得人眼感知的是连续线性的运动,这其实是归功于运动模糊的效果。24fps是电影胶圈通常使用的帧率,因为这个帧率已经足够支撑大部分电影画面需要表达的内容,同时能够最大的减少费用支出。但是低于30fps是无法顺畅表现绚丽的画面内容的,此时就需要用到60fps来达到想要的效果,当然超过60fps是没有必要的。
fps是啥玩意儿?这里说两个概念
-
屏幕刷新率(Hz):屏幕在一秒内刷新的次数,Android手机一般都是60Hz,也就是一秒刷新60次,当然也有高刷的,但是60Hz足矣。
-
帧速率(FPS):cpu在一秒内合成的帧数,比如60FPS,就是60 frame per sconds,意思就是一秒合成60帧。如上所述,当屏幕刷新率大于帧速率的时候,会发生卡顿;屏幕刷新率小于帧速率的时候,会发生撕裂。那么怎么解决这个问题呢?
怎么解决呢?Vsync 和 双缓冲
- 屏幕正在从前缓冲读取第一帧数据并渲染,此时cpu计算完第二帧数据,放在后缓冲,等待VSYNC信号。
- 屏幕将第一帧数据渲染完毕,发出VSYNC信号,cpu收到VSYNC信号,将后缓冲的第二帧数据复制到前缓冲。
- 同时屏幕继续绘制第二帧数据,cpu开始计算下一帧数据,循环往复。
牛~~,双缓冲+Vsync解决了撕裂问题,但是并没有解决卡顿的问题,当cpu收到Vsync信号后,如果cpu还没有计算完,也肯定就不会交换前后缓冲的数据,也就是说,屏幕再次读取的还是前缓冲的数据,也就是两次显示了一样的画面,也就是产生了卡顿。
是的,卡顿问题是解决不了的,只能优化。
还能怎么优化?三缓冲 !
首先要清楚下,Android屏幕绘制流程。
- 任何一个View都是依附于window的
- 一个window对应一个surface
- view的measure、layout、draw等均是计算数据,这些是cpu干的事
- cpu把这些事干好后,在经过一系列计算将数据转交给gpu
- gpu将数据栅格化后,就交给SurfeceFlinger(以下简称SF)
- SF将多个surfece数据合并处理后,就放入后缓冲区
- 屏幕以固定频率从前缓冲区拿出数据渲染,渲染完毕后发送VSYNC,此时前后缓冲区数据交换,屏幕绘制下一帧。
上述7步是建立在开启硬件加速的情况下的,如果没有硬件加速,就去掉gpu部分,就可以简单理解为cpu直接将数据转交给sf,我们简单整理一下数据的传递流程:
「cpu -> gpu -> display」
,而且我们看到,cpu和gpu是排队工作的,它俩和屏幕是并行工作的。好,我们来看发生卡顿(jank)的场景
- 我们可以将Display那一行看作是前缓冲,将GPU和CPU两行叠加起来看作是后缓冲(因为它俩排队使用),将VSYNC线隔离开的竖行看作一个帧。
- 我们看到,在第一帧里面,GPU墨迹了半天没搞完,以至于在第二帧里面,Display(屏幕)显示的还是第一帧的A数据,此时就产生了Jank(卡顿),并且在一个vsync信号过来后,cpu什么都没做,因为gpu占着后缓冲(那个绿色的长B块),所以cpu只能再等下一个vsync,在下一个vsync里面,cpu终于拿到了后缓冲的使用权,但是cpu计算时间比较长,导致了gpu时间不够用,数据又没算完,再次发生了卡顿,可以说,这次卡顿直接受到了第一次卡顿的影响。
- 试想: 如果在第一次卡顿的时候,cpu也能计算数据,那么,第二次卡顿可能就不存在了,因为cpu已经在第一次卡顿的时候把蓝色的A给计算完了,第二次完全可以让gpu独自计算(绿色的A),就不存在因为排队导致的时间不够用了,但是!cpu和gpu共用后缓冲,这就导致它们只能轮流使用后缓冲,怎么解决呢?再加一个后缓冲区,让cpu、gpu各用一块。
我们来看引入三缓冲后的效果:
三缓冲使用效果
- 我们看到,在第一次jank内,cpu使用了第三块缓冲区,自己计算了C帧的数据,假如此时没有三缓冲,那么cpu就只能再继续等下一个vsync信号,也就是在图中蓝色A块的地方,才能开始计算C帧数据,就又引发下一次卡顿。我们看到,通过引入三缓冲,虽然不能避免卡顿问题,但是却可以大幅优化卡顿问题,尤其是避免连续卡顿。
- 但是,三缓冲也有缺点,就是耗资源,所以系统并非一直开启三缓冲,要想真正解决问题,还需要在cpu层对数据尽量优化,从而减小cpu和gpu的计算量,比如:View尽量扁平化,少嵌套,少在UI线程做耗时操作等。
源码层面看cpu做了哪些事
请看这篇文章,View的加载流程
从这里我们可以看出,View加载过程中,使用到的耗时操作
- IO操作(读取布局文件)
- Xml解析(解析布局文件)
- 使用了递归(递归查找每一个View)。
- 反射(使用反射初始化view)
解决IO操作和Xml解析
如果在可以的情况下,不推荐使用Xml来写布局,推荐使用代码直接new。但是这样一来可维护性大大降低。
借用此思想,github上也有一个由掌阅发布的开源库:https://github.com/iReaderAndroid/X2C
- 为了即保留xml的优点,又解决它带来的性能问题,我们开发了X2C方案。即在编译生成APK期间,将需要翻译的layout翻译生成对应的java文件,这样对于开发人员来说写布局还是写原来的xml,但对于程序来说,运行时加载的是对应的java文件。 我们采用APT(Annotation Processor Tool)+ JavaPoet技术来完成编译期间【注解】->【解注解】->【翻译xml】->【生成java】整个流程的操作。
RecyclerView优化
RecyclerView一些你可能需要知道的优化技术
参考:
Android 性能优化必知必会
性能优化系列总篇
Android性能优化
一篇小短文,带你了解屏幕刷新背后的故事
Android UI性能优化实战 识别绘制中的性能问题
Android UI性能优化 检测应用中的UI卡顿
Google 发布 Android 性能优化典范
Android性能优化
网友评论