前言
Android开发中,一个好的应用,除了要有吸引人的功能和交互之外,在性能上也应该有高的要求,如果单单实现页面和业务功能只是完成了基本任务,Android系统对内存要求也是非常高的,稍不注意,就会发生某个页面绘制突然发生卡顿甚至OOM,这对产品的用户体验都是致命性的打击,这就需要我们在日常开发中注意性能方面的优化。
目录
- 造成卡顿的原因
-
如何分析当前页面绘制情况
- 使用GPU过度绘制检测页面渲染层级
- 使用Layout Inspector查看布局层级 -
如何优化
- 移除叠加的背景
- 合理使用布局设计
- 采用布局标签减少布局嵌套
------ include和merge的用法
------ ViewStub的用法
正文
布局实现是Android开发中必不可少的一部分,绝大部分页面都离不开布局文件的支持,对于有一定经验的开发者来说,通过编写布局文件实现页面展示是一个很简单的操作,然而当页面设计复杂起来,层级越来越深,页面会变得越来越卡顿,作为开发者都应该关注下性能优化,在平时的开发工作中注意一些细节,针对布局文件进行多方位分析和调优,尽可能地去优化应用。
造成卡顿的原因
我们都知道,Android中是以层级叠加来实现页面的展示,一个Activity绑定着一个Window,Window又管理着页面的根ViewGroup,然后ViewGroup中包含着View,层层包裹,就如同Photoshop中的图层:
因此如果同一个位置上面叠加了多个层级,该像素点就会被绘制多次。本来用户只需要看到最上面的那一层就够了,但我们多余的渲染了多次,这就浪费了大量的GPU和CPU资源,并且也增加了绘制时长。而人眼与大脑之间的协作无法感知超过60fps的画面更新,也就是1秒内如果必须展示60帧,才能看起来流畅,1s=1000ms,因此,平均每16ms就要绘制一帧,如果布局层级很深,渲染时长超过16ms,就会看起来稍显卡顿。
如何分析当前页面绘制情况
1)使用GPU过度绘制检测页面渲染层级
Android系统支持我们查看页面绘制情况的功能,在手机的设置-开发者选项中有一个调试GPU绘制的开关:
打开之后,会发现手机界面上出现了很多颜色区域:
GPU过度绘制帮我们显示了每个像素点的绘制情况,并通过几种颜色来代表当前像素点的绘制次数(比如说上图中只有背景的区域都是蓝色,有文字或者图标叠加的地方变成了绿色),GPU过度绘制一共有以下几种颜色:
原色:没有过度绘制
蓝色:1 次过度绘制
绿色:2 次过度绘制
粉色:3 次过度绘制
红色:4 次及以上过度绘制
平常开发的界面中,应该尽可能地将过度绘制控制为 2 次(绿色)及其以下,原色和蓝色是最理想的。粉色和红色应该尽可能避免,在实际项目中避免不了时,应该尽可能减少粉色和红色区域。
2)使用Layout Inspector查看布局层级
在SDK以前的旧版本,是可以通过Hierarchy Viewer来查看,但后面更新了版本,Google采用Layout Inspector来代替Hierarchy Viewer,详见 Android SDK Tools Revision 25.3.0。
可以在新版本的AndroidStudio中,菜单栏的Tools里面找到Layout Inspector:
然后选择所要分析的进程,比如选择你自己正在运行中的应用,然后跳转到你要分析的页面,确认之后会在项目目录下生成一个captuers文件夹,Layout Inspector会根据当前手机正在显示的页面生成一个文件存放在这个目录下:
一共有三块区域,左边可以看到页面的层级,从最顶层的DecorView开始,其下所有当前页面存在的View都会显示在这里(关于DecorView的层级可见我另一篇文章Android 从源码看懂窗口绘制流程),中间显示的是当前页面的预览,右边显示的是每个View的布局属性,包括宽高、padding等等。通过Layout Inspector能清晰地看到页面的层次结构,比如说布局文件中某一处重复包裹了两层View,或者不小心在自定义ViewGroup的时候多加了一层根View,在这里都能看得出来。
注意:Layout Inspector有一定限制,要求运行的机器Android版本为16(Android4.1)以上,且要是当前正在运行的应用进程才可以,其实它就相当于对当前界面的一个快照。
如何优化
1)移除叠加的背景
我们注册Activity时一般都会为它设置主题,主题一般都会有默认背景 windowBackground
,比如下面这种:
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="windowBackground">@color/colorPrimary</item>
</style>
它会为我们的页面设置一个背景,但有些时候,布局文件里面的根布局也会设置一个背景,这个时候window的background用户完全看不到,实际上没有作用,这种情况下我们可以移除它的背景:
<item name="android:windowBackground">@null</item>
刚才是针对window的背景做了处理,同理,布局嵌套中也有可能出现这种情况,比如说一个LinearLayout里包裹了两个子View,而且这两个子View刚好占满了LinearLayout的全部空间,那LinearLayout同样就没必要设置背景了。
2)合理使用布局设计
我们平时都是用五大布局组合成页面的结构,相同的效果,可以用不同的ViewGroup来组合实现,但是RelativeLayout
底层会测绘两次,而LinearLayout和FrameLayout只会绘制一次(详见 RelativeLayout和LinearLayout及FrameLayout性能分析)。因此性能上不如LinearLayout
和FrameLayout
,比如下面这个布局:
我们可以采用
RelativeLayout
来实现:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#ffffff"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Title"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:text="Describe"/>
</RelativeLayout>
也可以采用 FrameLayout
来实现:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#ffffff"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left"
android:text="Title"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:text="Describe"/>
</FrameLayout>
但是注意,以上是在不增加页面层级的情况下,可以用FrameLayout
和LinearLayout
代替RelativeLayout
实现,但是RelativeLayout
也有它的优点,利用它的各种相对属性可以减少我们的页面层级,所以总的来说就是,如果能减少页面层级可以考虑采用RelativeLayout
,如果是相同层级的情况下,优先考虑采用FrameLayout
和LinearLayout
。
另外,很多时候我们为了方便喜欢在LinearLayout
中,采用它的layout_weight
来为子View设置显示的比例,但是layout_weight
同样会触发LinearLayout测量两遍,所以慎用。
推荐使用Google推出的一个约束布局——ConstraintLayout
,它的出现主要是为了解决布局嵌套过多的问题,以灵活的方式定位和调整小部件。它与 RelativeLayout
一样有相对的属性,但性能上比RelativeLayout
更胜一筹。另外它还能按照比例约束控件的位置和尺寸。
3)采用布局标签减少布局嵌套
Android中提供了几种布局标签——merge
、include
、ViewStub
。利用它们能够为我们减少很多不必要的嵌套。
include和merge的用法
比如说一个项目中多处使用到一个重复的布局,如下:
就是一个纯色背景上面叠加一个文字居中的布局,布局文件如下:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="@color/colorPrimary"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textColor="#ffffff"
android:text="Common Card"/>
</FrameLayout>
</FrameLayout>
这样一方面布局文件显得很累赘,当View多起来时不方便查看,另一方面万一项目中要统一更改该布局,也要一处处改动,这个时候可以考虑将我们重复的部分抽取成一个xml文件:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="@color/colorPrimary"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textColor="#ffffff"
android:text="Common Card"/>
</FrameLayout>
通过 include
标签来将刚才的布局引入:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<include
android:layout_width="match_parent"
android:layout_height="wrap_content"
layout="@layout/layout_item"/>
</FrameLayout>
但是通过include
只是换了种方式包裹布局,我们使用Layout Inspector查看该页面,本质上布局层级还是跟刚才一样:
这个时候就要结合merge
标签来进行合并了,使用merge
标签可以帮我们忽略掉我们的子布局的根View,相当于直接将子布局添加到我们主布局下,我们将刚才的 layout_item
的根FrameLayout改为merge标签:
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<ImageView
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="@color/colorPrimary"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textColor="#ffffff"
android:text="Common Card"/>
</merge>
再次运行,查看布局层级如下:
可以看到少了一层
FrameLayout
,我们的ImageView
和TextView
都被直接添加到activity的根FrameLayout
下,因此merge
经常与include
搭配使用,减少include
场景下的布局嵌套。merge
还有其他使用场景,比如Activity的xml文件的根View是FrameLayout,并且只有宽高,没有设置任何其他属性时,可以考虑采用merge
来替换它,因为我们xml根View的父View其实也是个FrameLayout,所以实际上是重叠了两层。
但是,merge
标签有很多要注意的地方:
merge标签必须使用在根布局(这也正是为何推荐搭配include使用的原因)
merge会帮我们忽略掉根View,因此根View的布局属性也全都会失效,会直接采用主布局中其父View的布局属性
merge标签本质上不是一个View,对它设置的任何布局属性都是没有意义的,并且在通过LayoutInflate.inflate()方法获取它的时候,第二个参数必须指定一个父容器,且第三个参数必须为true,也就是必须为merge下的视图指定一个父亲节点。(详见我另一篇 LayoutInflater.inflate各个参数作用了解一下?)
ViewStub的用法
ViewStub可以用来包裹布局,被包裹的布局在页面加载时是没有被加载出来的,只有调用了viewStub.inflate()
或者viewStub.setVisible()
时,才会被加载出来,也就是类似一种懒加载的机制,很适用于用来包裹我们的一些缺省布局,比如无网络提示、加载错误提示等等,或者一些不需要页面一启动就显示出来的View,因为这些布局不一定会展示给用户,如果全部写在layout文件里面的话,页面加载的时候无论可不可见实际上都是会加载出来的(注意区分加载和可见的概念)。
以无网络提示为例子,首先定义我们的无网络布局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_gravity="center_horizontal"
android:src="@drawable/ic_nonet"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Check Your Internet Connection"/>
</LinearLayout>
将其通过ViewStub引入到主布局文件中:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ViewStub
android:id="@+id/no_net_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout="@layout/layout_no_net"/>
<TextView
android:id="@+id/loading_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="正在加载中..."/>
</FrameLayout>
在Activity中模拟无网络提示,让其延迟2秒加载:
viewStub = findViewById(R.id.no_net_view);
loadingTv = findViewById(R.id.loading_tv);
Handler handler = new Handler();
handler.postDelayed(new Runnable() {
@Override
public void run() {
loadingTv.setVisibility(View.GONE);
viewStub.inflate();
}
}, 2000);
效果如下:
ViewStub
使用时也有一些要注意的点:
ViewStub通过inflate来进行布局的渲染,但是该方法只能调用一次。
ViewStub包裹的布局根部不能是merge标签
结语
Android开发中布局优化不是一两天的事,日常开发中灵活运用,根据场景所需采用对应的优化策略,虽然这些都是比较小的细节处理,但是老话说的好,细节决定成败,养成优化的习惯,才能让你的页面体验纵享丝滑。
关于作者
一个在奋斗路上的Android小生,欢迎关注,互相交流Android开发的那些事~