性能优化 -- 卡顿

一、绘制原理

Android应用程序把经过测量、布局、绘制后的surface缓存数据,通过SurfaceFlinger把数据渲染到显示屏幕上,通过Android的刷新机制来刷新数据。也就是说应用层负责绘制,系统层负责渲染,通过进程间通信把应用层需要绘制的数据传递到系统层服务,系统层服务通过刷新机制把数据更新到屏幕。

1.应用层
在Android系统中整体的绘图源码是在ViewRootImpl类的performTraversals()方法,通过这个方法可以看出Measure和Layout都是递归来获取View的大小和位置,并且以深度作为优先级。可以看出,层级越深,元素越多,耗时也就越长。
(1)Measure
用深度优先原则递归得到所有视图(View)的宽、高。当获取当前View的正确宽度childWidthMeasureSpec和高度childHeightMeasureSpec之后,可以调用它的成员函数Measure来设置它的大小。如果当前正在测量的子视图child是一个视图容器,那么它又会重复执行操作,直到它的所有子孙视图的大小都测量完成为止。
(2)Layout
用深度优先原则递归得到所有视图(View)的位置。当一个子View在应用程序窗口左上角的位置确定之后,再结合它在前面测量过程中确定的宽度和高度,就可以完全确定它在应用程序窗口中的布局。
(3)Draw
目前Android支持两种绘制方式:软件绘制(CPU)和硬件加速(GPU),其中硬件加速在Android 3.0开始已经全面支持,硬件加速在UI的显示和绘制的效率远远高于CPU绘制,但硬件加速也存在明显的缺点:
耗电:GPU的功耗比CPU高。
兼容问题:某些接口和函数不支持硬件加速。
内存大:使用OpenGl的接口至少需要8MB内存。

是否使用硬件加速需要考虑一些接口是否支持硬件加速,同时结合产品的形态和平台,比如TV版本就不需要考虑功耗问题,而且TV屏幕大,使用硬件加速容易实现更好的显示效果。

2.系统层
真正把需要显示的数据渲染到屏幕上,是通过系统级进程中的SurfaceFlinger服务来实现的,SurfaceFlinger把缓存区数据渲染到屏幕上主要是驱动层的事情。

3.绘制时长
FPS:每秒传递的帧数。在理想情况下,60FPS就感觉不到卡,这意味着每个绘制时长应该在16ms以内。Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染,如果每次渲染都成功,这样就能够达到流畅的画面所需的60FPS。即为了实现60FPS,就意味着程序的大多数绘制操作都必须在16ms内完成,如果某个操作花费的时间是24ms,系统在得到VSYNC信号时就无法进行正常渲染,这样就发生了丢帧现象,那么用户在32ms内看到的会是同一帧画面。主要场景是在执行动画或者滑动ListView时更容易感知到卡顿不流畅,是因为这里的操作相对复杂,容易发生丢帧的现象,从而感觉卡顿。

4.刷新机制
在Android 4.1版本推出了Project Butter,对Android Display系统进行了重构,引入了三个核心元素:VSYNC、Triple Buffer和Choreographer。
VSYNC:垂直同步信号。
双缓冲:使用两个缓冲区。
三重缓冲:使用三个缓冲区。
虽然Android系统在显示机制上解决了Android UI不流畅的问题,但实际在应用开发过程中仍然存在卡顿的现象。

二、卡顿的场景

1.UI绘制(绘制、刷新)
2.应用启动(安装启动、冷启动、热启动)
3.页面跳转(页面间切换、前后台切换)
4.事件响应(按键、系统事件、滑动)

三、卡顿的根本原因

1.界面绘制
主要原因是绘制的层级深、页面复杂、刷新不合理,由于这些原因导致卡顿的场景更多出现在UI和启动后的初始界面以及跳转到页面的绘制上。
2.数据处理
导致这种卡顿场景的原因是数据处理量太大,一般分为三种情况:
一是数据处理在UI线程(这种应该避免),
二是数据处理占用CPU高,导致主线程拿不到时间片,
三是内存增加导致GC频繁,从而引起卡顿。

