性能优化技巧知识梳理(1) 布局优化

1、前言

性能优化包含的部分不少,包括布局、内存、耗电、流量等等,其中布局优化是最容易掌握,也最容易被你们所忽视的一个方面,今天,就来介绍一下有关布局优化的一些技巧。android

2、布局优化技巧

(1) 使用 标签进行布局复用

当咱们的布局中有多个相同的布局时,可使用include标签来进行布局的复用,这样,当视觉须要修改单个Item的间距,文字大小时,只须要修改一个布局就能够了,例如像下面这种状况,咱们就可使用include标签来实现: canvas

根布局为:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <include android:id="@+id/include_1" layout="@layout/layout_is_merge" android:layout_width="wrap_content" android:layout_height="wrap_content"/>
    <include android:id="@+id/include_2" layout="@layout/layout_is_merge" android:layout_width="wrap_content" android:layout_height="wrap_content"/>
    <include android:id="@+id/include_3" layout="@layout/layout_is_merge" android:layout_width="wrap_content" android:layout_height="wrap_content"/>
</LinearLayout>
复制代码

单个Item的布局为:性能优化

<?xml version="1.0" encoding="utf-8"?>
<merge
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <TextView
            android:id="@+id/tv_content_1"
            android:text="tv_content_1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
        <TextView
            android:id="@+id/tv_content_2"
            android:text="tv_content_2"
            android:layout_marginLeft="40dp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
    </LinearLayout>
</merge>
复制代码

要点bash

  • 直接在根布局中,若是但愿找到<include>所指定的layout中包含的控件,那么就须要给<include>指定id,再经过它来寻找子容器中的控件。
  • <include>标签中,能够指定layout_xxx属性,它将会覆盖子布局中的根标签中的属性。

(2) 使用 标签减小布局层级

当出现下面这种状况:一个xml布局文件的根节点是一个FrameLayout,而且它没有一个有用的背景,那么当该xml布局文件渲染出的ViewGroup被添加到父布局中时,链接处就会出现一个多余的节点,而采用<merge>标签能够去掉这一无用节点,从而下降布局的层级。异步

例如,在上面的例子当中,咱们使用了<merge>标签的情形为: async

假如咱们没有使用 <merge>标签,那么情形为:
要点

  • 当须要经过LayoutInflaterinflate方法渲染出以<merge>做为根节点标签的xml文件时,必须传入不为nullroot参数,且attachToRoot参数必须为true
  • <merge>只可做为xml的根节点。
  • <merge>既不是View也不是ViewGroup,它只是表示一组等待被添加的视图,所以,对它设定的任何属性都是无用的。

(3) 使用 ViewStub 标签动态加载布局

当咱们的布局中,存在一些须要按序加载的控件,那么就可使用ViewStub标签预先声明,当状况知足时再去实例化ViewStub中所声明的布局,其用法以下:ide

  • 首先,在布局中预先声明ViewStub,而且经过layout标签指定对应的布局layout_stub
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ViewStub
        android:id="@+id/view_stub"
        android:inflatedId="@+id/view_inflated"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout="@layout/layout_stub"/>
</LinearLayout>
复制代码
  • 当须要加载以上指定的布局时,那么首先经过得到ViewStub,再调用它的inflate或者setVisibility(View.VISIBLE)方法,其返回的布局就是layout=所指定的布局的根节点:
private void inflateIfNeed() {
        //1.获取到布局中的ViewStub。
        mViewStub = (ViewStub) findViewById(R.id.view_stub);
        //2.调用其inflate方法实例化它所指定的layout。
        mStubView = mViewStub.inflate();
    }
复制代码

要点工具

  • 任何ViewStub只能调用一次inflate或者setVisibility(View.VISIBLE)方法,而且调用完以后它将再也不可用,ViewStub原先所在位置将被替换成为layout参数所指定的布局的根节点,而且其根节点的id值将变成android:inflatedId所指定的值:

(4) 选择合适的父容器以减小布局层级和测量次数

当咱们须要经过父容器来容纳多个子控件时,如何选择父容器,将会影响到布局的效率,而对于父容器的选择,有如下几点原则:布局

  • 首先应当考虑布局层级最小的方案。
  • 布局层级相同时,就应当选取合适的父容器,通常来讲,有如下几点经验:
  • 选取的优先级为:FrameLayout、不带layout_weight参数的LinearLayoutRelativeLayout,这里选取的标准为带有layout_weightLinearLayout或者RelativeLayout会测量两次。
  • 当使用LinearLayout时,应当尽可能避免使用layout_weight参数。
  • 避免使用RelativeLayout嵌套RelativeLayout
  • 若是容许,那么可使用Google新推出的ConstraintLayout布局。

(5) 使用 SpannableStringBuilder 替换多个 TextView 的实现

