Android性能优化之布局优化

管你APP采用了多么高大上的技术,做出了多么炫酷的动画,如果使用起来体验不好,各种花式卡顿,内存占用大得一批的话,估计很少有人愿意愉快地使用.因此我们不能只考虑如何实现功能效果,还必须要重视性能的优化!

本篇文章就从看得见的入手,先来学习如何优化我们的布局,闲话不多说,直接开始.

image

include标签

include标签是用于将一个布局引入到当前布局中.举个例子,一般我们的APP几乎每个页面都有标题栏吧,那你需要在每个页面都去写一次标题栏的布局,麻不麻烦?(不麻烦,反正都是ctrl+v,怕个卵),好吧,突然有一天,产品叫你把标题栏中的一个TextView从中间移动到左边,那你就需要去每一个布局文件里面修改代码,你再敢说不麻烦?

image

因此,我们就可以用include标签来进行优化.首先在res/layout中新建一个layout_title_bar.xml:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="?android:attr/actionBarSize"
    android:background="@color/colorPrimary">

    <TextView
        android:id="@+id/tv_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:textColor="#ffffff"
        android:textSize="18sp"
        tools:text="标题" />

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical"
        android:layout_marginLeft="10dp"
        android:src="@drawable/back_arrow" />
</FrameLayout>

你贴这代码啥意思?不给我看效果图?

image

大哥,效果图来了.实际上这个布局里面写的就是我们每个页面需要使用的标题栏的布局,布局很简单,不多解释.然后我们在需要使用标题栏的布局中通过include将layout_title_bar.xml中的内容引入即可:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <include layout="@layout/layout_title_bar"/>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginTop="10dp"
        android:text="道路千万条,安全第一条,行车不规范,亲人两行泪" />
</FrameLayout>

效果...行了,别说了:

image

我们发现,在布局中有一个<include />标签,里面有一个叫layout的属性,没错,这个layout属性的值就是你要引入的布局.如果你在每一个需要使用标题栏的布局中都通过include的方式引入的话,那么当有一天,需要将标题栏中的标题从中间移动到左边,只需要更改layout_title_bar.xml就好了,所有通过include标签引入的标题栏布局都会跟着变化.因此我们总结下include的好处是:降低xml的维护成本,提高代码复用率.听起来还是很不错的,但是,include的本质还是将引入布局的代码放在include标签所在处,这样并没有优化什么性能呀!上面使用include后,实际上就等同于下面的代码:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="?android:attr/actionBarSize"
        android:background="@color/colorPrimary">

        <TextView
            android:id="@+id/tv_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:textColor="#ffffff"
            android:textSize="18sp"
            tools:text="标题" />

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:layout_marginLeft="10dp"
            android:src="@drawable/back_arrow" />
    </FrameLayout>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginTop="10dp"
        android:text="道路千万条,安全第一条,行车不规范,亲人两行泪" />
</FrameLayout>

确实,无论你是直接ctrl+v还是使用include,最终的样子都是这样,只不过使用include会使你的xml更简洁,也更方便修改和扩展.但是,并没有优化什么性能呀(你个骚小伙坏得很,净写些不沾边的东西),别急嘛,马上介绍merge.

merge标签

merge标签一般是配合include标签使用的,用于减少布局嵌套层次.我们知道,要想让我们的layout被系统更快的解析,必须要保证嵌套尽可能的少,如果你写过这样的布局,那么你真的应该反思下了:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        ...
        ...>
        <LinearLayout
            ...
            ...>
            <LinearLayout
                ...
                ...>
                <LinearLayout
                    ...
                    ...>
                    <LinearLayout
                        ...
                        ...>
    
                    </LinearLayout>
                </LinearLayout>
            </LinearLayout>
        </LinearLayout>
    </LinearLayout>
</RelativeLayout>

我们再回过头来看看前面的那个include的布局,我们知道,使用了include就等同于将另一个布局文件的内容加到了本布局里面,所以使用include的后就等同于下面的代码:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="?android:attr/actionBarSize"
        android:background="@color/colorPrimary">

        <TextView
            android:id="@+id/tv_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:textColor="#ffffff"
            android:textSize="18sp"
            tools:text="标题" />

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:layout_marginLeft="10dp"
            android:src="@drawable/back_arrow" />
    </FrameLayout>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginTop="10dp"
        android:text="道路千万条,安全第一条,行车不规范,亲人两行泪" />
