Android 性能优化之布局优化

在Android应用开发中,常用的布局方式主要有LinearLayout、RelativeLayout、FrameLayout等,通过这些布局可以实现各种各样的界面。我们需要知道如何高效地使用这些布局方式来组织UI控件,布局的好坏影响到绘制的时间,本节将通过减少Layout层级减少测量绘制时间提高复用性三个方面来优化布局,优化的目的就是减少层级,让布局扁平化,以提高绘制的时间,提高布局的复用性节省开发和维护成本。

减少层级

层级越少,测试和绘制的时间就越短,通常减少层级有以下两个常用方案:
合理使用 RelativeLayout 和 LinearLayout。
合理使用Merge。

(1)RelativeLayout与LinearLayout

使用 RelativeLayout 减少层级:

!

通过Hierarchy View来查看下层级情况:

!

可以看到一共有7级,使用RelativeLayout进行优化,达到相同的布局效果,并且RelativeLayout允许子元素指定它们相对于其他元素或父元素的位置,有最大自由度的布局属性,而且布局层次最浅,占用内存最少。

!

这样就可以减少两个层级,用一个RelativeLayout就可以达到显示的效果,再使用Hierarchy View来查看层级,可以看到减少到5层。

但ReativeLayout也存在性能低的问题,原因是RelativeLayout会对子View做两次测量,在RelativeLayout中子View的排列方式是基于彼此的依赖关系,因为这个依赖关系可能和布局中View的顺序并不相同,在确定每个子View的位置时,需要先给所有子View做一次排序。如果在RelativeLayout中允许子View横向和纵向互相依赖,就需要横向、纵向分别进行一次排序测量。但如果在LinearLayout中有weight属性,也需要进行两次测量,因为没有更多的依赖关系,所以仍然会比RelativeLayout的效率高,在布局上RelativeLayout不如LinearLayout快

但是如果布局本身层次太深,还是推荐用RelativeLayout减少布局本身层次,相较于测量两次,虽然会增加一些计算时间,但在体验上影响不会特别大,如果优化掉两层仅仅是增加一次测量,还是非常值得的,布局层次深会增加内存消耗,甚至引起栈溢出等问题,即使耗点时间,也不能让应用不可用。

根据以上分析,可以总结出以下几点布局原则:
尽量使用RelativeLayout和LinearLayout。
在布局层级相同的情况下,使用LinearLayout。
用LinearLayout有时会使嵌套层级变多,应该使用RelativeLayout,使界面尽量扁平化。

注意
由于Android的碎片化程度很高,市面上的屏幕尺寸也是各式各样,使用RelativeLayout能使构建的布局适应性更强,构建出来的UI布局对多屏幕的适配效果更好,通过指定UI控件间的相对位置,使不同屏幕上布局的表现基本保持一致。当然,也不是所有情况下都得使用相对布局,根据具体情况选择和搭配使用其他布局方式来实现最优布局。

(2)Merge的使用

从名字上就可以看出,Merge就是合并的意思。使用它可以有效优化某些符合条件的多余的层级。使用Merge的场合主要有以下两处:

在自定义View中使用,父元素尽量是FrameLayout或者LinearLayout。
在Activity中整体布局,根元素需要是FrameLayout。

我们仍以前面的布局为例,在页面增加一个自定义控件TopBar:

其中TopBar的XML布局如下:

显示结果下图所示。这种布局在一些列表的Item中非常常见,而且列表中Item本身的层级比较深,因此优化显得更有意义。

我们使用HierarchyView查看增加TopBar后的布局层级,如图。可以看到,就是这么简单的一个布局,却把层级增加了两级,从图中很明显地看出TopBar后一层的LinearLayout是多余的,这时可以使用Merge把这一层消除。

使用Merge来优化布局,使用Merge标签替换LinearLayout后,原来的LinearLayout属性也没有用了,修改后的代码如下

运行后再使用Hierarchy View查看当前层级

这样就把多余的LinearLayout消除了,原理是在Android布局的源码中,如果是Merge标签,那么直接将其中的子元素添加到Merge标签Parent中,这样就保证了不会引入额外的层级。

注意
如果Merge代替的布局元素为LinearLayout,在自定义布局代码中将LinearLayout的属性添加到引用上,如垂直或水平布局、背景色等。

但Merge不是所有地方都可以任意使用,有以下几点要求:

Merge只能用在布局XML文件的根元素。

使用merge来加载一个布局时,必须指定一个ViewGroup作为其父元素,并且要设置加载的attachToRoot参数为true(参照inf late(int, ViewGroup, boolean))。

不能在ViewStub中使用Merge标签。原因就是ViewStub的inf late方法中根本没有attachToRoot的设置。