当咱们存在多种不一样大小、颜色或者图文混排须要显示时,咱们每每会利用多个TextView来进行组合,可是某些效果经过一个TextView就能够实现,通常来讲,利用SpannableStringBuilder能够经过单个TextView实现多种不一样的布局,更多Span的用法能够参考这篇文章:Android 中各类 Span 的用法,下面以不一样大小的TextView为例:性能

private void useSpan() {
        TextView textView = (TextView) findViewById(R.id.tv_span);
        SpannableStringBuilder ssb = new SpannableStringBuilder("300 RMB");
        //设置文字大小。
        ssb.setSpan(new RelativeSizeSpan(6.0f), 0, 3, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
        //设置文字颜色。
        ssb.setSpan(new ForegroundColorSpan(0xff303F9F), 0, 3, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
        textView.setText(ssb);
    }
复制代码

最终能够实现以下的效果:

除此以外,还能够实现图文混排,例以下面这样:

(6) 使用 LinearLayout 自带的分割线,而不是在布局中手动添加一个 ImageView

例以下面的布局:

此时咱们就可使用 LinearLayout自带的 divider属性来实现分割线:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:showDividers="beginning|end|middle"
    android:divider="@android:drawable/divider_horizontal_bright"
    android:dividerPadding="5dp"
    android:paddingTop="20dp">
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Line 1"/>
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Line 2"/>
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Line 3"/>
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Line 4"/>
</LinearLayout>
复制代码

与分割线相关的属性包括如下几个:

  • divider:传入分割线的drawable,能够是一个图片,也能够是本身经过xml实现的drawable
  • showDividers:分割线显示的位置,beginning/middle/end,分割对应头部、中间、尾部。
  • dividerPadding:分割线距离两边的间距。

(7) 使用 Space 控件进行合理的占位

Space控件位于android.support.v4.widget包中,与通常控件不一样,它的draw方法是一个空实现,所以它只占位置,而不去渲染,使用它来进行占位填充比其它控件更加高效,例以下面,咱们须要将一行均等地分红五份,有颜色部分位于2,4当中:

这时候,就能够经过 Space控件,加上 layout_weight属性来实现:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <android.support.v4.widget.Space
        android:layout_width="0dp"
        android:layout_height="50dp"
        android:layout_weight="1"/>
    <View
        android:layout_width="0dp"
        android:layout_height="50dp"
        android:layout_weight="1"
        android:background="@color/colorAccent"/>
    <android.support.v4.widget.Space
        android:layout_width="0dp"
        android:layout_height="50dp"
        android:layout_weight="1"/>
    <View
        android:layout_width="0dp"
        android:layout_height="50dp"
        android:layout_weight="1"
        android:background="@color/colorAccent"/>
    <android.support.v4.widget.Space
        android:layout_width="0dp"
        android:layout_height="50dp"
        android:layout_weight="1"/>
</LinearLayout>
复制代码

(8) 使用 TextView 的 drawableLeft/drawableTop 属性来替代 ImageView + TextView 的布局

当出现图片在文案的四周时,咱们应当首先考虑可以经过单个TextView来实现,而不是经过LinearLayout包裹TextView+ImageView的方式来实现,例以下面的效果:

其布局以下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <!-- 方式一:使用 ImageView + TextView -->
    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center_vertical">
        <ImageView
            android:src="@android:drawable/ic_btn_speak_now"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
        <TextView
            android:text="ImageView + TextView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>
    </LinearLayout>
    <!-- 方式二:使用单个 TextView -->
    <TextView
        android:drawableLeft="@android:drawable/ic_btn_speak_now"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center_vertical"
        android:text="单个 TextView"/>
</LinearLayout>
复制代码

能够看到,虽然都是实现了图片加上文字的显示效果,可是第二种经过单个TextView来实现其布局层级更少,而且控件的个数更少,所以效率更高,而且图片不只能够显示在左边,还能够显示在TextView的四周,图片和TextView之间的间隔能够经过drawablePadding来实现。

(9) 去掉没必要要的背景

  • 在布局层级中避免重叠部分的背景

当两个控件在布局上有重叠的部分,可是它们具备背景时,就会出现过分绘制的状况,形成无用的性能损耗。而且肉眼没法发现,须要经过设置当中的”调试GPU过分绘制"选项进行检查,详细使用以下:性能优化工具知识梳理(3) - 调试GPU过分绘制 & GPU呈现模式分析。例以下面布局当中,根布局和子控件有100dp部分重叠,而且它们都有背景:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#FFFFFF">
    <LinearLayout
        android:background="#FFFFFF"
        android:layout_width="match_parent"
        android:layout_height="100dp"/>
</LinearLayout>
复制代码

那么最终,打开过分绘制检测时,就会出现下面的效果:

  • 去掉无用的WindowBackgroud 当咱们使用某些主题时,系统有可能在DecorView中给咱们加上一个背景,可是有时候它是无用的,例如上面的例子中,咱们根布局为紫色,这其实就是因为默认主题中的背景所致使的,咱们能够经过下面的方式去除掉该背景。
@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_overdraw);
        getWindow().setBackgroundDrawable(null);
    }
