Android 性能优化之布局优化

2,531 阅读14分钟

1. 为什么进行布局优化?

设计者对 UI 的效果要求越来越高,导致的直接结果是界面复杂度越来越高。界面越复杂,加载速度越慢,界面加载越慢,用户体验越差,用户体验越差,App 被卸载的概率就越大。因此,布局布局优化势在必行。

2. Android 设备 UI 刷新机制

2.1 Android 设备 UI 刷新机制

Android 设备 UI 刷新主要涉及三个模块,分别是:

  • CPU;
  • GPU;
  • DISPLAY(硬件);

CPU 用于计算数据,GPU 用于将图像数据进行栅格化处理并把处理好的数据存入 BUFFER,DISPLAY 从 BUFFER 中取数据并将图像数据展示出来:

但这是理想状态,实际情况是:
DISPLAY 的刷新率和 GPU 的帧率并不能总保持一致,也就是说,GPU 存数据的时间和 DISPLAY 取数据的时间是不一致的。针对此,Android 引入了 Vsync(垂直同步)。

Vsync 主要是为了让 DISPLAY 的刷新率和 GPU 的帧率保持一致,只有收到 Vsync 信号的时候,CPU 才开始计算数据、GPU 才开始处理数据、DISPLAY 才开始展示数据。

2.2 多长时间刷新一次 UI 比较合适?

人眼与大脑之间的协作无法感知超过 60 fps(Frame per Second)的画面更新,也就是当画面的更新频率超过 60HZ 之后,人眼已经看不出它和更新频率为 60HZ 的界面之间的区别了。

1s = 1000ms
1000ms / 60 = 16.7 ms

因此,每 16.7 ms 刷新一次界面,用户就会感觉很流畅。也正是因此,View 的 measure、layout、draw 必须在 16ms 内完成,否则用户将感觉到卡顿。

2.3 相关问题

2.3.1 什么是刷新率?

一秒内刷新屏幕的次数,如 60 HZ,多数情况下,屏幕刷新率越高越好。不过,如果画面的内容还未生成,刷新再快也没有什么卵用,不是吗?

2.3.2 什么是帧率?

GPU 在一秒内操作界面的帧数,如 30 fps,60 fps。

3. 如何排查布局性能问题?

我用过的排查布局性能的工具有三种:

  1. LayoutInspector;
  2. 手机自带「过度绘制检测工具」;
  3. systrace & traceview;

3.1 LayoutInspector

Android Studio 自带的用于检测布局文件嵌套层级的工具,可以在开发阶段方便的检测布局文件的嵌套层级。

打开的方法:

Tools → Layout Inspector → 选择要测试的 App  

在左边「View Tree」栏可以查看当前界面的嵌套层级,「View Tree」右边对应的就是当前界面的显示的效果,当点击「View Tree」中不同的控件时,「View Tree」右边界面中的相应控件会被蓝色框框起来。

3.2 手机自带「过度绘制检测工具」

打开手机自带「过度绘制检测工具」方法:

设置 → 开发者选项 → 显示过度绘制区域

打开「显示过度绘制区域」之后,界面就会根据当前像素被绘制了几次显示不同的颜色:

各种颜色代表的意思:

颜色 过度重绘次数
蓝色 1
绿色 2
粉色 3
红色 4+

举个例子,创建一个下面这样的「个人中心」,这是经手机自带的「过度绘制检测工具」检测的结果:

3.3 systrace & traceview

systrace 搭配 traceview 定义布局性能问题,通过 systrace 定位大方向,通过 traceview 解析具体细节。

具体步骤:

3.3.1 systrace 定位大方向

  1. 启动 App,并到目标测试界面或者目标测试界面前一界面;
  2. 在「终端」执行「python systrace.py --time=10 -o xxx.html」;
  3. 进入目标界面,操作目标界面;
  4. 在 Chrome 中查看「file:///Users/xxx/Library/Android/sdk/platform-tools/systrace/xxx.html」中的 xxx.html 文件,解析 xxx.html 文件;

