运行的Android
手机,虽然配置在不断的提升,但是仍然无法和PC
相比,无法做到PC
那样拥有超大内存以及高性能的CPU
。因此在开发Android
应用程序时也不可能无限制的使用CPU
和内存,如果对CPU
和 内存使用不当也会造成应用的卡顿和内存溢出等问题。
1 绘制性能分析
Android
应用需要将自己的洁面展示给用户,用户会和洁面进行交互,界面流畅度至关重要。
1.1 绘制原理
View
的绘制流程有3
个步骤,分别是measure
、layout
和draw
,它们主要运行在系统的应用框架层,而真正将数据渲染到屏幕上的则是系统Native
层的SurfaceFlinger
服务来完成的。
绘制过程主要由CPU
来进行Measure
、Layout
、Record
、Execute
的数据计算工作,GPU
负责栅格化、渲染。CPU
和GPU
(图形处理器graphics processing unit
)是通过图形驱动层来进行连接的,图形驱动层维护了一个队列,CPU
将display list
添加到该队列中,这样GPU
就可以从这个队列中取出数据进行绘制。
说到绘制性能就需要提到帧这个概念。帧数就是在1s
时间里传输的图片的量,也可以理解为图形处理器每秒钟能刷新几次,通过用FPS
(Frames Per Second
)表示。每一帧其实就是静止的图像,通过快速连续地显示帧便形成了运动的假象。最简单的举例就是我们在玩游戏的时候,如果画面在60fps
则不会感到卡顿,如果低于60fps
,比如50fps
则会感到卡顿。这是因为人类的大脑会不断的接收并处理眼球看到的信息,单位时间内越多的帧被处理,就越能有效地被大脑识别,大脑能感知的最小的帧数载10fps ~ 12fps
,这个时候大脑就分不清楚这个图像是静止的还是变化的。
要想画面保持在60fps
,需要屏幕在1s
内刷新60
次,也就是没16.6667ms
刷新一次(绘制时长在16ms
以内)。
Android
系统每隔16ms
发出VSYNC
信号,触发对UI
进行渲染,如果每次渲染都成功,这样就能够达到流畅画面所需要的60fps
,那什么是VSYNC
呢?VSYNC
就是Vertical Synchronization
(垂直同步)的缩写,是一种定时中断,一旦收到VSYNC
信号,CPU
就开始处理各帧数据。如果某个操作要花费24ms
,这样系统在得到VSYNC
信号时无法进行正常的渲染,会发生丢帧。用户会在32ms
中看到同一帧的画面。
产生卡顿原因有很多,主要有以下几点:
布局Layout
过于复杂,无法在16ms
内完成渲染;同一时间动画执行的次数过多,导致CPU
或GPU
负载过重;View
过度绘制,导致某些像素在同一帧时间内会被绘制多次;在UI
线程中做了稍微耗时的操作;GC
回收时暂停时间过长或者频繁GC
产生大量的暂停时间;
1.2 工具
1.2.1 Profile GPU Rendering
Profile GPU Rendering 是Android 4.1
系统提供的开发辅助功能,可以在开发者选项中打开这一功能:设置–>开发者选项–>GPU呈现模式分析–>在屏幕上显示为条形图:
图中横轴代表时间,纵轴表示某一帧的耗时,绿色的横线为警戒线,超过这条线则意味着时长超过了16ms
,尽量要保证垂直的彩色柱状图保持在绿线下面。这些垂直的彩色柱状图代表着一帧,不同颜色的彩色柱状图代表不同的含义:
橙色代表处理的时间,是CPU
告诉GPU
渲染一帧的地方,这是一个阻塞调用,因为CPU
会一直等待GPU
发出接到命令的回复,如果橙色柱状图很高,则表明GPU
很繁忙;红色代表执行的时间,这部分是Android
进行2D
渲染Display List
的时间,如果红色的柱状图很高,可能由于重新提交了视图而导致的。还有复杂的自定义View
也会导致红的柱状图变高;蓝色代表测量绘制的时间,也就是需要多长时间去创建和更新Display List
。如果蓝色柱状图很高,可能需要重新绘制,或者View.onDraw()
方法处理事情太多;
随着界面的刷新,界面上会以实时柱状图来显示每帧的渲染时间,柱状图越高表示渲染时间越长,每个柱状图偏上都有一根代表16ms
基准的绿色横线,每一条竖着的柱状线都包含三部分(蓝色代表测量绘制Display List
的时间,红色代表OpenGL
渲染Display List
所需要的时间,黄色代表CPU
等待GPU
处理的时间),只要我们每一帧的总时间低于基准线就不会发生UI
卡顿问题(个别超出基准线其实也不算什么问题的)。
1.2.2GPU
绘制
对于UI
性能的优化还可以通过开发者选项中的GPU
过度绘制工具来进行分析。在设置->开发者选项->调试GPU
过度绘制(不同设备可能位置或者叫法不同)中打开调试后可以看见如下图(对settings
当前界面过度绘制进行分析):
以下说明:
蓝色(1x
过度绘制),淡绿(2x
过度绘制),淡红(3x
过度绘制),深红(4x
过度绘制)代表了4
种不同程度的Overdraw
情况,我们的目标就是尽量减少红色Overdraw
,看到更多的蓝色区域。
Overdraw
有时候是因为UI
布局存在大量重叠的部分,还有的时候是因为非必须的重叠背景。例如某个Activity
有一个背景,然后里面的Layout
又有自己的背景,同时子View
又分别有自己的背景。仅仅是通过移除非必须的背景图片,这就能够减少大量的红色Overdraw
区域,增加蓝色区域的占比。这一措施能够显著提升程序性能。
如果布局中既能采用RealtiveLayout
和LinearLayout
,那么直接使用LinearLayout
,因为Relativelayout
的布局比较复杂,绘制的时候需要花费更多的CPU
时间。如果需要多个LinearLayout
或者Framelayout
嵌套,那么可采用Relativelayout
。因为多层嵌套导致布局的绘制有大部分是重复的,这会减少程序的性能。
2 布局优化工具 — Layout Inspector
使用布局检查器和布局验证工具调试布局
3 布局优化方法
布局优化方法很多,主要包括合理运用布局、include
、merge
和ViewStub
。
3.1 合理运用布局
常用的布局主要有LinearLayout
、RelativeLayout
和FrameLayout
等,合理地使用它们可以使得Android
绘制工作量变少,性能得到提高。举例来说:
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="/apk/res/android"xmlns:app="/apk/res-auto"xmlns:tools="/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="horizontal"tools:context=".MainActivity"><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="布局优化" /><LinearLayoutandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginLeft="10dp"android:orientation="vertical"><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="Merge" /><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="ViewStub" /></LinearLayout></LinearLayout>
可以看到布局共三层:
布局共3
层,一共含有5
个View
,如果用RelativeLayout
进行改写,代码如下所示:
<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="/apk/res/android"xmlns:app="/apk/res-auto"xmlns:tools="/tools"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".MainActivity"><TextViewandroid:id="@+id/tv_text1"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="布局优化" /><TextViewandroid:id="@+id/tv_text2"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginLeft="10dp"android:layout_toRightOf="@+id/tv_text1"android:text="Merge" /><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginLeft="10dp"android:layout_toRightOf="@+id/tv_text1"android:layout_below="@+id/tv_text2"android:text="ViewStub" /></RelativeLayout>
可以看到布局有两层:
布局有2
层,一共有4
个View
,从这里可以看出RelativeLayout
减少了一层的布局。如果布局复杂,可以合理的利用RelativeLayout
来减少布局层次。RelativeLayout
的性能比LinearLayout
低,因为RelativeLayout
中的View
排列方式是基于彼此依赖的。
但是,在实际开发过程中面对的情况比较多,不能轻易说谁的性能更好。在一般情况下,如果布局层数较多时,推荐使用RelativeLayout
,如果布局嵌套较多,推荐使用LinearLayout
来实现。
3.2 使用include
标签来进行布局复用
当多个布局需要复用一个相同的布局,比如一个TitleBar
,如果这些洁面都要加上这个相同布局TitleBar
,维护起来很麻烦,需要复制TitleBar
的布局到每个需要添加的洁面,这样容易发生遗漏。如果修改TitleBar
则需要去引用TitleBar
的布局中进行修改。为了解决这些问题,可以用include
标签来解决。
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="/apk/res/android"android:layout_width="match_parent"android:layout_height="40dp"><ImageViewandroid:layout_width="30dp"android:layout_height="30dp"android:layout_gravity="center"android:src="@drawable/ic_launcher_background"android:padding="3dp" /></LinearLayout>
这个TitleBar
由ImageView
和TextView
组成。下面将TitleBar
引入到此前用过的布局中,如下所示:
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="/apk/res/android"xmlns:app="/apk/res-auto"xmlns:tools="/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"><include layout="@layout/title_bar" /><RelativeLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"tools:context=".MainActivity"><TextViewandroid:id="@+id/tv_text1"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="布局优化" /><TextViewandroid:id="@+id/tv_text2"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginLeft="10dp"android:layout_toRightOf="@+id/tv_text1"android:text="Merge" /><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_below="@+id/tv_text2"android:layout_marginLeft="10dp"android:layout_toRightOf="@+id/tv_text1"android:text="ViewStub" /></RelativeLayout></LinearLayout>
可以看到布局有两层:
3.3 用merge
标签去除多余层级
merge
意味着合并,在合适的场景使用merge
标签可以减少多余的层级。merge
标签一般和include
标签搭配使用。对于上一节的例子,如果用merge
标签来替换LinearLayout
,代码如下所示:
<?xml version="1.0" encoding="utf-8"?><merge xmlns:android="/apk/res/android"android:layout_width="match_parent"android:layout_height="40dp"><ImageViewandroid:layout_width="30dp"android:layout_height="30dp"android:layout_gravity="center"android:padding="3dp"android:src="@drawable/ic_launcher_background" /><TextViewandroid:layout_width="match_parent"android:layout_height="wrap_content"android:layout_gravity="center"android:gravity="center"android:text="绘制优化" /></merge>
布局层次:
可以看到,之前的LinearLayout
没有了,但是这里有merge
标签来替代LinearLayout
会导致LinearLayout
失效,布局会错乱。merge
标签最好是替代FrameLayout
或者布局方向一致的LinearLayout
,比如当前父布局的LinearLayout
的布局方向是垂直的,包含的子布局LinearLayout
的布局防线也是垂直的,就可以用merge
标签。但是本场景下TitleBar
的跟布局LinearLayout
的布局方向是水平的,显然不符合这一要求。
3.4 使用ViewStub
来提高加载速度
一个常见的开发场景就是某个布局上并不是所有的控件都要显示出来,而是显示其中的一部分,对于这种情况,一般采用的方法就是使用View
的GONE
和VISIBLE
属性,这种方法效率不高,虽然达到了隐藏的目的,但是仍在布局当中,系统仍会解析它们,可以使用ViewStub
来解决这一问题。
ViewStub
是轻量级的View
,不可见并且不占据布局位置。当ViewStub
调用inflate
方法或者设置可见时,系统会夹在ViewStub
指定的布局,然后将这个布局添加到ViewStub
中,在对ViewStub
调用inflate
方法或者设置可见之前,它是不占据布局空间和系统资源的,它主要的目的就是为了目标视图占用一个位置。因此,使用ViewStub
可以提高洁面初始化的性能,从而提高界面的加载速度。首先,在布局中加入ViewStub
标签:
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="/apk/res/android"xmlns:app="/apk/res-auto"xmlns:tools="/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"><ViewStubandroid:layout_width="match_parent"android:layout_height="40dp"android:layout="@layout/title_bar" /></LinearLayout>
在ViewStub
标签中使用android:layout
引用了此前写好的布局title_bar.xml
。在运行程序时,ViewStub
标签所引用的布局是显示不出来的,因为该布局还没有加载到ViewStub
中,接下来在代码中使用ViewStub
:
public class MainActivity extends AppCompatActivity {private ViewStub viewStub;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);viewStub = findViewById(R.id.view_stub);viewStub.inflate(); // 1viewStub.setVisibility(View.VISIBLE); // 2}}
注释1
和注释2
处的嗲吗用来将ViewStub
应用的布局家在到ViewStub
中,这样应用布局就显示出来了。在使用ViewStub
时需要注意以下问题:
ViewStub
只能加载一次,加载后ViewStub
对象会被置为空,这样在ViewStub
引用的布局被加载后,就不能用用ViewStub
来控制引用的布局了。因此,如果一个控件需要不断地显示和隐藏,还是要使用View
的Visibility
属性;ViewStub
不能嵌套merge
标签;ViewStub
操作的是布局文件,如果只是想操作具体的View
,还是要使用View
的Visibility
属性;
3.5 绘制优化
绘制优化主要是指View.onDraw
方法需要避免执行大量的操作:
onDraw
方法不需要创建新的局部对象,这是因为onDraw
方法是实时执行的,产生大量的临时对象,导致占用了更多内存,并且使系统不断的GC
,降低了执行效率;onDraw
方法不需要执行耗时操作,在onDraw
方法里少使用循环,因为循环会占用CPU
的时间。导致绘制不流畅,卡顿等等。Google官方指出,View
的绘制帧率稳定在60dps
,这要求每帧的绘制时间不超过16ms
(1000/60
)。虽然很难保证,但我们需要尽可能的降低;
60dps
是目前最合适的图像显示速度,也是绝大部分Android
设备设置的调试频率,如果在16ms
内顺利完成界面刷新操作可以展示出流畅的画面,而由于任何原因导致接收到VSYNC
信号的时候无法完成本次刷新操作,就会产生掉帧的现象,刷新帧率自然也就跟着下降(假定刷新帧率由正常的60fps
降到30fps
,用户就会明显感知到卡顿)。
如果觉得《Android性能优化(三)—— 绘制优化》对你有帮助,请点赞、收藏,并留下你的观点哦!