复制代码

此时的检测结果以下,能够看到,根布局就不存在过分绘制的状况了:

(10) 优化自定义控件中的 onDraw 方法

当咱们在自定义控件,并重写onDraw方法来完成相应的需求时,一些错误的操做每每会致使布局效率的下降,通常来讲,有两点须要注意:

  • 避免在其中进行对象的分配
  • 使用CanvasClipRect方法避免过分绘制

这里用一个简单的例子来讲明一下第二点的实现,当咱们须要实现下面这个多张图片重叠的自定义控件时:

假如咱们直接使用下面的方式,也能够实现上面的效果:

public class ClipRectView extends View {

    private static final int[] ID = new int[]{R.drawable.pic_1, R.drawable.pic_2, R.drawable.pic_3};
    private Bitmap[] mBitmaps;

    public ClipRectView(Context context) {
        super(context);
        prepareBitmap();
    }

    public ClipRectView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        prepareBitmap();
    }

    public ClipRectView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        prepareBitmap();
    }

    private void prepareBitmap() {
        mBitmaps = new Bitmap[ID.length];
        int i = 0;
        for (int id : ID) {
            mBitmaps[i++] = BitmapFactory.decodeResource(getResources(), id);
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        for (Bitmap bitmap : mBitmaps) {
            canvas.drawBitmap(bitmap, 0, 0, null);
            canvas.translate(bitmap.getWidth() / 2, 0);
        }
    }
}
复制代码

可是,若是咱们打开调试GPU过分绘制的开关,那么能够获得下面的检测结果,能够发如今两张图片重叠的地方,会出现明显的过分绘制:

而若是,咱们采用 ClipRectonDraw方法进行优化:

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.save();
        int bits = mBitmaps.length;
        for (int i = 0; i < bits; i++) {
            Bitmap bitmap = mBitmaps[i];
            int bitW = bitmap.getWidth();
            int bitH = bitmap.getHeight();
            if (i != 0) {
                canvas.translate(bitW / 2, 0);
            }
            canvas.save();
            if (i != bits - 1) {
                canvas.clipRect(0, 0, bitW / 2, bitH);
            }
            canvas.drawBitmap(bitmap, 0, 0, null);
            canvas.restore();
        }
        canvas.restore();
    }
复制代码

此时,检测的结果以下,和上图相比,咱们很好地解决了过分绘制的问题:

(11) 使用 AsyncLayoutInflater 异步加载布局

Android Support Library 24中,提供了一个AsyncLayoutInflater工具类用于实现xml布局的异步inflate,它的用法和普通的LayoutInflater相似,只不过它inflate的执行是在子线程当中,当这一过程完成以后,再经过OnInflateFinishedListener接口,回调到主线程当中。

首先是整个Activity的根布局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/ll_root"
    android:orientation="vertical" 
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:id="@+id/tv_async"
        android:text="开始异步 Inflate 布局"
        android:gravity="center"
        android:layout_width="match_parent"
        android:layout_height="40dp"/>
</LinearLayout>
复制代码

接下来是须要异步inflate的子布局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:text="异步 Inflate 的布局"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"/>
</LinearLayout>
复制代码

使用方式以下:

private void asyncInflated() {
        TextView textView = (TextView) findViewById(R.id.tv_async);
        final ViewGroup root = (ViewGroup) findViewById(R.id.ll_root);
        textView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                AsyncLayoutInflater asyncLayoutInflater = new AsyncLayoutInflater(OptActivity.this);
                asyncLayoutInflater.inflate(R.layout.layout_async, root, new AsyncLayoutInflater.OnInflateFinishedListener() {

                    @Override
                    public void onInflateFinished(View view, int resId, ViewGroup parent) {
                        parent.addView(view);
                    }
                });
            }
        });
    }
复制代码

inflate方法接收三个参数:

  • 须要异步inflate的布局id
  • 所须要添加到的根布局的实例。
  • 异步inflate完成的回调,该回调是在主线程当中执行。须要注意,在该回调执行时,异步inflate出来的布局并无添加到父布局当中,所以,咱们须要经过addView的方法将其添加到View树当中。

最终的运行结果为:

(12) 使用性能检测工具,找出布局中的性能瓶颈

在分析布局有可能致使的性能问题时,咱们通常会用到如下几种工具,这些工具咱们在以前学习性能优化工具的时候都有接触过:


更多文章,欢迎访问个人 Android 知识梳理系列:

相关文章
相关标签/搜索