卡顿优化小结
卡顿的本质
卡顿的本质是因为一次垂直同步信号来的时候,当前帧要显示的图像数据还没准备好,只能等待16ms下一次垂直同步信号来时才能更新画面,在这段时间里显示器只能一直停留在上一帧的画面,如果跳过的帧数过多,就会看到上一帧的画面一直没有变化,给人的感觉就是卡顿;
垂直同步信号:是为了解决显示器刷新率和CPU/GPU生产图形数据速度不匹配问题;当CPU/GPU制造图形数据的速度大于显示器刷新速度时,生产的很多帧数据就浪费了,浪费了cpu/GPU的性能;而当CPU/GPU制造帧数据的速度小于显示器刷新速度时,就会导致跳帧,也就是会感觉画面更新很慢,给人卡顿的感觉;
有了垂直同步信号后,当屏幕显示完一帧画面后会发送一个垂直同步信号,cpu、gpu在接收到信号后才开始生产帧数据,这样就不会浪费cpu/GPU性能
GPU的ALU单元比CPU多很多,GPU更擅长计算;而cpu更擅长逻辑运算
卡顿优化
造成卡顿的原因主要有这三个方面:
-
第一,在主线程上做了一些耗时操作,我们需要将这些耗时操作转移到线程池中执行
-
第二,短时间内创建大量对象造成频繁GC,比如在自定义View的onDraw方法中、列表滚动事件中、创建了大量对象就会引起GC频繁;解决办法就是通过享元模式复用已经创建过的对象,避免对象大量重复创建
-
第三,布局设计不合理,导致过度绘制,层级过多,从而引起绘制需要的时间更长;解决办法是通过include、merge、viewStub、ConstraintLayout这些标签对布局进行优化,减少布局的层级,减少过度绘制的现象
卡顿监测
-
对于布局方面的检测,我们可以通过Android Studio自带的Layout Inspector查看界面中的布局情况;在开发者模式中打开过度绘制开关,可以查看应用过度绘制情况
-
通过编舞者类不断请求发起垂直同步信号请求,每次回调时计算跳过的帧数,超过一定的阈值,就认为卡顿发生了;编舞者类也会打印出跳帧的系统log
-
在一些可能比较耗时的代码前后预先插入统计耗时的代码;也可以调用Trace.beginSection和endSection代码,然后通过系统工具systemtrace分析该段代码运行情况
-
如果需要定位到具体比较耗时的代码,可以通过给主线程Looper设置一个自定义打印log类,Looper会在事件分发前后使用该Log打印类输出log;我们可以在事件分发之前开始定时收集主线程的调用栈信息,在事件分发结束后判断耗时情况,如果跳过一定的帧数,则判定卡顿发生,并且打印出这段时间内主线程的调用栈情况,从而定位到是主线程哪里耗时比较多,第三方监控卡顿框架BlockCanary就是通过这种方式实现卡顿监控的
UI渲染机制
-
当我们在Activity的onCreate方法中设置一个布局之后,会先创建一个根布局DecorView,然后通过LayoutInflater将我们设置的xml布局文件解析成View对象并添加到DecorView中
-
接着在onResume生命周期过后会回调makeVisible方法;接着依次调用WindowManagerImpl\WindowManagerGlobal的addView方法,为DecorView生成一个ViewRootImpl对象用于管理DevorView以及和WMS通信;
-
接着DecorView会调用setView方法,在里面会调用requestLayout方法请求开始绘制,并将窗体相关信息添加到WMS中
-
在requestLayout方法中不会立刻执行View的三大绘制流程方法,而是先通过消息队列发送一个同步屏障消息,然后通过编舞者类发起一次垂直同步信息请求,当垂直同步信息来的时候,ViewRootImpl就开始绘制并调用View的onMeasure\onLayout\onDraw方法,在调用绘制方法draw之前,会通过Surface创建一个画布Canvas传递到各个View的onDraw方法中,各个View就在这个画布上进行绘制图像;绘制完成之后,会将这个带有绘制数据的Surface发送给SurfaceFlinger,SurfaceFlinger将应用绘制数据和系统UI数据整合之后交给GPU进行栅格化渲染,最终形成可供显示器显示的一帧数据存到帧缓冲区,等待一次垂直同步信号后显示到屏幕
-
之后的每次View刷新,都会最终调用到ViewRootImpl的requestLayout方法,等待一次垂直同步信号后执行绘制流程
编舞者类Choreographer
编舞者类主要是用于控制绘制节奏,通过请求垂直同步信号,接收垂直同步信号来通知应用层绘制的时机,主要流程如下:
-
编舞者类在每一个线程都有一个唯一的单例对象,它是存在ThreadLocal中的
-
当调用它的postCalllback或者postFrameCallback方法时,会将回调方法保存到队列中,这两个方法的区别是postFrameCallback请求的是动画类型的垂直同步信号,而postCallback支持传入指定类型的垂直同步信号;垂直同步信号类型一共有五种:分别用于 输入、动画、插入动画、绘制、提交;不同类型只是它们的callback调用顺序不一样,input类型的callback最先调用
-
接着会判断要不要延迟发起请求、当前线程是否跟Looper线程是否一致来决定要不要立刻发起请求;发起请求是通过调用native方法实现的,主要也是通过向SurfaceFlinger注册监听垂直同步信号
-
当接收到垂直同步信号时,会调用编舞者类的回调方法,编舞者类接着会通过Handler发送异步消息去调用doFrame方法
-
在doFrame方法中会判断有没有跳帧,因为消息队列中可能会有其他耗时任务在执行,导致doFrame方法执行延后,如果跳帧了就输出log、接着就会依次调用那五种类型的Callback回调函数
-
然后就可以在接收到垂直同步信号后进行UI绘制、动画等操作
同步屏障
Handler中有三种类型的消息,同步消息、同步屏障和异步消息;Message里有个属性标记是否异步,默认是同步消息;而同步屏障消息是一个target等于null的Message对象,这个target也就是Handler;当消息队列遍历到这个同步屏障消息时,会开启一个while循环,优先遍历处理之后的异步消息;