</FrameLayout>

当前的ViewTree图:

image

我们发现,这个布局的根布局为FrameLayout,而layout_title_bar.xml的根布局也是FrameLayout,因此将layout_title_bar.xmlinclude进来后,就多了一层不必要的FrameLayout,怎么解决这个问题呢?就是使用merge标签了.
我们对layout_title_bar.xml的代码稍作修改:

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

    <TextView
        android:id="@+id/tv_title"
        android:layout_width="match_parent"
        android:layout_height="?android:attr/actionBarSize"
        android:background="@color/colorPrimary"
        android:gravity="center"
        android:textColor="#ffffff"
        android:textSize="18sp"
        tools:text="标题" />

    <ImageView
        android:layout_width="?android:attr/actionBarSize"
        android:layout_height="?android:attr/actionBarSize"
        android:scaleType="center"
        android:src="@drawable/back_arrow" />
</merge>

注意看根布局已经从原来的FrameLayout变成了merge,并且对里面的View属性也做了一些调整.现在如果再有布局includelayout_title_bar.xml这个布局的话,就会忽略它的父布局,直接将全部子View include进去.

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <include layout="@layout/layout_title_bar"/>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginTop="10dp"
        android:text="道路千万条,安全第一条,行车不规范,亲人两行泪" />
</FrameLayout>

现在上面的这段代码就等同于:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/tv_title"
        android:layout_width="match_parent"
        android:layout_height="?android:attr/actionBarSize"
        android:background="@color/colorPrimary"
        android:gravity="center"
        android:textColor="#ffffff"
        android:textSize="18sp"
        tools:text="标题" />

    <ImageView
        android:layout_width="?android:attr/actionBarSize"
        android:layout_height="?android:attr/actionBarSize"
        android:scaleType="center"
        android:src="@drawable/back_arrow" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginTop="10dp"
        android:text="道路千万条,安全第一条,行车不规范,亲人两行泪" />
</FrameLayout>

再来看现在的ViewTree图

image

可以发现,已经没有了多余的嵌套.所以merge多与include联合使用,意在减少因使用include而造成的布局嵌套.另外需要注意的是:merge只能用于根布局

还有很多朋友可能会觉得,并不一定我当前布局的根布局正好是FrameLayout,但是我引入的布局使用了merge且里面的控件都是按照FrameLayout进行布局的,这怎么办呢?可以手动给include加上FrameLayout根布局,例如当我inlcudelayout_title_bar.xml的根布局不是FrameLayout而是LinearLayout,这个时候可以这样做来达到引入标题栏布局不错乱:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <include layout="@layout/layout_title_bar"/>
    </FrameLayout>
    ...
</LinearLayout>

ViewStub视图

我们先来看一个场景,反手就是一个效果图:

image

我们模拟一个需求,当点击"点我查看详情"按钮时,中间的内容显示出来.这个需求简直不能再简单了,我们先完成这个布局:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <FrameLayout
        android:id="@+id/fl_title_bar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <include layout="@layout/layout_title_bar" />
    </FrameLayout>

    <Button
        android:id="@+id/btn_show_detail"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/fl_title_bar"
        android:layout_centerHorizontal="true"
        android:text="点我查看详情" />

    <LinearLayout
        android:id="@+id/ll_detail"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:layout_marginLeft="30dp"
        android:layout_marginRight="30dp"
        android:orientation="vertical">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="40dp"
            android:background="@color/colorPrimary"
            android:gravity="center"
            android:text="《流浪地球》"
            android:textColor="#ffffff"
            android:textSize="16sp" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="#CFE7FA"
            android:gravity="center"
            android:lineSpacingExtra="10dp"
            android:padding="12dp"
            android:text="@string/main_lyric" />
    </LinearLayout>
</RelativeLayout>

