用ConstraintLayout来优化Android的XML布局的层级

邓琴文

背景

Android中XML布局作为用户界面直接显示在Activity(活动、界面)上。但是一个XML布局中的View树的高度会影响测量,布局和绘制的速度和用户体验。View树的高度越高,那么需要测量、布局和绘制的时间就越长。所以我们要优化XML布局,就要想办法来降低View树的高度,提高用户体验。其中最有效的就是减少布局层级。

以前我们一直用RelativeLayout来减少布局层级,但是RelativeLayout在减少布局层级方面还有优化的地方,所以谷歌在它的基础上推出了ConstraintLayout。

那么ConstraintLayout到底有什么性能优势呢?


Android 如何绘制视图?

为了更好地理解ConstraintLayout的性能,我们先回过头来看看Android 如何绘制视图。

当用户将某个Android视图作为焦点时,Android框架会指示该视图进行自我绘制。这个绘制过程包括 3 个阶段:

1. 测量
系统自顶向下遍历视图树,以确定每个ViewGroup 和 View 元素应当有多大。在测量 ViewGroup 的同时也会测量其子对象。
2. 布局
系统执行另一个自顶向下的遍历操作,每个 ViewGroup 都根据测量阶段中所确定的大小来确定其子对象的位置。
3. 绘制
系统再次执行一个自顶向下的遍历操作。对于视图树中的每个对象,系统会为其创建一个 Canvas 对象,以便向GPU发送一个绘制命令列表。这些命令包含系统在前面 2 个阶段中确定的 ViewGroup 和 View 对象的大小和位置。

image

绘制过程中的每个阶段都需要对视图树执行一次自顶向下的遍历操作。因此,视图层次结构中嵌入(或嵌套)的视图越多,设备绘制视图所需的时间和计算功耗也就越多。我们通过在Android 应用布局中保持扁平的层次结构,可以为应用创建响应快速而灵敏的界面。

比较使用ConstraintLayout和其他布局容器的XML布局的嵌套层级和性能差异

 我们选取商家发单页面来作例子比较:

image

嵌套层级

  • RelativeLayout作为布局容器:(嵌套层级最深7层) image
  • ConstraintLayout作为布局容器:(嵌套层级最深4层) image

    通过上面两张图片的对比我们可以看出使用ConstraintLayout作为布局容器时整个布局层级嵌套大大减少,最多只有四层,比使用RelativeLayout减少了三层嵌套,而且整个布局结构更加扁平。

比较UI过度绘制情况

我们打开Android手机的开发者选项-调试GPU过度绘制-打开显示过度绘制区域后,可以看到一下对比图:

  • RelativeLayout作为布局容器:(深红色-(4X+)过度绘制)

image

  • ConstraintLayout作为布局容器:(蓝色、绿色(1X和2X)过度绘制)

image

    通过比较我们可以看出ConstraintLayout减少布局层级之后,页面过度绘制的情况也大大改善(颜色越深代表过度绘制越严重。)

使用Systrace分析两个页面的UI性能

image

    可以看出使用ConstraintLayout大大减少了页面测量、布局、绘制的次数和时间。(每个F标记表示一帧,原点颜色越深,性能问题越大,我们点击标记红色的区域,可以看出给的具体的分析)

ConstraintLayout的基本属性

通过上面的对比我们可以看出ConstraintLayout在优化XML布局方面的优势,那么我们来看看ConstraintLayout有哪些常用的属性可以减少布局层级。

  • 几个最重要的属性
app:layout_constraintLeft_toLeftOf="parent"  
app:layout_constraintTop_toTopOf="parent"  
app:layout_constraintRight_toRightOf="@id/id"  
app:layout_constraintBottom_toBottomOf="@id/id"

app:layout_constraintLeft_toRightOf="@+id/tv1"  
app:layout_constraintRight_toLeftOf="@id/id"  
app:layout_constraintBottom_toTopOf="@id/id"  
app:layout_constraintTop_toBottomOf="@+id/tv2"  
layout_constraintXXX_toYYYof;
当这里XXX与YYY是一样时,表示控件自身的XXX和约束控件的YYY的一侧对齐,
例如:app:layout_constraintLeft_toLeftOf="parent"表示控件自身的左侧和父布局的左侧对齐。
XXX与YYY不相同,表示当前控件的XXX在约束控件的YYY。
例如:app:layout_constraintLeft_toRightOf="@+id/tv1"表示当前控件的左侧在控件tv1的右侧
  • layoutconstraintXXXtoYYYof可以控制控件的位置,那么控件的宽、高度怎么控制?
<?xml version="1.0" encoding="utf-8"?>  
<android.support.constraint.ConstraintLayout  
    xmlns:android="http://schemas.android.com/apk/res