3.3.2 traceview 解析具体细节

  1. 在目标测试界面加入追踪代码:Debug.startMethodTracing(Constants.TRACE)(开始追踪)、Debug.stopMethodTracing()(停止追踪);
  2. 在 Android Studio 中查看「sdcard/Android/data/package name/files/xxx.trace」中的 xxx.trace 文件,解析 xxx.trace 文件;
  3. 在 「TopDown」 栏,直接查看每个方法的调用时长,进而找到耗时最多的方法,并进行相应的优化;

4. 布局优化策略

  1. 根据实际情况,选用性能高的布局文件;
  2. 减少布局嵌套层级(打造「宽而浅」,远离「窄而深」);
  3. 提高布局复用性;
  4. 减少测量、布局、绘制时间;
  5. 减少控件的使用(善用控件属性);

4.1 根据实际情况,选用性能高的布局文件

在 Android 中,并不是所有的 ViewGroup 性能都一样,如果非要排个序的话,那大概会是下面这个样子:

ConstraintLayout > FrameLayout > LinearLayout > RelativeLayout  

上面的排序是针对「四种容器」均能实现的效果而说的,也就是说,假设有某一场景,以上四种布局均可实现,ConstraintLayout 性能是最好的。

ConstraintLayout 能在不经任何嵌套的情况下,创建复杂的布局,因此,在今后的开发中应多使用它。另外,当 LinearLayout 和 RelativeLayout 均能在无需嵌套的情况下实现某效果时,用 LinearLayout,因为 RelativeLayout 测量时需要测量两次(当 LinearLayout 的子 View 有 layout_weight 属性时,也需要测两次)。

4.2 减少布局嵌套层级

4.2.1 合理选择布局类型

在实现某个布局时,如果有可能不用任何嵌套层级,那就不要用任何嵌套层级实现该界面。

举个例子,有一个登录界面,分别用两种方式实现:

  1. 嵌套的 LinearLayout;
  2. RelativeLayout;
//1. 第一种实现方式:嵌套的 LinearLayout  
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center_horizontal"
    android:orientation="vertical"
    android:paddingLeft="@dimen/item_height"
    android:paddingTop="@dimen/padding_medium"
    android:paddingRight="@dimen/item_height"
    android:paddingBottom="@dimen/padding_medium">


    <de.hdodenhof.circleimageview.CircleImageView
        android:id="@+id/avatar"
        android:layout_width="@dimen/padding_ninety_six"
        android:layout_height="@dimen/padding_ninety_six"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="@dimen/padding_ninety_six"
        android:scaleType="centerCrop"
        android:src="@drawable/bird_woodpecker"
        app:civ_border_color="@color/grey_800"
        app:civ_border_width="@dimen/padding_micro_x" />

    <LinearLayout
        android:id="@+id/login_username_container"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/padding_large"
        android:orientation="horizontal">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/login_username" />

        <EditText
            android:id="@+id/login_username"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="@dimen/padding_medium"
            android:background="@null"
            android:hint="@string/login_username_hint"
            android:inputType="text"
            android:maxLines="1"
            android:singleLine="true"
            android:textColor="@color/grey_700"
            android:textCursorDrawable="@drawable/common_edit_text_cursor"
            android:textSize="@dimen/font_micro" />
    </LinearLayout>


    <View
        android:layout_width="match_parent"
        android:layout_height="@dimen/padding_micro_xx"
        android:layout_marginTop="@dimen/padding_small"
        android:layout_marginBottom="@dimen/padding_small"
        android:background="@color/grey_300" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/padding_medium"
        android:orientation="horizontal">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/login_password" />

        <EditText
            android:id="@+id/login_password"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="@dimen/padding_medium"
            android:background="@null"
            android:hint="@string/login_password_hint"
            android:inputType="textPassword"
            android:maxLines="1"
            android:singleLine="true"
            android:textColor="@color/grey_700"
            android:textCursorDrawable="@drawable/common_edit_text_cursor"
            android:textSize="@dimen/font_micro" />
    </LinearLayout>

    <View
        android:layout_width="match_parent"
        android:layout_height="@dimen/padding_micro_xx"
        android:layout_marginTop="@dimen/padding_small"
        android:layout_marginBottom="@dimen/padding_small"
        android:background="@color/grey_300" />

    <TextView
        android:id="@+id/login"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="@dimen/padding_medium"
        android:layout_marginTop="@dimen/padding_large"
        android:layout_marginRight="@dimen/padding_medium"
        android:background="@drawable/ripple_login"
        android:clickable="true"
        android:elevation="@dimen/divider_height"
        android:gravity="center"
        android:paddingTop="@dimen/padding_medium"
        android:paddingBottom="@dimen/padding_medium"
        android:text="@string/login"
        android:textColor="@color/grey_700"
        android:textSize="@dimen/font_small"
        android:textStyle="bold" />