四、卡顿优化

布局优化

1.减少层级
1.1合理使用RelativeLayout和LinearLayout
(1)尽量使用RelativeLayout和LinearLayout。
(2)在布局层级相同的情况下,使用LinearLayout。
(3)用LinearLayout有时会使嵌套层级变多,应该使用RelativeLayout,使界面尽量扁平化。
1.2合理使用merge
(1)在自定义View中使用,父元素尽量是FrameLayout或者LinearLayout。
(2)在Activity中整体布局,根元素需要是FrameLayout。

merge使用场景:
(1)merge只能用在布局XML文件的根元素。
(2)使用merge来加载一个布局时,必须指定一个ViewGroup作为其父元素,并且要设置加载的attachToRoot参数为true。
(3)不能在ViewStub中使用merge标签,原因是ViewStub的inflate方法中根本没有attachToRoot的设置。

2.使用ViewStub
ViewStub是一个轻量级的View,它是一个看不见,并且不占布局位置,占用资源非常小的视图对象。可以为ViewStub指定一个布局,加载布局时,只有ViewStub会被初始化,然后当ViewStub被设置为可见时,或是调用了ViewStub#inflate时,ViewStub所指向的布局会被加载和实例化,然后ViewStub的布局属性都会传给它指向的布局。这样就可以使用ViewStub来设置是否显示某个布局。

ViewStub使用场景:
(1)在程序运行期间,某个布局在加载后,就不会有变化,除非销毁该页面再重新加载。
(2)想要控制显示和隐藏的是一个布局文件,而非某个View。

3.使用include
使用include标签实现布局复用。

避免过度绘制

过度绘制是指在屏幕上的某个像素在同一帧的时间内被绘制了多次。
在多层次重叠的UI结构中,如果不可见的UI也在做绘制的操作,就会导致某些像素区域被绘制了多次,从而浪费多余的CPU及GPU资源。

过度绘制的主要原因:
(1)XML布局,控件有重叠且都有设置背景。
(2)View自绘,View#onDraw里面同一个区域被绘制多次。

避免过度绘制方法:
(1)布局上优化。
移除XML中非必需的背景,或根据条件设置。
移除Window默认的背景。
按需显示占位背景图片。
(2)自定义View优化。
快速判断Canvas是否需要绘制:Canvas#quickReject。
避免绘制越界:Canvas#clipRect。

合理的刷新机制

1.减少刷新次数
(1)控制刷新频率
(2)避免没有必要的刷新
2.避免后台线程的影响
3.缩小刷新区域
(1)在自定义View中,使用以下两个方法,只更新需要更新的区域,其他不需要数据更新的区域不会更新。
invalidate(Rect dirty)
invalidate(int left, int top, int right, int bottom)
(2)容器中的某个item发生了变化,只需要更新这一个item即可。

提升动画性能

1.优先考虑使用属性动画
2.使用硬件加速
(1)硬件加速原理
在硬件加速的渲染模型中有一个重要的核心类:DisplayList,每个View内部都会维护一个DisplayList。在不支持硬件渲染的Android版本中,系统是通过draw方法或invalidate方法去通知屏幕更新并重新渲染,这两个方法的区别只是实际处理绘制的方式不同;而在支持硬件渲染的Android版本中,在打开硬件渲染后绘制View时,其中执行绘制的draw方法会把所有绘制命令记录到一个新的显示列表(DisplayList),这个显示列表(DisplayList)包含了输出的View层级的绘制代码,但并不是加入到显示列表(DisplayList)就立刻执行,当这个ViewTree的DisplayList全都记录完毕后,由OpenGLRender负责将Root View中的DisplayList渲染到屏幕上。而invalidate方法只是在显示列表中记录和更新显示层级,去标记不需要绘制的View。
(2)硬件加速控制级别
Application级别、Activity级别、Window级别、View级别。

五、相关链接

Android应用卡顿优化之路(一)-TraceView
Systrace 分析性能工具使用方法详解

    原文作者:TomyZhang
    原文地址: https://www.jianshu.com/p/fc7e0bcedc6b
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