/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/bt1"
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        android:text="button1"/>

    <Button
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="button2"
        app:layout_constraintLeft_toRightOf="@+id/bt3"
        app:layout_constraintRight_toRightOf="parent"/>
</android.support.constraint.ConstraintLayout>  

image

   在这里,button2在button1的左边,和父布局右边对齐,button2宽度设置为0dp时,填充了屏幕剩下的宽度,那么0值是什么含义,其实在ConstraintLayout中0代表:MATCH_CONSTRAINT,填充约束的布局。对于上面的button2,约束的布局就是左侧button1的左边到屏幕的右边,设置为0dp,就填充了这整个约束布局,就是设定了宽度是剩下的布局宽度。
   button2宽度设置为wrap-content或者固定宽度时,button2会居中于约束布局。因为button2左右两侧的约束对它的拉力值是一样的。
   这里就涉及到了一个拉力的属性了,我们对button2设置宽度为固定值,设置属性app:layout_constraintHorizontal_bias为不同值时button2会处于不同位置。如下图:

image

  • ConstraintLayout还可以设置控件的宽高比例
布局宽高比16:6
app:layout_constraintDimensionRatio="H,16:6"  
布局宽高比6:16
app:layout_constraintDimensionRatio="W,16:6"  
  • ConstraintLayout中类似LinearLayout的比重排列
    <TextView
        android:id="@+id/tab1"
        android:layout_width="0dp"
        android:layout_height="30dp"
        android:background="#fe751a"
        android:gravity="center"
        android:text="tab1"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toLeftOf="@+id/tab2"/>

    <TextView
        android:id="@+id/tab2"
        android:layout_width="0dp"
        android:layout_height="30dp"
        android:background="#fe76"
        android:gravity="center"
        android:text="tab2"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toRightOf="@+id/tab1"
       app:layout_constraintRight_toLeftOf="@+id/tab3"/>

    <TextView
        android:id="@+id/tab3"
        android:layout_width="0dp"
        android:layout_height="30dp"
        android:background="#fe79"
        android:gravity="center"
        android:text="tab3"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toRightOf="@+id/tab2"
        app:layout_constraintRight_toRightOf="parent"/>

image 其中,这三个tab都是两两相互约束,横向的约束相当于组成了一个链(Chains)。在这个链的最左侧的元素(tab1)成为链头,我们可以在其身上设置一些属性,来决定这个链的展示效果:

  • 可以设置每个tab占的比重:
    //水平方向设置的比重
    app:layout_constraintHorizontal_weight="2"
    //竖直方向设置的比重
    app:layout_constraintVertical_weight="2"
  • 设置app:layoutconstraintHorizontalchainStyle 属性,这个属性默认是spread,我们设置weight就是在这个属性值下,当我们把每个tab的宽度不是设置为0(就是MATCHCONSTRAINT),而是设置固定值或wrapcontent时,然后把app:layoutconstraintHorizontalchainStyle属性设置成不同值时会有不同的效果:
app:layout_constraintHorizontal_chainStyle属性设置成这个属性默认是spread时,效果如图:  

image

把app:layout_constraintHorizontal_chainStyle属性设置成这个属性默认是spread_inside时,效果如图:

image

把app:layout_constraintHorizontal_chainStyle属性设置成这个属性默认是spread_inside时,效果如图:

image

设置app:layout_constraintHorizontal_chainStyle=“spread_inside”时,再设置水平方向的拉力app:layout_constraintHorizontal_bias="0.2":

image 总结起来就是 image

  • 还有一个特殊的线Guideline,主要用于辅助布局,即类似为辅助线,横向的、纵向的。该布局是不会显示到界面上的。它有几个属性:
android:orientation取值为"vertical"和"horizontal".

layout_constraintGuide_begin="30dp"//表示距顶部有30dp有一条线  
layout_constraintGuide_end //表示距底部多少有一条线  
layout_constraintGuide_percent="0.6"//这个用小数表示距顶部60%的地方有一条线  
  • 其他属性: 当约束的View为Gone时。可以设置被约束的View不动。
layout_goneMarginStart  
layout_goneMarginEnd  
layout_goneMarginLeft  
layout_goneMarginTop  
layout_goneMarginRight  
layout_goneMarginBottom

在约束的布局gone时,控件自身的marginXXX会被goneMarginXXX替换掉,也就是想保持被约束的View不动,那么goneMarginXXX的值要把gone掉的View的宽度和margin还有被约束的View的margin全部加上。

总结

以上就是ConstraintLayout的一些基本属性,我们把它用到XML布局中去,可以很好的减少布局的层级,从而优化XML布局的性能。但是并不是所有页面都适合用这个,因为ConstraintLayout的属性决定它的测量是很耗时的,所以对于单层级的布局来说,还是Linerelayout是首选。对于嵌套层级过多的布局来说,ConstraintLayout是很好的选择。