</LinearLayout>

通过 LayoutInspector 检测该界面的嵌套层级:

//2. 第二种实现方式:RelativeLayout  
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingLeft="@dimen/item_height"
    android:paddingTop="@dimen/padding_medium"
    android:paddingRight="@dimen/item_height"
    android:paddingBottom="@dimen/padding_medium">


    <de.hdodenhof.circleimageview.CircleImageView
        android:id="@+id/avatar"
        android:layout_width="@dimen/padding_ninety_six"
        android:layout_height="@dimen/padding_ninety_six"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="@dimen/padding_ninety_six"
        android:scaleType="centerCrop"
        android:src="@drawable/bird_woodpecker"
        app:civ_border_color="@color/grey_800"
        app:civ_border_width="@dimen/padding_micro_x" />

    <TextView
        android:id="@+id/login_username_label"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/avatar"
        android:layout_marginTop="@dimen/padding_large"
        android:text="@string/login_username" />

    <EditText
        android:id="@+id/login_username"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/avatar"
        android:layout_marginLeft="@dimen/padding_medium"
        android:layout_marginTop="@dimen/padding_large"
        android:layout_toRightOf="@id/login_username_label"
        android:background="@null"
        android:hint="@string/login_username_hint"
        android:inputType="text"
        android:maxLines="1"
        android:singleLine="true"
        android:textColor="@color/grey_700"
        android:textCursorDrawable="@drawable/common_edit_text_cursor"
        android:textSize="@dimen/font_micro" />


    <View
        android:id="@+id/login_username_divider"
        android:layout_width="match_parent"
        android:layout_height="@dimen/padding_micro_xx"
        android:layout_below="@id/login_username_label"
        android:layout_marginTop="@dimen/padding_small"
        android:layout_marginBottom="@dimen/padding_small"
        android:background="@color/grey_300" />

    <TextView
        android:id="@+id/login_password_label"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/login_username_divider"
        android:layout_marginTop="@dimen/padding_medium"
        android:text="@string/login_password" />

    <EditText
        android:id="@+id/login_password"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/login_username_divider"
        android:layout_marginLeft="@dimen/padding_medium"
        android:layout_marginTop="@dimen/padding_medium"
        android:layout_toRightOf="@id/login_password_label"
        android:background="@null"
        android:hint="@string/login_password_hint"
        android:inputType="textPassword"
        android:maxLines="1"
        android:singleLine="true"
        android:textColor="@color/grey_700"
        android:textCursorDrawable="@drawable/common_edit_text_cursor"
        android:textSize="@dimen/font_micro" />

    <View
        android:id="@+id/login_password_divider"
        android:layout_width="match_parent"
        android:layout_height="@dimen/padding_micro_xx"
        android:layout_below="@id/login_password_label"
        android:layout_marginTop="@dimen/padding_small"
        android:layout_marginBottom="@dimen/padding_small"
        android:background="@color/grey_300" />

    <TextView
        android:id="@+id/login"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/login_password_divider"
        android:layout_marginLeft="@dimen/padding_medium"
        android:layout_marginTop="@dimen/padding_large"
        android:layout_marginRight="@dimen/padding_medium"
        android:background="@drawable/ripple_login"
        android:clickable="true"
        android:elevation="@dimen/divider_height"
        android:gravity="center"
        android:paddingTop="@dimen/padding_medium"
        android:paddingBottom="@dimen/padding_medium"
        android:text="@string/login"
        android:textColor="@color/grey_700"
        android:textSize="@dimen/font_small"
        android:textStyle="bold" />

</RelativeLayout>

通过 LayoutInspector 检测该界面的嵌套层级:

4.2.2 使用 标签

在使用 标签引入其他布局文件时,不会产生多余的嵌套层级。因为布局层级少,所以绘制的工作量小,因此绘制速度更快、性能更高。

需要注意的是,最终 标签还是要靠 或 标签来引用才能真正的「被使用」,另外, 标签本身是没有任何属性可以配置的, 标签用的是父 View 的属性。

举个例子:

//1. <merge> 标签  
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white"
        android:paddingLeft="@dimen/padding_medium"
        android:paddingTop="@dimen/padding_medium"
        android:paddingBottom="@dimen/padding_medium"
        android:text="@string/clear_cache"
        android:textSize="@dimen/font_micro" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white"
        android:paddingLeft="@dimen/padding_medium"
        android:paddingTop="@dimen/padding_medium"
        android:paddingBottom="@dimen/padding_medium"
        android:text="@string/version_update"
        android:textSize="@dimen/font_micro" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white"
        android:paddingLeft="@dimen/padding_medium"
        android:paddingTop="@dimen/padding_medium"
        android:paddingBottom="@dimen/padding_medium"
        android:text="@string/about"
        android:textSize="@dimen/font_micro" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white"
        android:paddingLeft="@dimen/padding_medium"
        android:paddingTop="@dimen/padding_medium"
        android:paddingBottom="@dimen/padding_medium"
        android:text="@string/logout"
        android:textSize="@dimen/font_micro" />
</merge>

//2. 引用 <merge> 标签  
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/merge_parent"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:divider="@drawable/divider_line"
    android:dividerPadding="@dimen/padding_medium"
    android:orientation="vertical"
    android:showDividers="middle">

    <LinearLayout
        android:id="@+id/user_info"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/item_height"
        android:layout_marginBottom="@dimen/item_height"
        android:gravity="center_horizontal"
        android:orientation="vertical">

        <de.hdodenhof.circleimageview.CircleImageView
            android:id="@+id/user_avatar"
            android:layout_width="@dimen/padding_ninety_six"
            android:layout_height="@dimen/padding_ninety_six"
            android:scaleType="centerCrop"
            android:src="@drawable/bird_woodpecker"
            app:civ_border_color="@color/grey_800"
            app:civ_border_width="@dimen/padding_micro_x" />

        <TextView
            android:id="@+id/user_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="@dimen/padding_medium"
            android:text="@string/test_list_item6"
            android:textColor="@color/grey_700"
            android:textSize="@dimen/font_large" />
    </LinearLayout>


    <include
        android:id="@+id/marge_include"
        layout="@layout/activity_main_merge_child"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</LinearLayout>

标签中的内容:

引用 标签:

通过 LayoutInspector 检测该界面的嵌套层级:

由检测结果可知:

标签引入其他布局文件时,确实不会产生多余的嵌套层级。

4.3 提高布局复用性

在 Android 开发中,不仅 Java 代码可以被复用,XML 布局文件的内容也可以被复用。在 Android 中,布局文件的复用,要通过 标签。

很多 App 中,用的都是自定义的 Title,像这种在多个地方都使用且内容相同的构件,完全没必要每次使用的时候都重新写一个,而是可以只写一个,在多个地方使用。

举个例子,创建一个通用的 Title 布局,通过 标签使用:

