一、前言
性能优化包含的部分很多,包括布局、内存、耗电、流量等等,其中布局优化是最容易掌握,也最容易被大家所忽视的一个方面,今天,就来介绍一下有关布局优化的一些技巧。
二、布局优化技巧
(1) 使用 标签进行布局复用
当我们的布局中有多个相同的布局时,可以使用include
标签来进行布局的复用,这样,当视觉需要修改单个Item
的间距,文字大小时,只需要修改一个布局就可以了,例如像下面这种情况,我们就可以使用include
标签来实现:
复制代码
单个Item
的布局为:
复制代码
要点:
- 直接在根布局中,如果希望找到
<include>
所指定的layout
中包含的控件,那么就需要给<include>
指定id
,再通过它来寻找子容器中的控件。 - 在
<include>
标签中,可以指定layout_xxx
属性,它将会覆盖子布局中的根标签中的属性。
(2) 使用 标签减少布局层级
当出现下面这种情况:一个xml
布局文件的根节点是一个FrameLayout
,并且它没有一个有用的背景,那么当该xml
布局文件渲染出的ViewGroup
被添加到父布局中时,连接处就会出现一个多余的节点,而采用<merge>
标签可以去掉这一无用节点,从而降低布局的层级。
例如,在上面的例子当中,我们使用了<merge>
标签的情形为:
<merge>
标签,那么情形为: 要点: - 当需要通过
LayoutInflater
的inflate
方法渲染出以<merge>
作为根节点标签的xml
文件时,必须传入不为null
的root
参数,且attachToRoot
参数必须为true
。 <merge>
只可作为xml
的根节点。<merge>
既不是View
也不是ViewGroup
,它只是表示一组等待被添加的视图,因此,对它设定的任何属性都是无用的。
(3) 使用 ViewStub 标签动态加载布局
当我们的布局中,存在一些需要按序加载的控件,那么就可以使用ViewStub
标签预先声明,当情况满足时再去实例化ViewStub
中所声明的布局,其用法如下:
- 首先,在布局中预先声明
ViewStub
,并且通过layout
标签指定对应的布局layout_stub
:
复制代码
- 当需要加载以上指定的布局时,那么首先通过获得
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
参数的LinearLayout
、RelativeLayout
,这里选取的标准为带有layout_weight
的LinearLayout
或者RelativeLayout
会测量两次。 - 当使用
LinearLayout
时,应当尽量避免使用layout_weight
参数。 - 避免使用
RelativeLayout
嵌套RelativeLayout
。 - 如果允许,那么可以使用
Google
新推出的ConstraintLayout
布局。
(5) 使用 SpannableStringBuilder 替换多个 TextView 的实现
当我们存在多种不同大小、颜色或者图文混排需要显示时,我们往往会利用多个TextView
来进行组合,但是某些效果通过一个TextView
就可以实现,一般来说,利用SpannableStringBuilder
可以通过单个TextView
实现多种不同的布局,更多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
属性来实现分割线: 复制代码
与分割线相关的属性包括以下几个:
divider
:传入分割线的drawable
,可以是一个图片,也可以是自己通过xml
实现的drawable
。showDividers
:分割线显示的位置,beginning/middle/end
,分割对应头部、中间、尾部。dividerPadding
:分割线距离两边的间距。
(7) 使用 Space 控件进行合理的占位
Space
控件位于android.support.v4.widget
包中,与一般控件不同,它的draw
方法是一个空实现,因此它只占位置,而不去渲染,使用它来进行占位填充比其它控件更加高效,例如下面,我们需要将一行均等地分成五份,有颜色部分位于2,4
当中:
Space
控件,加上 layout_weight
属性来实现: 复制代码
(8) 使用 TextView 的 drawableLeft/drawableTop 属性来替代 ImageView + TextView 的布局
当出现图片在文案的四周时,我们应当首先考虑能够通过单个TextView
来实现,而不是通过LinearLayout
包裹TextView+ImageView
的方式来实现,例如下面的效果:
复制代码
可以看到,虽然都是实现了图片加上文字的显示效果,但是第二种通过单个TextView
来实现其布局层级更少,并且控件的个数更少,因此效率更高,并且图片不仅可以显示在左边,还可以显示在TextView
的四周,图片和TextView
之间的间隔可以通过drawablePadding
来实现。
(9) 去掉不必要的背景
- 在布局层级中避免重叠部分的背景
当两个控件在布局上有重叠的部分,但是它们具有背景时,就会出现过度绘制的情况,造成无用的性能损耗。并且肉眼无法发现,需要通过设置当中的”调试GPU过度绘制"选项进行检查,详细使用如下:。例如下面布局当中,根布局和子控件有100dp
部分重叠,并且它们都有背景:
复制代码
那么最终,打开过度绘制检测时,就会出现下面的效果:
- 去掉无用的
WindowBackgroud
当我们使用某些主题时,系统有可能在DecorView
中给我们加上一个背景,但是有时候它是无用的,例如上面的例子中,我们根布局为紫色,这其实就是由于默认主题中的背景所导致的,我们可以通过下面的方式去除掉该背景。
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_overdraw); getWindow().setBackgroundDrawable(null); }复制代码
此时的检测结果如下,可以看到,根布局就不存在过度绘制的情况了:
(10) 优化自定义控件中的 onDraw 方法
当我们在自定义控件,并重写onDraw
方法来完成相应的需求时,一些错误的操作往往会导致布局效率的降低,一般来说,有两点需要注意:
- 避免在其中进行对象的分配
- 使用
Canvas
的ClipRect
方法避免过度绘制
这里用一个简单的例子来说明一下第二点的实现,当我们需要实现下面这个多张图片重叠的自定义控件时:
假如我们直接使用下面的方式,也可以实现上面的效果: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
过度绘制的开关,那么可以得到下面的检测结果,可以发现在两张图片重叠的地方,会出现明显的过度绘制:
ClipRect
对 onDraw
方法进行优化: @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
的根布局:
复制代码
接下来是需要异步inflate
的子布局:
复制代码
使用方式如下:
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) 使用性能检测工具,找出布局中的性能瓶颈
在分析布局有可能导致的性能问题时,我们一般会用到以下几种工具,这些工具我们在之前学习性能优化工具的时候都有接触过:
HierecyViewer
- 调试
GPU
过度绘制 Lint
检查
更多文章,欢迎访问我的 Android 知识梳理系列:
- Android 知识梳理目录:
- 个人主页:
- 个人知识总结目录: