『Android性能优化手册』布局分析与调优

7字数 2559阅读 1400

前言

Android开发中,一个好的应用,除了要有吸引人的功能和交互之外,在性能上也应该有高的要求,如果单单实现页面和业务功能只是完成了基本任务,Android系统对内存要求也是非常高的,稍不注意,就会发生某个页面绘制突然发生卡顿甚至OOM,这对产品的用户体验都是致命性的打击,这就需要我们在日常开发中注意性能方面的优化。


封面

 

目录

  • 造成卡顿的原因
  • 如何分析当前页面绘制情况
    - 使用GPU过度绘制检测页面渲染层级
    - 使用Layout Inspector查看布局层级
  • 如何优化
    - 移除叠加的背景
    - 合理使用布局设计
    - 采用布局标签减少布局嵌套
    ------ include和merge的用法
    ------ ViewStub的用法

 

正文

布局实现是Android开发中必不可少的一部分,绝大部分页面都离不开布局文件的支持,对于有一定经验的开发者来说,通过编写布局文件实现页面展示是一个很简单的操作,然而当页面设计复杂起来,层级越来越深,页面会变得越来越卡顿,作为开发者都应该关注下性能优化,在平时的开发工作中注意一些细节,针对布局文件进行多方位分析和调优,尽可能地去优化应用。
 

造成卡顿的原因

我们都知道,Android中是以层级叠加来实现页面的展示,一个Activity绑定着一个Window,Window又管理着页面的根ViewGroup,然后ViewGroup中包含着View,层层包裹,就如同Photoshop中的图层:

View层级示意图

因此如果同一个位置上面叠加了多个层级,该像素点就会被绘制多次。本来用户只需要看到最上面的那一层就够了,但我们多余的渲染了多次,这就浪费了大量的GPU和CPU资源,并且也增加了绘制时长。而人眼与大脑之间的协作无法感知超过60fps的画面更新,也就是1秒内如果必须展示60帧,才能看起来流畅,1s=1000ms,因此,平均每16ms就要绘制一帧,如果布局层级很深,渲染时长超过16ms,就会看起来稍显卡顿。

 

如何分析当前页面绘制情况

1)使用GPU过度绘制检测页面渲染层级

Android系统支持我们查看页面绘制情况的功能,在手机的设置-开发者选项中有一个调试GPU绘制的开关:

GPU过度绘制开关

打开之后,会发现手机界面上出现了很多颜色区域:
 
显示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:

Layout Inspector

然后选择所要分析的进程,比如选择你自己正在运行中的应用,然后跳转到你要分析的页面,确认之后会在项目目录下生成一个captuers文件夹,Layout Inspector会根据当前手机正在显示的页面生成一个文件存放在这个目录下:

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性能分析)。因此性能上不如LinearLayoutFrameLayout,比如下面这个布局:

布局优化demo

我们可以采用 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>

但是注意,以上是在不增加页面层级的情况下,可以用FrameLayoutLinearLayout代替RelativeLayout实现,但是RelativeLayout也有它的优点,利用它的各种相对属性可以减少我们的页面层级,所以总的来说就是,如果能减少页面层级可以考虑采用RelativeLayout,如果是相同层级的情况下,优先考虑采用FrameLayoutLinearLayout
另外,很多时候我们为了方便喜欢在LinearLayout中,采用它的layout_weight来为子View设置显示的比例,但是layout_weight同样会触发LinearLayout测量两遍,所以慎用。

推荐使用Google推出的一个约束布局——ConstraintLayout,它的出现主要是为了解决布局嵌套过多的问题,以灵活的方式定位和调整小部件。它与 RelativeLayout 一样有相对的属性,但性能上比RelativeLayout更胜一筹。另外它还能按照比例约束控件的位置和尺寸。

 

3)采用布局标签减少布局嵌套

Android中提供了几种布局标签——mergeincludeViewStub。利用它们能够为我们减少很多不必要的嵌套。

include和merge的用法

比如说一个项目中多处使用到一个重复的布局,如下:


布局优化demo

就是一个纯色背景上面叠加一个文字居中的布局,布局文件如下:

<?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查看该页面,本质上布局层级还是跟刚才一样:

include包裹之后的布局层级

这个时候就要结合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>

再次运行,查看布局层级如下:

merge优化后布局层级

可以看到少了一层FrameLayout,我们的ImageViewTextView都被直接添加到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效果图.gif

ViewStub使用时也有一些要注意的点:

ViewStub通过inflate来进行布局的渲染,但是该方法只能调用一次。
ViewStub包裹的布局根部不能是merge标签

 

结语

Android开发中布局优化不是一两天的事,日常开发中灵活运用,根据场景所需采用对应的优化策略,虽然这些都是比较小的细节处理,但是老话说的好,细节决定成败,养成优化的习惯,才能让你的页面体验纵享丝滑。
 

关于作者

一个在奋斗路上的Android小生,欢迎关注,互相交流Android开发的那些事~

GitHubGitHubZJY
CSDN博客IT_ZJYANG
简 书Android小Y

关注Android 技术小栈,更多精彩原创

推荐阅读更多精彩内容