//1. Title Layout  
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/title_container"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@color/grey_700"
    android:orientation="horizontal">

    <ImageView
        android:id="@+id/back"
        android:layout_width="@dimen/item_height"
        android:layout_height="@dimen/item_height"
        android:padding="@dimen/padding_medium_x"
        android:src="@drawable/icon_back" />

    <TextView
        android:id="@+id/title"
        android:layout_width="match_parent"
        android:layout_height="@dimen/item_height"
        android:layout_toLeftOf="@id/share"
        android:layout_toRightOf="@id/back"
        android:gravity="center"
        android:text="@string/test_title"
        android:textColor="@color/white"
        android:textSize="@dimen/font_medium" />

    <ImageView
        android:id="@+id/share"
        android:layout_width="@dimen/item_height"
        android:layout_height="@dimen/item_height"
        android:layout_alignParentRight="true"
        android:padding="@dimen/padding_medium_x"
        android:src="@drawable/icon_share" />
</RelativeLayout>

//2. 在 SecondActivity 中通过 <include> 标签引用 Title 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="wrap_content"
    android:background="@color/grey_700"
    android:orientation="horizontal">

    <include
        layout="@layout/layout_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</LinearLayout>

标签中的内容:

引用 标签:

通过 LayoutInspector 检测该界面的嵌套层级:

由检测结果可知:

标签引入其他布局文件时,会产生多余的嵌套层级。

需要注意的是,当在 标签中重新定义 标签所引用的布局文件属性时,将覆盖掉原布局文件中定义的属性。例如,原布局文件的 layout_width 和 layout_height 为 wrap_content,而 标签中定义的 layout_width 和 layout_height 为 match_parent,那最终显示的效果将是 match_parent。

使用 标签的好处除了复用 XML 布局文件之外,还有一个好处——将复杂的布局模块化,提高复用率,降低学习成本。

4.4 减少测量、布局、绘制时间

在 Android 中,默认情况下,所有的 View 都是需要测量(measure())、布局(layout())和绘制(draw())的,无论该 View 是否可见。

但有这样一种标签布局,只有当它可见的时候才会占用资源,它就是 ViewStub。因此,我们可以借助它的这个特性对布局进行一些优化。

举个例子,在一个界面中请求数据时,当请求失败时展示一个错误提示界面,当请求成功时,展示请求到的信息:

//1. 错误提示界面  
<?xml version="1.0" encoding="utf-8"?>
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:src="@drawable/icon_error" />
    
//2. 引用「错误提示界面」的界面
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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:id="@+id/user_info_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".third.ThirdActivity">

    <ViewStub
        android:id="@+id/error_placeholder"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout="@layout/layout_error" />

    <include
        android:id="@+id/user_info"
        layout="@layout/activity_main_merge_parent"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</FrameLayout>

//3. 在 Activity 中做相应的处理  
public class ThirdActivity extends AppCompatActivity implements View.OnClickListener {

    private boolean isInvalid = true;
    private FrameLayout mContainer;
    private LinearLayout mUserInfo;
    private ViewStub mErrorPlaceHolder;
    private View mErrorView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_third);
        initView();
    }

    private void initView(){
        mContainer = findViewById(R.id.user_info_container);
        mContainer.setOnClickListener(this);
        mUserInfo = findViewById(R.id.user_info);
        mErrorPlaceHolder = findViewById(R.id.error_placeholder);
    }

    @Override
    public void onClick(View view) {
        isInvalid = !isInvalid;
        if(isInvalid){
            mUserInfo.setVisibility(View.VISIBLE);
            mContainer.setBackgroundColor(getResources().getColor(R.color.white));
            if(mErrorView != null){
                mErrorView.setVisibility(View.INVISIBLE);
            }
            Toast.makeText(this, getString(R.string.test_request_succeed), Toast.LENGTH_SHORT).show();
        }else{
            mUserInfo.setVisibility(View.INVISIBLE);
            mContainer.setBackgroundColor(getResources().getColor(R.color.cyan_200));
            if(mErrorView == null){
                mErrorView = mErrorPlaceHolder.inflate();
                mErrorView.setVisibility(View.VISIBLE);
            }else{
                mErrorView.setVisibility(View.VISIBLE);
            }
            Toast.makeText(this, getString(R.string.test_request_failed), Toast.LENGTH_SHORT).show();
        }
    }

}

