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

一、前言

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

二、布局优化技巧

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

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


根布局为:

<?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>

<include> 要点

  • 直接在根布局中,如果希望找到<include>所指定的layout中包含的控件,那么就需要给<include>指定id,再通过它来寻找子容器中的控件。
  • <include>标签中,可以指定layout_xxx属性,它将会覆盖子布局中的根标签中的属性。

(2) 使用 <merge> 标签减少布局层级

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

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


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

<merge> 要点

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

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

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

  • 首先,在布局中预先声明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> 要点

  • 任何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 知识梳理系列:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容