然后我们默认给id为ll_detail的LinearLayout加上android:visibility="gone",使它默认为隐藏状态,在代码中通过findViewById找到idbtn_show_detailll_detail,给按钮加上点击事件,在点击事件中让ll_detail变为可见状态就可以了.代码太简单就不贴上来了.但是我们想一想,有可能从头到尾,用户都不会去点击你的那个按钮,也就不会触发显示我们写的详情的布局,但是无论显示与否,我们写的布局内容都会被系统所解析与加载,都会占据系统资源.现在我们就遇到一个问题了,我们布局中的某部分布局,在程序运行的过程中有可能被显示也有可能不被显示,但是却一直都会占据着系统资源,对于这种布局, 我们能不能再它需要显示的时候,再去解析加载呢?答案是可以使用ViewStub来延迟加载布局.

首先我们把需要延迟加载的布局代码抽取到res/layout/layout_detail.xml中:

<?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:layout_marginLeft="30dp"
    android:layout_marginRight="30dp"
    android:orientation="vertical">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:background="@color/colorPrimary"
        android:gravity="center"
        android:text="《流浪地球》"
        android:textColor="#ffffff"
        android:textSize="16sp" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#CFE7FA"
        android:gravity="center"
        android:lineSpacingExtra="10dp"
        android:padding="12dp"
        android:text="@string/main_lyric" />
</LinearLayout>

然后我们修改activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <FrameLayout
        android:id="@+id/fl_title_bar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <include layout="@layout/layout_title_bar" />
    </FrameLayout>

    <Button
        android:id="@+id/btn_show_detail"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/fl_title_bar"
        android:layout_centerHorizontal="true"
        android:text="点我查看详情" />

    <ViewStub
        android:id="@+id/vs_detail"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout="@layout/layout_detail"
        android:layout_centerInParent="true" />
</RelativeLayout>

注意这里已经将之前写的详情布局替换成了ViewStub,在ViewStub中通过android:layout="@layout/layout_detail"将需要动态加载的布局指定给ViewStub.默认情况下ViewStub是隐藏的,实际上ViewStub是通过将自身的宽高设置为0来实现隐藏的,当布局加载后,ViewStub会存在于布局当中,只不过宽高都为0所以看不到

image

怎么在需要的时候去加载指定的布局内容呢?我们可以通过使用ViewStub的View inflate()方法或是void setVisibility(int visibility)方法来控制是否加载指定布局.推荐使用inflate方法,因为此方法会返回被加载布局的根节点View,你可以进一步的通过该View对象的findViewById方法去找到被加载布局中的控件.

private ViewStub mVsDetail;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    mVsDetail = findViewById(R.id.vs_detail);
    findViewById(R.id.btn_show_detail).setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            View detailView = mVsDetail.inflate();
        }
    });
}

到目前一切都是那么简单,运行程序,点击按钮,果然我们的详情界面就加载出来了.现在我们再来看看ViewTree的情况

image

你会发现,ViewStub不见了,取而代之的是我们动态加载的布局内容.因此调用ViewStub的inflate方法的时候,就是将ViewStub从布局中移除,加载通过layout属性指定给ViewStub的布局,并将ViewStub中的所有layout属性都复制到动态加载布局的根布局中.

你以为这样就完了?再点一次按钮试试?芽儿哦!闪退

java.lang.IllegalStateException: ViewStub must have a non-null ViewGroup viewParent

说明inflate方法只能调用一次,那怎么解决这个问题呢?我们可以通过一个boolean类型的标记变量以及ViewStub的一个叫做OnInflateListener的回调接口解决,具体看代码,很简单

private ViewStub mVsDetail;
private boolean mIsInflate;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    mVsDetail = findViewById(R.id.vs_detail);
    findViewById(R.id.btn_show_detail).setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            if (!mIsInflate) {
                View detailView = mVsDetail.inflate();
            }
        }
    });

    mVsDetail.setOnInflateListener(new ViewStub.OnInflateListener() {
        @Override
        public void onInflate(ViewStub stub, View inflated) {
            mIsInflate = true;
        }
    });
}

谢谢

以上就是比较常用的Android布局性能优化方案,谢谢大家耐心看完,如有误,请指出,谢谢!

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

推荐阅读更多精彩内容