程序流畅度是很重要的一个指标,重要是因为用户可以直观体验到,为啥别人家的程序都是如丝般顺滑( •̀ ω •́ )✧( •̀ ω •́ )✧( •̀ ω •́ )✧,今天就来聊聊系统是如何判断程序卡顿。
不知道你曾注意过这样一条系统日志
Choreographer(1434): Skipped 35 frames! The application may be doing too much work on its main thread.
当出现这条信息,意味着我们程序执行过程中出现了卡顿,需要进一步优化。
一、黄油计划(Project Butter)
android在不同的版本都会优化“UI的流畅性”问题,但是直到在android 4.1版本中做了有效的优化,这就是Project Butter,所谓的黄油计划,android工程师真会起名字。
Project Butter加入了三个核心元素:VSYNC、Triple Buffer和Choreographer。其中VSYNC是理解Project Buffer的核心。VSYNC是Vertical Synchronization的缩写 也就是“垂直同步”
1.1.没有VSYNC前
- 时间从0开始,进入第一个16ms:Display显示第0帧,CPU处理完第一帧后,GPU紧接其后处理继续第一帧。三者互不干扰,一切正常。
- 时间进入第二个16ms:因为早在上一个16ms时间内,第1帧已经由CPU,GPU处理完毕。故Display可以直接显示第1帧。显示没有问题。但在本16ms期间,CPU和GPU却并未及时去绘制第2帧数据(注意前面的空白区),而是在本周期快结束时,CPU/GPU才去处理第2帧数据。
- 时间进入第3个16ms,此时Display应该显示第2帧数据,但由于CPU和GPU还没有处理完第2帧数据,故Display只能继续显示第一帧的数据,结果使得第1帧多画了一次(对应时间段上标注了一个Jank)。
- 通过上述分析可知,此处发生Jank的关键问题在于,为何第1个16ms段内,CPU/GPU没有及时处理第2帧数据?原因很简单,CPU可能是在忙别的事情(比如某个应用通过sleep固定时间来实现动画的逐帧显示),不知道该到处理UI绘制的时间了。可CPU一旦想起来要去处理第2帧数据,时间又错过了
1.2.引入了VSYNC
Vsync提高了图形性能,为了确保整个体验的帧速率一致,从Android 4.1开始将vsync定时扩展到Android框架处理的所有绘图和动画。一切都与16毫秒的vsync“心跳”相协调——应用程序渲染、触摸事件、屏幕合成和显示刷新——因此帧不会超前或落后。
二、应用卡顿检测
Android系统每隔16.6ms发出VSYNC信号,来通知界面进行输入、动画、绘制等动作,每一次同步的周期为16.6ms,代表一帧的刷新频率,理论上来说两次回调的时间周期应该在16.6ms,如果超过了16.6ms我们则认为发生了卡顿,利用两次回调间的时间周期来判断是否发生卡顿 这个方案的原理主要是通过Choreographer类设置它的FrameCallback函数,当每一帧被渲染时会触发回调FrameCallback, FrameCallback回调void doFrame (long frameTimeNanos)函数。一次界面渲染会回调doFrame方法,如果两次doFrame之间的间隔大于16.6ms说明发生了卡顿
2.1.Choreographer类
监控应用的流畅度一般都是通过Choreographer类的postFrameCallback方法注册一个VSYNC回调事件
public static void start(final Builder builder) { Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() { long lastFrameTimeNanos = 0; long currentFrameTimeNanos = 0; @Override public void doFrame(long frameTimeNanos) { if (lastFrameTimeNanos == 0) { lastFrameTimeNanos = frameTimeNanos; LogMonitor.getInstance().setFrequency(builder.frame * 17 / 2); if (builder.targetPackageName != null) { LogMonitor.getInstance().setTargetPackageName(builder.targetPackageName); } LogMonitor.getInstance().setDumpListener(builder.onDumpListener); } currentFrameTimeNanos = frameTimeNanos; skipFr