需要注意的是,默认情况下,ViewStub 是不可见的,也是不占用资源的,只有 inflate() 之后,ViewStub 才会占用资源,与此同时,ViewStub 本身也会被它指向的布局文件(ViewStub 中 layout 属性对应的布局文件)所替代。

ViewStub inflate() 之前,通过 LayoutInspector 检测该界面的嵌套层级:

由检测结果可知,此时 ViewStub 在 View Tree 中是虚线,也就表示它此时不占用资源。

ViewStub inflate() 之后,通过 LayoutInspector 检测该界面的嵌套层级:

由检测结果可知,此时 ViewStub 已经被它所指向的布局文件所替代。

4.5 减少控件的使用(善用控件属性)

  1. LinearLayout 分割线;
  2. TextView 同时显示文字和图片;

4.5.1 LinearLayout 分割线

//1. 定义分割线  
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">

    <size
        android:width="@dimen/padding_micro_xx"
        android:height="@dimen/padding_micro_xx" />

    <solid android:color="@color/grey_200" />

</shape>

//2. 在 LinearLayout 中应用分割线  
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:divider="@drawable/divider_line"
    android:dividerPadding="@dimen/padding_medium"
    android:orientation="vertical"
    android:showDividers="middle"
    tools:context=".four.FourActivity">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white"
        android:paddingLeft="@dimen/padding_medium"
        android:paddingTop="@dimen/padding_medium"
        android:paddingBottom="@dimen/padding_medium"
        android:text="@string/clear_cache"
        android:textSize="@dimen/font_micro" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white"
        android:paddingLeft="@dimen/padding_medium"
        android:paddingTop="@dimen/padding_medium"
        android:paddingBottom="@dimen/padding_medium"
        android:text="@string/version_update"
        android:textSize="@dimen/font_micro" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white"
        android:paddingLeft="@dimen/padding_medium"
        android:paddingTop="@dimen/padding_medium"
        android:paddingBottom="@dimen/padding_medium"
        android:text="@string/about"
        android:textSize="@dimen/font_micro" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white"
        android:paddingLeft="@dimen/padding_medium"
        android:paddingTop="@dimen/padding_medium"
        android:paddingBottom="@dimen/padding_medium"
        android:text="@string/logout"
        android:textSize="@dimen/font_micro" />

</LinearLayout>

最终效果:

4.5.2 TextView 同时显示文字和图片