这一节讲了如何减少层级,那么在Android系统中,多少层才是合理的呢?当然是越少越好,但从Lint检查的配置上看,超过10层才会报警,实际上在开发时,随着产品设计的丰富和多样性,很容易超过10层,根据实际开发过程中超过15层就要重视并准备做优化,20层就必须修改了。在实在没有办法优化的情况下,需要把复杂的层级用自绘控件来实现,自绘控件中的图层层级再多,在布局上也只是一层,但这样也会带来过度绘制的问题,后面会讲。

注意
在Activiy的总布局中使用Merge,但又想设置整体的属性(布局方式或背景色),可以不使用setContentView方法加载Layout,而使用(id/content)将FrameLayout取出来,在代码中手动加载布局,但如果层级压力不大(小于10级),则没有必要,因为这样代码的维护性较差。

提高显示速度

我们在开发的过程中会碰到这样的场景或者显示逻辑:某个布局当中的子布局非常多,但并不是所有元素都同时显示出来,而是二选一或者N选一,打开这个界面根据不同的场景和属性显示不同的Layout。例如:一个页面对不同的用户(未登录、普通用户、会员)来说,显示的布局不同。或者,有些用户喜欢对不同的元素使用INVISIBLE或者GONE隐藏,通过设计元素的visable属性来控制,这样虽然达到了隐藏的目的,但效率非常低,原因是即使将元素隐藏,它们仍在布局中,仍会测试和解析这些布局。Android提供了ViewStub控件来解决这个场景。

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

下面的代码是两个ViewStub通过不同的初始化来加载两个不同的布局,以满足用户的需求。

在调用时,根据需求切换不同的Layout,这样可以提高页面初始化的速度,使用代码如下:

ViewStub显示有两种方式,上面代码使用的是inf ate方法,也可以直接使用ViewStub.setVisibiltity(View.Visible)方法。
使用ViewStub时需要注意以下几点:

  1. ViewStub只能加载一次,之后ViewStub对象会被置为空。换句话说,某个被ViewStub指定的布局被加载后,就不能再通过ViewStub来控制它了。所以它不适用于需要按需显示隐藏的情况。

  2. ViewStub只能用来加载一个布局文件,而不是某个具体的View,当然也可以把View写在某个布局文件中。如果想操作一个具体的View,还是使用visibility属性。

  3. VIewStub中不能嵌套Merge标签。

不过这些限制都无伤大雅,我们还是能够用ViewStub来做很多事情,ViewStub的主要使用场景如下:
在程序运行期间,某个布局在加载后,就不会有变化,除非销毁该页面再重新加载。
想要控制显示与隐藏的是一个布局文件,而非某个View。

注意
因为ViewStub只能Inf late一次之后会被置空无法继续使用ViewStub来控制布局。所以当需要在运行时不止一次显示和隐藏某个布局时,使用ViewStub是无法实现的。这时只能使用View的可见性来控制。

布局复用

我们在开发应用时还会碰到另一个常见的场景,就是一个相同的布局在很多页面(Activity或Fragment)会用到,如果给这些页面的布局文件都统一加上相同的布局代码,维护起来就很麻烦,可读性也差,一旦需要修改,很容易有漏掉的地方,Android的布局复用可以通过标签来实现,就像提取代码公用部分一样,在编写Android布局文件时,也可以将相同的部分提取出来,在使用时,用添加进去。

例如,在大部分应用中,基本上所有的应用都会带有头部栏(TopBar),主要是显示标题和返回键功能,这样只需要维护一份代码,就可以修改所有的显示效果。

提示
类似于TopBar的这类常用控件,包括菜单,可以把具体实现抽象到页面的基类(BaseActivity)中,这样布局和具体的实现都收归到一个地方,方便维护。
提高布局效率的方法总体来说就是减少层级,提高绘制速度和布局复用。影响布局效率主要有以下几点:

布局的层级越少,加载速度越快。
减少同一层级控件的数量,加载速度会变快。
一个控件的属性越少,解析越快。

根据本节的分析,对优化的总结如下:
尽量多使用RelativeLayout或LinearLayout,不要使用绝对布局AbsoluteLayout。
将可复用的组件抽取出来并通过< include />标签使用。
使用< ViewStub />标签加载一些不常用的布局。
使用< merge />标签减少布局的嵌套层次。
尽可能少用wrap_content, wrap_content会增加布局measure时的计算成本,已知宽高为固定值时,不用wrap_content。
删除控件中的无用属性。

避免过度绘制

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

当设计上追求更华丽的视觉效果时,我们很容易陷入采用复杂的多层次重叠视图来实现这种视觉效果的怪圈。这很容易导致大量的性能问题,为了获得最佳性能,必须尽量减少Overdraw情况发生。

