一个好用的App,应该是一个对用户及时响应的,在用户打开页面(或者打开app)时,给出相应结果,用户点击按钮时的按下效果,或者动画效果,用户切换界面没有任何阻塞和卡顿的感觉,当用户滑动列表时,或者关闭页面时,能快速的响应用户的行为。而这一切的自然发生的前提是我们的应用要保持在60FPS,并且稳定,为什么是60FPS?
这是由人眼和大脑协同工作的结果,人眼能感知到刷新上限就是60FPS,超过这个阈值,人眼识别不了更快的刷新频率,我们平常看到的快速翻书的帧率接近12FPS,看上去是连续的动画,但是人眼还是能感觉到有视觉停留,达到24FPS时,基本上比较流畅。所以大多数设备的刷新的频率60FPS。
这里有兴趣的同学可以了解下双缓冲技术和垂直同步的原理:双缓冲和垂直同步
为了让大家了解GPU和CPU的协同工作原理,以及屏幕刷新的机制,我贴几张图:
上半部分可以认为是CPU和GPU协同工作的部分,省略了CPU,我们只看GPU就好,下半部分是屏幕刷新,在GPU中的双缓冲中有:
Back Buffer
Frame Buffer
每次屏幕刷新时从Frame Buffer读取一帧的数据,同时向Gpu发送VSYNC信号(Vertical synchronization),这时候Gpu开始在Back Buffer准备下一帧,如果下一次VSYNC信号到来时,Back Buffer 刚好准备好了,则会将Back Buffer的数据同步到Frame Buffer,就像下面这样的效果:
如果能每次屏幕刷新发出VSYNC信号时,Back Buffer都准备好了,则Frame Buffer能从Back Buffer拿到最新的帧数据,显示最新的图像,就像下面这样:
如果屏幕刷新发出VSYNC信号时,Back Buffer还没有准备好,则不会把Back Buffer的内容同步到Frame Buffer,而直接显示上一次Frame Buffer的内容,像下面这样
了解动屏幕刷新的流程和原理,我们知道每一帧的准备时间只有16ms,16ms并不是一个很长的时间,我们要重视16ms的边界,渲染布局,动画,包括在UI Thread里的操作,在每一次刷新来临时,都必须准备好。那么平时我们会遇到哪些耗时的操作和卡顿呢?
性能问题之一:非UI操作发生在UI线程
磁盘IO 文件读写,数据库读写,库文件的加载,SharePreference读写
网络IO 网络访问
造成的结果是:导致界面打开延迟,动画不流畅,甚至ANR
性能问题之二:非UI操作在后台线程
网络访问没有超时处理,导致线程被占用,其他任务的执行时间被占用。
对数据库或者文件操作之后,对获取的数据进行预处理,比如过滤,排序,筛选的逻辑过于复杂,算法复杂度高。总而言之,就是后台线程本身比较耗时,而UI需要等待后台线程的数据,用户得到的反馈很慢。解决办法(业务相关):优化数据结构和算法,将任务拆分到多个线程中并行执行,并指定线程的优先级(重要的任务,可以设置更高优先级)
性能问题之三:UI操作发生在UI线程
事件处理 -- 事件回调的处理在主线程,如果耗时,放在后台线程
动画 -- 动画中的位移,旋转,缩放,都会占用时间,动画,动画尽量不要太复杂,元素不要太多,如果动画复杂,并且元素太多,以至于影响性能,可以考虑使用canvas来实现,可以极大的提升性能,
Measure/Layout -- 从根视图开始向下传递Measure/Layout,布局层次越深,元素越多,时间越长,尽量减少布局,可以使用Hierarchy Viewer 查看不合理的布局和渲染时间,使用ViewStub来延迟加载
绘制 -- onDraw,dispatchDraw
帧资源同步 -- 例如图片,一次显示数量少,但是单个文件非常大,或者一次显示大量图片,但是每个文件大小适中
绘制命令的合并和排序 -- 不同的调用顺序会导致不同的消耗
水平方向的绿线是16ms基准线,大部分的帧渲染都在16ms之内,右上角显示的是每一帧中各个阶段消耗的时间
可能看不清楚 ,我放一张局部放大的:
从下到上分别是:
VSYNC 就是上面说的垂直同步信号的发送时间 (0ms)
Input Handing 处理输入事件的时间 (0.41ms)
Animation 动画处理的时间 (0.00ms)
Measure/Layout 测量和定位的时间 (0.07ms)
Draw Draw()方法执行的时间,也就是生成DisplayList的时间(0.28ms)
Sync/Upload 是将要绘制的资源送到GPU的时间,比如Bitmap上传到Gpu(0.06ms)
Command Issue 对DisplayList进行Order 和Merge的时间,也就是说Draw()方法生成的DisplayList并不是全部执行,必须进行合并,裁剪,排序优化后再送入GPu绘制 (1.45ms)
Swap Buffers 可以理解成Gpu的实际绘制了,这是最后一步 (1.74ms)
而最终要的Misc Time (2.74ms)全称是:Miscellaneous 这是发生在UI Thread,并且跟UI渲染无关的操作,应尽量减少在UI Thread 跟UI渲染无关的操作!
当前截图的这一帧的总耗时为:6.75ms,是非常快的,尤其2.74ms其实是可以避免的,是我在UI Thread进行了Bitmap相关的处理,所以比较耗时
说完了绘制的原理,大家想必对16ms有了深刻的认识了,16ms时间真的不多。
上面说了列举了可能导致程序卡顿的操作和造成卡顿的原因,以及16ms,这一切的基础是你对图像渲染的工作原理有一定的了解,才会重视这16ms,那我们如果追踪和定位程序中卡顿和耗时操作呢?
StrickMode
这是StrickMode能提供的检测在UI Thread的违例操作
磁盘读取操作 detectDiskReads()
磁盘写入操作 detectDiskWrites()
网络操作 使用 detectNetwork()
自定义的耗时调用 detectCustomSlowCalls()
但是我要说的,SharePreference (9.0之上 apply异步提交 )的读写有时候确实要在UI Thread,还有SO文件的同步加载,Resources.getDrawable(),这些我们平常都是在UI Thread操作的,而且有时候也是必须的,在我们的工程里,如果开始了StrickMode模式,会打印出一大堆的违例操作,比如 SharePreference,和View初始化时的getDrawable()操作,而且一些耗时 1ms,2ms的操作也会被追踪出来,但是我们的其他的磁盘耗时操作,比如我从Sdcard上都文件的操作,他没有检查出来,反而是一大堆无关痛痒的信息,而且巨多,给大家看看官方的说法:
StrictMode is not a security mechanism and is not guaranteed to find all disk or network accesses
我也是醉了,StrickMode不是安全的,而且不保证能检测到所有的磁盘读写和网络操作。
反正我使用StrickMode没有得到任何有用的信息。
另外值得一提的是,StrickMode可以检查泄露,和那些没有关闭的流,以及检测实例个数,有兴趣的同步可以自己研究下
detectActivityLeaks()
detectLeakedClosableObjects() 未关闭的Closable对象
detectLeakedSqlLiteObjects()泄露的Sqlite对象
setClassInstanceLimit()检测实例数量
BlockCanary
你们可以用用试试看,最肯爹的是耗时操作的定义是我们自己决定的,定义成多少呢?不能完全以来于工具,他的原理就是每次Message的执行前和执行后,计算时间差,如果大于我们定义的时间阈值,就会可以以通知的信息告知开发者,但是最肯爹的是,我们到底定义成多少才算Block呢?作者留给我们一个难题,其实这个真不好定义,我不建议用
最好用和最实用,并且能真正定性和定位问题的,我强烈推荐CPU Monitor(Method Trace)和Systrace
看看Ststrace的截图吧:
同样的我上两张局部放大图:
这个就是定量分析了,这是Systrace在5s内的操作追踪,上面两幅图已经说明了,在UI Thread有耗时操作,但是渲染被延迟,84次的延迟,以及一次较耗时的 measure/layout操作,以及在OnDraw里面的操作耗时较多,看看系统给的优化建议:
不要阻塞UI Thread,耗时的操作移到别的线程执行,比如网络访问,bitmap的加载和处理,并且应该把这些任务执行的线程的优先级及降低
用Systrace定性分析后,就可以用CPU Monitor来定位分析了:
CPU Monitor 可以追踪应用的所有线程的所有方法调用的时间,在工具栏上可以选择要呈现的线程追踪
WallClock Time 表示方法调用和方法返回的时间(包括阻塞等待,睡眠的时间)
CPU Time 实际运行的时间(使用CPU)
Inclusive Time 表示方法执行的时间包括子方法的调用执行时间
Exclusive Time 表示方法执行的时间,不包括子方法的执行
这幅图有两个方向:水平方向,垂直方向
水平方向表示方法的顺序调用
fanctionA();
fanctionB();
垂直方向表示方法的嵌套调用
fanctionA(){
factionC();
factionB();
}
通过GPU Monitor可以抓出到底是哪个方法耗时,把耗时的操作简化,优化,如果不是UI渲染相关的,应该放在工作线程来做,还可以检查做布局渲染的耗时,布局的优化可能要深入业务去分析,比如可以延迟加载(使用ViewStub)
通过CPU Monitor就可以把很多非UI渲染的操作揪出来,剩下的纯UI方面的优化了,这个要深入到代码和业务中,但是并不多说耗时操作放在后台就行了,就像上面一开始说的,有些后台操作的任务本身就可以优化,比如算法,数据结构的不合理,都是可以优化,如果我们不是一开始重视性能优化,那么后期要做的优化非常多。其实最难的就是UI渲染方面的优化。
比如官方是不推荐用LinearLayout和RelativeLayout,gridLayout,因为效率比较低,会导致整个使用LinearLayout和RelativeLayout,gridLayout的布局重新测量多次,而测量和布局多次,消耗是很大的。
推荐使用ConstraintLayout,可以替代LinearLayout和ReltiveLayout,但是性能却好很多。
再有就是在ListView或者RecyclerView或者列表滑动的时候,最好不好出发Measure/layout操作,比如列表中的TextView.setText()频繁调用会导致 Measure/Layout出发,导致列表滑动不流畅。
还有比如有一张动态图是由5张不同图叠加到一起,其中有4张是静止不懂的,还有一张图是变化的,如果自定义这个效果,我们肯定是用canvas来实现了,draw 不同的bitmap,但是我们是要绘制5次吗?不是,如果是绘制5次,就会有重绘了,其实我们只需要绘制2次就够了,因为有4张图是不变的,我们可以先把这4张图一次性合成一张,这样每次绘制的时候,其实只用画两次就够了,而且还节省内存
还有比如有下面的操作
canvas.rotate()
canvas.clip()
canvas.drawBitmap()
canvas.clip()
canvas.rotate()
canvas.drawBitmap()
最终绘制出来的效果是一样的,但是性能是不一样的呢,这里面设计到 Stencil Buffer,不讲深了,其实我也不懂。。。
关于布局的一些技巧我就不说了,HierarchyViewer对我来说可有可无
平常开发中还有很多优化技巧,如果你做UI比较多,要细细的琢磨才行