//1. TextView 同时显示文字和图片
<?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:divider="@drawable/divider_line"
    android:dividerPadding="@dimen/padding_medium"
    android:orientation="vertical"
    android:showDividers="middle">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:drawableLeft="@drawable/icon_animal_01"
        android:drawableRight="@drawable/icon_forward"
        android:drawablePadding="@dimen/padding_medium"
        android:gravity="center_vertical"
        android:paddingLeft="@dimen/padding_medium"
        android:paddingTop="@dimen/padding_medium"
        android:paddingRight="@dimen/padding_medium"
        android:paddingBottom="@dimen/padding_medium"
        android:text="@string/spending_traffic"
        android:textSize="@dimen/font_micro" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:drawableLeft="@drawable/icon_animal_02"
        android:drawableRight="@drawable/icon_forward"
        android:drawablePadding="@dimen/padding_medium"
        android:gravity="center_vertical"
        android:paddingLeft="@dimen/padding_medium"
        android:paddingTop="@dimen/padding_medium"
        android:paddingRight="@dimen/padding_medium"
        android:paddingBottom="@dimen/padding_medium"
        android:text="@string/spending_clothes"
        android:textSize="@dimen/font_micro" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:drawableLeft="@drawable/icon_animal_03"
        android:drawableRight="@drawable/icon_forward"
        android:drawablePadding="@dimen/padding_medium"
        android:gravity="center_vertical"
        android:paddingLeft="@dimen/padding_medium"
        android:paddingTop="@dimen/padding_medium"
        android:paddingRight="@dimen/padding_medium"
        android:paddingBottom="@dimen/padding_medium"
        android:text="@string/spending_diet"
        android:textSize="@dimen/font_micro" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:drawableLeft="@drawable/icon_animal_04"
        android:drawableRight="@drawable/icon_forward"
        android:drawablePadding="@dimen/padding_medium"
        android:gravity="center_vertical"
        android:paddingLeft="@dimen/padding_medium"
        android:paddingTop="@dimen/padding_medium"
        android:paddingRight="@dimen/padding_medium"
        android:paddingBottom="@dimen/padding_medium"
        android:text="@string/spending_communication"
        android:textSize="@dimen/font_micro" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:drawableLeft="@drawable/icon_animal_05"
        android:drawableRight="@drawable/icon_forward"
        android:drawablePadding="@dimen/padding_medium"
        android:gravity="center_vertical"
        android:paddingLeft="@dimen/padding_medium"
        android:paddingTop="@dimen/padding_medium"
        android:paddingRight="@dimen/padding_medium"
        android:paddingBottom="@dimen/padding_medium"
        android:text="@string/spending_live"
        android:textSize="@dimen/font_micro" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:drawableLeft="@drawable/icon_animal_06"
        android:drawableRight="@drawable/icon_forward"
        android:drawablePadding="@dimen/padding_medium"
        android:gravity="center_vertical"
        android:paddingLeft="@dimen/padding_medium"
        android:paddingTop="@dimen/padding_medium"
        android:paddingRight="@dimen/padding_medium"
        android:paddingBottom="@dimen/padding_medium"
        android:text="@string/spending_game"
        android:textSize="@dimen/font_micro" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:drawableLeft="@drawable/icon_animal_07"
        android:drawableRight="@drawable/icon_forward"
        android:drawablePadding="@dimen/padding_medium"
        android:gravity="center_vertical"
        android:paddingLeft="@dimen/padding_medium"
        android:paddingTop="@dimen/padding_medium"
        android:paddingRight="@dimen/padding_medium"
        android:paddingBottom="@dimen/padding_medium"
        android:text="@string/spending_others"
        android:textSize="@dimen/font_micro" />

</LinearLayout>

最终效果:

4.6 其他布局优化方法

  1. 移除不必要的 background,避免 OverDraw;
  2. 在自定义 View 中巧用 Canvas.clipRect() 避免重绘;
  3. ListView
    • 复用 ConvertView;
    • 使用 ViewHolder,减少 findViewById() 次数;
    • 分批加载;
  4. WebView
    • 定义全局的 WebView,以减少 WebView 首次打开时间;
    • 先加载文本,再加载图片,以优化网页加载速度;
  5. ViewPager 懒加载

5. 总结

正如启动优化一样,布局优化也势在必行,因为,复杂的布局会影响其加载速度,而布局加载速度越慢,用户体验越差,用户体验越差,App 被卸载的概率就越大,而这不是开发者想看到的,也不是老板想看到的。

布局优化的总目标就一个——快速加载布局,布局优化的策略有:

  1. 根据实际情况,选用性能高的布局文件;
  2. 减少布局嵌套层级(打造「宽而浅」,远离「窄而深」);
  3. 提高布局复用性;
  4. 减少测量、布局、绘制时间;
  5. 减少控件的使用(善用控件属性);

我相信,在使用了上面这些策略之后,你开发的 App 的性能肯定会有一定的提升,赶紧去试试吧~


参考文档

  1. Android性能优化:布局优化
  2. Android 性能优化(二)之布局优化面面观
  3. Android布局优化技巧
  4. Android UI性能优化实战 识别绘制中的性能问题