我们一般在XML布局和自定义控件中绘制,因此可以看出导致过度绘制的主要原因是:
XML布局->控件有重叠且都有设置背景
View自绘-> View.OnDraw里面同一个区域被绘制多次

如何查看是否有过度绘制:

在手机的“设置”→“开发者选项”中打开“显示GPU过度重绘”开关(注:对未默认开启硬件加速的界面需要同时打开“强制进行GPU渲染”开关)。

打开后可以根据不同的颜色观察UI上的Overdraw情况,蓝色、淡绿、淡红、深红代表4种不同程度的Overdraw情况,不同颜色的含义如下:
无色:没有过度绘制,每个像素绘制了1次。
蓝色:每个像素多绘制了1次。大片的蓝色还是可以接受的。如果整个窗口是蓝色的,可以尝试优化减少一次绘制。
绿色:每个像素多绘制了2次。
淡红:每个像素多绘制了3次。一般来说,这个区域不超过屏幕的1/4是可以接受的。
深红:每个像素多绘制了4次或者更多。严重影响性能,需要优化,避免深红色区域。
我们的目标是尽量减少红色Overdraw,看到更多的蓝色区域。

如何避免过度绘制

1.布局上的优化

在XML布局上,如果出现了过度绘制的情况,可以使用Hierarchy View来查看具体的层级情况,可以通过XML布局优化来减少层级。需要注意的是,在使用XML文件布局时,会设置很多背景,如果不是必需的,尽量移除。布局优化总结为以下几点:
移除XML中非必需的背景,或根据条件设置。

移除Window默认的背景。

按需显示占位背景图片。

使用Android自带的一些主题时,activity往往会被设置一个默认的背景,这个背景由DecorView持有。当自定义布局有一个全屏的背景时,比如设置了这个界面的全屏黑色背景,DecorView的背景此时对我们来说是无用的,但是它会产生一次Overdraw。因此没有必要的话,也可以移除,代码如下:

注意
针对 ListView 中的 Avatar ImageView 的设置,在 getView 的代码中,判断是否获取对应的 Bitmap,获取 Avatar的图像之后,把 ImageView 的 Background 设置为 Transparent,只有当图像没有获取到时,才设置对应的Background占位图片,这样可以避免因为给Avatar设置背景图而导致的过度渲染。

2.自定义View优化

事实上,由于我们的产品设计总是追求更华丽的视觉效果,仅仅通过布局优化很难做到最好,这时可以对复杂的控件使用自定义View来实现,虽然自定义View减少了Layout的层级,但在实际绘制时也是会过度绘制的。原因是有些过于复杂的自定义View(通常重写了onDraw方法), Android系统无法检测在onDraw中具体会执行什么操作,无法监控并自动优化,也就无法避免Overdraw了。

但是在自定义View中可以通过canvas.clipRect()来帮助系统识别那些可见的区域。这个方法可以指定一块矩形区域,只有在这个区域内才会被绘制,其他的区域会被忽视。canvas.clipRect()可以很好地帮助那些有多组重叠组件的自定义View来控制显示的区域。clipRect方法还可以帮助节约CPU与GPU资源,在clipRect区域之外的绘制指令都不会被执行,那些部分内容在矩形区域内的组件,仍然会得到绘制,并且可以使用canvas.quickreject()来判断是否没和某个矩形相交,从而跳过那些非矩形区域内的绘制操作。接下来介绍使用一个自定义View避免OverDraw的案例。

快速判断Canvas是否需要绘制:Canvas.QuickReject:

在绘制一个单元之前,首先判断该单元的区域是否在Canvas的剪切域内。若不在,直接返回,避免CPU和GPU的计算和渲染工作。

避免绘制越界:Canvas.ClipRect:

每个绘制单元都有自己的绘制区域,绘制前,Canvas.ClipRect(Region.Op.INTERSECT)帮助系统识别那些可见的区域。这个方法可以指定一块矩形区域,只有在这个区域内,才会被绘制,其他的区域被忽视。这个API可以很好地帮助那些有多组重叠组件的自定义View来控制显示的区域。clipRect方法还可以帮助节约CPU与GPU资源,在clipRect区域之外的绘制指令都不会被执行,那些部分内容在矩形区域内的组件,仍然会得到绘制。

本文参考:罗彧成的《Android应用性能优化最佳实践》一书。强烈推荐此书!

文章目录
  1. 1. 减少层级
    1. 1.1. (1)RelativeLayout与LinearLayout
    2. 1.2. (2)Merge的使用
  2. 2. 提高显示速度
  3. 3. 布局复用
  4. 4. 避免过度绘制
    1. 4.1. 如何避免过度绘制
|