抑郁症健康,内容丰富有趣,生活中的好帮手!
抑郁症健康 > Android性能优化(三)—— 绘制优化

Android性能优化(三)—— 绘制优化

时间:2020-12-01 16:32:14

相关推荐

运行的Android手机,虽然配置在不断的提升,但是仍然无法和PC相比,无法做到PC那样拥有超大内存以及高性能的CPU。因此在开发Android应用程序时也不可能无限制的使用CPU和内存,如果对CPU和 内存使用不当也会造成应用的卡顿和内存溢出等问题。

1 绘制性能分析

Android应用需要将自己的洁面展示给用户,用户会和洁面进行交互,界面流畅度至关重要。

1.1 绘制原理

View的绘制流程有3个步骤,分别是measurelayoutdraw,它们主要运行在系统的应用框架层,而真正将数据渲染到屏幕上的则是系统Native层的SurfaceFlinger服务来完成的。

绘制过程主要由CPU来进行MeasureLayoutRecordExecute的数据计算工作,GPU负责栅格化、渲染。CPUGPU(图形处理器graphics processing unit)是通过图形驱动层来进行连接的,图形驱动层维护了一个队列,CPUdisplay list添加到该队列中,这样GPU就可以从这个队列中取出数据进行绘制。

说到绘制性能就需要提到帧这个概念。帧数就是在1s时间里传输的图片的量,也可以理解为图形处理器每秒钟能刷新几次,通过用FPSFrames 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内完成渲染;同一时间动画执行的次数过多,导致CPUGPU负载过重;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区域,增加蓝色区域的占比。这一措施能够显著提升程序性能。

如果布局中既能采用RealtiveLayoutLinearLayout,那么直接使用LinearLayout,因为Relativelayout的布局比较复杂,绘制的时候需要花费更多的CPU时间。如果需要多个LinearLayout或者Framelayout嵌套,那么可采用Relativelayout。因为多层嵌套导致布局的绘制有大部分是重复的,这会减少程序的性能。

2 布局优化工具 — Layout Inspector

使用布局检查器和布局验证工具调试布局

3 布局优化方法

布局优化方法很多,主要包括合理运用布局、includemergeViewStub

3.1 合理运用布局

常用的布局主要有LinearLayoutRelativeLayoutFrameLayout等,合理地使用它们可以使得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层,一共含有5View,如果用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层,一共有4View,从这里可以看出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>

这个TitleBarImageViewTextView组成。下面将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来提高加载速度

一个常见的开发场景就是某个布局上并不是所有的控件都要显示出来,而是显示其中的一部分,对于这种情况,一般采用的方法就是使用ViewGONEVISIBLE属性,这种方法效率不高,虽然达到了隐藏的目的,但是仍在布局当中,系统仍会解析它们,可以使用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来控制引用的布局了。因此,如果一个控件需要不断地显示和隐藏,还是要使用ViewVisibility属性;ViewStub不能嵌套merge标签;ViewStub操作的是布局文件,如果只是想操作具体的View,还是要使用ViewVisibility属性;

3.5 绘制优化

绘制优化主要是指View.onDraw方法需要避免执行大量的操作:

onDraw方法不需要创建新的局部对象,这是因为onDraw方法是实时执行的,产生大量的临时对象,导致占用了更多内存,并且使系统不断的GC,降低了执行效率;onDraw方法不需要执行耗时操作,在onDraw方法里少使用循环,因为循环会占用CPU的时间。导致绘制不流畅,卡顿等等。Google官方指出,View的绘制帧率稳定在60dps,这要求每帧的绘制时间不超过16ms1000/60)。虽然很难保证,但我们需要尽可能的降低;

60dps是目前最合适的图像显示速度,也是绝大部分Android设备设置的调试频率,如果在16ms内顺利完成界面刷新操作可以展示出流畅的画面,而由于任何原因导致接收到VSYNC信号的时候无法完成本次刷新操作,就会产生掉帧的现象,刷新帧率自然也就跟着下降(假定刷新帧率由正常的60fps降到30fps,用户就会明显感知到卡顿)。

如果觉得《Android性能优化(三)—— 绘制优化》对你有帮助,请点赞、收藏,并留下你的观点哦!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。