美文网首页Android进阶之路Android性能Android开发
『Android性能优化手册』布局分析与调优

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

作者: Android小Y | 来源:发表于2019-04-08 21:38 被阅读108次

    前言

    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 技术小栈,更多精彩原创

    相关文章

      网友评论

        本文标题:『Android性能优化手册』布局分析与调优

        本文链接:https://www.haomeiwen.com/subject/akaciqtx.html