翻下之前写下的笔记,找到了这篇文章,当时为了弄清楚这玩意,花了几天时间网上找资料,看源码,不厌其烦的找研发咨询,回想起来满满的回忆,因此也想分享跟大家看看;
----------------------------------------华丽的分割线----------------------------------
之前工作时,研发新增了SM指标来衡量流畅度这一指标,但貌似木有过多的文档来介绍啥是SM,而之前的工作,都是用FPS来衡量的,那这个SM到底是啥?因此就怀着SM是什么跟SM与FPS有啥区别的疑问,去找资料;
流畅度的重要性都不言而喻了,为了提升android流畅性,Google对android系统进行大量优化,比如GPU硬件加速、dalvik换成art等等;而业界公认的流畅度指标就是FPS,但在使用FPS的时候却发现一些奇怪的现象。。。
FPS的“奇怪”现象:
一般而言,帧率越高说明流畅度越高,相反,帧率越低说明流畅度越低,而且如果停止操作,FPS应该是恒定不变才对,但在使用的时候发现存在诡异的地方:
1)滑动时发现,有时候显示FPS很低,但实际使用却没有卡顿感觉,这是why?
2)当app停止操作时,FPS却一直存在变化,这又是why?
上网查询了一下系统获取FPS的原理,发现是这样的:
手机屏幕显示的内容是通过android系统的SurfaceFLinger类,把当前系统里所有进程需要显示的信息合成一帧,然后提交到屏幕进行显示;
所以可以理解,FPS就是1S内SurfaceFLinger提交到屏幕的帧数;
根据这个原理,就能解释上面两个现象了:
1)android显示屏幕是以每秒60帧来刷新的,但如果app目前只有每秒30帧的显示需求,那FPS最高也就只有每秒30帧,但这不能说明此时是卡顿的,如果屏幕没有绘制需求,则FPS就为0了,而如果是停在一个界面,FPS就为1了;
2)app停止操作后FPS还一直变化,是因为屏幕每一帧的合成是针对手机所有进程,意思为即使当前使用的app停止了绘制,但是其他进程还在绘制的话,那FPS还是会持续变化的;
结合以上的内容发现,FPS的数据在这些场景下是并不准确,那有其他数据能在这种场景下说明问题吗?上网搜索了一下,发现有一个叫SM的指标,据说是能代表流畅度;
在开始之前,需要说一下几个知识点:
1)图形是如何绘制到屏幕上?
简单理解就是,CPU会把UI组件计算成polygons和textures,然后交给GPU进行渲染,最后GPU再将数据传送到屏幕,屏幕进行绘制显示;而上面说的FPS数据,其实就是GPU-->SCREEN这一过程的帧数;
2)android显示屏幕是以每秒60帧来刷新的,60帧每秒就意味着:1000/60=16.6ms;
为什么是以每秒60帧来刷新,简单来说是因为人眼与大脑之间的协作无法感知超过60fps的画面更新。
12fps类似手动快速翻书的帧率,24fps是电影胶卷常用帧率,但是低于30fps是无法顺畅表现绚丽的画面内容的,此时就需要用到60fps来达到想要的效果,当然超过60fps是没有必要的。
以上内容总结就是:每一帧的内容必须要再16ms内完成!
为什么会出现卡顿的现象?
接下来,我们来看看,为什么会出现卡顿的现象;
1)时间从0开始,进入第一个16ms:Display显示第0帧,CPU处理完第一帧后,GPU紧接其后处理继续第一帧。三者互不干扰,一切正常。
2)时间进入第二个16ms:因为在上一个16ms时间内,第1帧已经由CPU,GPU处理完毕。因此Display可以直接显示第1帧,显示没有问题。但在本16ms期间,CPU和GPU却并未及时去绘制第2帧数据(注意前面的空白区),而是在本周期快结束时,CPU/GPU才去处理第2帧数据。
3)时间进入第3个16ms,此时Display应该显示第2帧数据,但由于CPU和GPU还没有处理完第2帧数据,故Display只能继续显示第一帧的数据,结果使得第1帧多画了一次。
4)通过上述分析可知,为何第2个16ms段内,CPU/GPU没有及时处理第2帧数据?
原因可能是因为CPU当时是在忙别的事情,但当CPU处理完后再来处理第2帧的数据,此时16ms可能已经错过了,因此就会出现掉帧的现象了;
为了解决该问题,android就引入了VSYNC,这类似于中断信号(垂直信号)。结果如图2所示:
每隔16ms,就会发起VSync信号,由图2可知,每收到VSYNC中断,CPU就开始处理各帧数据。
那如果CPU/GPU出现处理慢的情况,会如何?
由图3可知:
在第二个16ms时间段,Display本应显示B帧,但却因为GPU还在处理B帧,导致A帧被重复显示。
同理,在第二个16ms时间段内,CPU无所事事,因为A Buffer被Display在使用。B Buffer被GPU在使用。特别提醒,一旦过了VSYNC时间点,CPU就不能被触发以处理绘制工作了。
为什么CPU不能在第二个16ms处开始绘制工作呢?原因就是只有两个Buffer。如果有第三个Buffer的存在,CPU就能直接使用它,而不至于空闲。出于这一思路就引出了Triple Buffer。结果如图4所示:
由图4可知:
第二个16ms时间段,CPU使用C Buffer绘图。虽然还是会多显示A帧一次,但后续显示就比较顺畅了。
是不是Buffer越多越好呢?回答是否定的。由图4可知,在第二个时间段内,CPU绘制的第C帧数据要到第四个16ms才能显示,这比双Buffer情况多了16ms延迟。
上面的内容涉及到两个关键信息:
VSYNC:核心关键,定时中断。这个的作用就是为了保证CPU、GPU生成帧的速度和display刷新的速度保持一致,而安卓系统是每16ms就会发出一次vsync信号触发UI渲染更新;
Triple Buffer:当双Buffer不够使用时,该系统可分配第三块Buffer。(本次内容不涉及此处)
看到这,各位可能一脸懵逼,这是啥??跟SM有啥关系?
简单而言:
VSync机制就像是一台转速固定的发动机(60转/s).
每一转带动着去做一些UI相关的事情,但是不是每一转都会有工作去做;
有时候因为各种阻力某一圈工作量比较重超过了16.6ms,那么这台发动机这秒内就不是60转了;
当然也有可能被其他因素影响,比如给主线程里干的活太多等等,就会出现转速降低的状况,我们把这个转速叫做流畅度,也叫作SM;
从FPS到SM:
文章开头说到过,FPS在部分情况下,是不适合用作流畅度的指标,比如静止不动,此时FPS应为1,但不能说明此时出现卡顿情况;
反观每秒VSync运行了多少次更加能说明当前app的流畅度,因为即使是app静止不动,那每秒VSync也会运行60次;
SM:SMoothness,在VSync机制中1s内Loop运行的次数
如何获取SM:
按照上面的说明,那每次在VSync运行之前通知到客户端,然后客户端进行计数就可以了~
幸运的是,在新的Android的机制中找到Choreographer这个对象帮忙处理这个事
根据Google的官方API文档描述,Choreographer是用来协调animations, input 以及 drawing的时序的,并且每个Looper共用一个Choreographer对象。
FrameHandler主要是处理3个类型的消息:
1.MSG_GO_FRAME:开始渲染下一帧的操作
2.MSG_DO_SCHEDULE_VSYNC:请求Vsync信号
3.MSG_GO_SCHEDULE_CALLBACK:请求执行callback
VSync信号由SurfaceFlinger实现并定时发送。FrameDisplayEventReceiver收到信号后,调用onVsync方法组织消息发送到主线程处理。这个消息主要内容就是run方法里面的doFrame;
当VSYNC信号到达时,doFrame就会被调用,而doFrame这里可以得出SF指标(Skipped Frame),简单理解就是丢帧,而执行doFrame是实现屏幕刷新;
丢帧可以这样理解:在16ms完成工作却因各种原因没做完,占了下n个16ms的时间.相当于丢了n帧;
更通俗点就是如果一帧的执行时间超过了16.6ms,那么多于16.6ms的时间除以16.6ms,即是当前App的丢帧;
doFrame里有一段关于丢帧的处理,如下:
换个角度思考,既然doFrame是实现屏幕刷新的,那就意味着只要有刷新动作,必然会调用到doFrame,因此如果想获取SM,只需要注册Choreographer的postFrameCallback回调,然后在每次执行doFrame回调通知我们统计即可;
Q:这里可能有疑问,为毛是注册postFrameCallback?
A:Choreographer对外提供了两个接口用于注册指定的Callback:
1)postCallback()用于注册Runnable对象
2)而postFrameCallback()函数用于注册FrameCallback对象
无论注册的是哪种对象,在CallbackRecord对象中统一装箱为Object类型。
在执行其回调函数时,就需要区别这两种对象类型,如果注册的是Runnable对象,则调用其run()函数,如果注册的是FrameCallback对象,则调用它的doFrame()函数,因此,只能选择postFrameCallback;
整理知识点:
1)在滑动情况下,FPS=SM=都靠谱,因为数据都是从SurfaceFLinger获取,共用一份源数据;在静止情况下,SM靠谱;
2)FPS是1S内SurfaceFLinger提交到屏幕的帧数,注意,是所有进程;
3)Vsync是16.6ms执行一次;
4)丢帧是16ms完成工作却因各种原因没做完,占了下n个16ms的时间.相当于丢了n帧;
5)SM就是1S内能运行多少次,即可以表示当前绘制的能力,也就是卡顿程度;
6)
FPS获取的方法 “adb shell dumpsys SurfaceFlinger --latency "的方式,然后在对应的activity操作即可,但得出的数据并非是直观的FPS数据;
需要3步:
1.adb shell dumpsys SurfaceFlinger --latency 产生fps数据
2.通过service call SurfaceFlinger 1013 来得到当前帧的索引以及时间戳,设置为A = {indexA,timeA}
3.公式:设上一次的数据为B = {indexB,timeB}
FPS = (indexA-indexB)/(timeA-timeB)
上面2、3步骤涉及到使用第三方库:https://github.com/ChromiumWebApps/chromium/tree/master/build/android/pylib ,如感兴趣的同学可自行喵喵;
简单而言,FPS可在不需要源码的情况下获取,而SM不行(需要源码);但SM在部分场景下比FPS要靠谱;
备注:
关于Android的渲染机制其实是一个很复杂的过程,上面也只是随便说说,很多过程和细节都忽略了,有兴趣的童鞋自行深入研究哈;