管你APP采用了多么高大上的技术,做出了多么炫酷的动画,如果使用起来体验不好,各种花式卡顿,内存占用大得一批的话,估计很少有人愿意愉快地使用.因此我们不能只考虑如何实现功能效果,还必须要重视性能的优化!
本篇文章就从看得见的入手,先来学习如何优化我们的布局,闲话不多说,直接开始.
include标签
include标签是用于将一个布局引入到当前布局中.举个例子,一般我们的APP几乎每个页面都有标题栏吧,那你需要在每个页面都去写一次标题栏的布局,麻不麻烦?(不麻烦,反正都是ctrl+v,怕个卵),好吧,突然有一天,产品叫你把标题栏中的一个TextView
从中间移动到左边,那你就需要去每一个布局文件里面修改代码,你再敢说不麻烦?
因此,我们就可以用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>
你贴这代码啥意思?不给我看效果图?
大哥,效果图来了.实际上这个布局里面写的就是我们每个页面需要使用的标题栏的布局,布局很简单,不多解释.然后我们在需要使用标题栏的布局中通过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>
效果…行了,别说了:
我们发现,在布局中有一个<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图:
我们发现,这个布局的根布局为FrameLayout,而layout_title_bar.xml
的根布局也是FrameLayout,因此将layout_title_bar.xml
include进来后,就多了一层不必要的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图
可以发现,已经没有了多余的嵌套.所以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视图
我们先来看一个场景,反手就是一个效果图:
我们模拟一个需求,当点击”点我查看详情”按钮时,中间的内容显示出来.这个需求简直不能再简单了,我们先完成这个布局:
<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_detail
和ll_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所以看不到
怎么在需要的时候去加载指定的布局内容呢?我们可以通过使用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的情况
你会发现,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布局性能优化方案,谢谢大家耐心看完,如有误,请指出,谢谢!
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!