Android 性能优化之布局优化

1. 为何进行布局优化?

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

2. Android 设备 UI 刷新机制

2.1 Android 设备 UI 刷新机制

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

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

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

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

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

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

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

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

所以,每 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性能优化实战 识别绘制中的性能问题
相关文章
相关标签/搜索