0. 前言
上一篇我们分析了为什么LinearLayout会比RelativeLayout性能更高,但是对于布局优化问题,对这两种布局的选择远不如减少布局层级、避免过分绘制、按需加载等效果明显。所以本篇将着重总结Android布局性能优化的各种技巧。
这里再补充一下为什么要布局优化。
Android系统中每16ms就发出一个VSYNC信号,触发UI渲染,要达到界面流程,大多数的操作必须在16ms内完成(对应60fps)。
本文原创,转载请注明出处:Android开发——布局性能优化的一些技巧(一)_SEU_Calvin的博客-CSDN博客。
1. 减少嵌套
(1)在不响应层级深度的情况下,如果使用LinearLayout和RelativeLayout都可以实现相同的效果,那么建议使用Linearlayout。如果使用LinearLayout不能完成某些效果,那么使用RelativeLayout,而不是两层或更多层的LinearLayout,这样就可以尽量少的View层级,提高布局性能。
(2)尽量不使用LinearLayout的权重属性,因为它会让LinearLayout多进行一次measure。
(3)RelativeLayout的子View如果高度尽量和RelativeLayout相同,因为如果不同,会导致RelativeLayout在onMeasure()方法中做横向测量时,纵向的测量结果尚未完成,只好暂时使用自己的高度传入子View系统。而父View给子View传入的值没有变化的时候,是不会做无谓的测量的,所以RelativeLayout的子View如果高度和RelativeLayout相同可以进行一些布局上的优化,如果实现不行可以使用padding代替margin。
2. <include/>
<include>标签把一个布局中加载到另外一个布局,比如titlebar这种布局,很适合复用,这样可以使代码结构清晰,又可统一修改使用。
如我们在主xml文件里调用<include>来使用这个公共布局:
<?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">
<include layout="@layout/title_bar"/>
</RelativeLayout>
【拓展】include标签仅支持layout_开头的属性(和id),且android:layout_width和android:layout_height属性必须存在,才能使用其它属性(如:android:layout_grivity、android:layout_align...),以避免include引用中的子组件属性影响到include的布局效果。
比如下面这个例子给我们include进的组件设置高度和位置:
<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">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="10dp">
<include layout="@layout/title_bar" />
<include
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_alignParentBottom="true"
layout="@layout/ title_bar"/>
</RelativeLayout>
</RelativeLayout>
3. <merge/>
<merge/>标签通常和<include/>标签一起使用,避免根标签的重复嵌套,降低布局的层级。
(1)比如说我们的RelativeLayout中只有一个TextView,这个TextView不需要指定任何针对父视图的布局属性,只添加到父视图上并显示,这种情况如果把<RelativeLayout/>标签改为<merge/>标签,那么层级结构里就少了RelativeLayout这层布局。
(2)还有就是比如LinearLayout里面使用include嵌入一个布局,而这个嵌入的布局的根节点也是LinearLayout,这样就多了一层没有用的嵌套,这个时候如果我们使用<merge/>作为嵌入布局的根标签就可以避免重复嵌套的问题。
例子演示如下:
<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" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="merge标签使用" />
</RelativeLayout>
运行后使用“DDMS -> DumpView Hierarchy for UI Automator”工具,截图如下:
最下面两层RelativeLayout与TextView就是布局中的内容,上面的FrameLayout是顶层视图。
下面如果我们将上述布局代码中的RelativeLayout修改为merge标签再查看层级结构如下:
4. ViewStub
一个最最最可能的使用场景就是请求网络,如果网络异常就需要有一个View来提示用户。显然这个View不经常被使用,如果我们通过代码逻辑动态更改这个View的可见性(GONE或者VISIBLE),在Inflate布局的时候View仍然会被Inflate,也就是说仍然会创建对象,会被实例化且耗费内存资源。
ViewStub解决了这个布局性能问题,它是一个轻量级的View,宽高都是0,不占布局位置、占用资源非常小。只有当ViewStub被设置为可见或调用了ViewStub.inflate()的时候,ViewStub所指向的布局才会被Inflate和实例化,从而达到了按需要才加载的目的,优化了布局性能。
【拓展】(ViewStub的布局属性会传给它所指向的布局,ViewStub对象会被置空,此时查看布局结构ViewStub是不存在的,取而代之的是被inflate的Layout。
综上ViewStub的原理,就可以使用它来方便的在运行时,决定要不要显示某个布局。使用实例如下:
<?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" >
……
<ViewStub
android:layout_gravity="center"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/hint_fail_view"
android:inflatedId="@+id/hint_fail_view"
android:layout="@layout/fail_view"/>
</merge>
android:layout="@layout/fail_view"指向页面加载失败的布局文件,里面包含一个id为tv的TextView。
当出现网络异常时,我们在代码里这样使用ViewStub:
private View hintFailView;
if (网络异常) {
if (hintFailView == null) {
ViewStub viewStub = (ViewStub)this.findViewById(R.id.hint_fail_view);
hintFailView = viewStub.inflate(); //注意这里
TextView textView = (TextView) hintFailView.findViewById(R.id.tv);
textView.setText("网络异常");
}
hintFailView.setVisibility(View.VISIBLE);
}else{
//网络正常
if (hintFailView!= null) {
hintFailView.setVisibility(View.GONE);
}
//业务逻辑
}
5. 避免OverDraw
如果父控件和子控件都设置了Background,那么就形成了OverDraw,我们可以通过设置-开发者选项-显示GPU过度绘制来查看应用是否存在严重的OverDraw问题。
如果你发现应用中有些色块为红色,那么你可要去优化它了,你需要去根据颜色提示去找到你过度绘制的地方。
(优化点1)如果我们有自己的背景色,顶层View的背景色我们可以置空来优化。
setContentView(R.layout.activity_overdraw_01);
getWindow().setBackgroundDrawable(null);
(优化点2)在自定义组件的onDraw()中考虑使用画布的clipRect()方法来绘制需要被绘制的区域。
在看clipRect方法之前先看看如果将res目录下的图片文件转换为bitmap对象,这里总结了两种方法,大家可以参考使用:
InputStream is = this.getContext().getResources().openRawResource(R.drawable.icon);
Bitmap mBitmap = BitmapFactory.decodeStream(is);
//或者使用BitmapDrawable
Bitmap mBitmap = new BitmapDrawable(is).getBitmap();
clipRect方法可以截取画布中的一个矩形区域,在此区域外的将不再绘制显示。实例如下:
/*
*author SEU_Calvin in 2016/10
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = mBmp.getWidth();
int height = mBmp.getHeight();
canvas.save();
mPaint.setColor(Color.CYAN);
//先在屏幕的0,0处绘制一个与我们Bitmap宽高相等的蓝色矩形
canvas.drawRect(0, 0, width, height, mPaint);
canvas.restore();
canvas.save();
//裁剪画布,左上角为0,0 右下角为指定宽高的2倍和1.5倍
canvas.clipRect(0, 0, width*2, height*3/2);
//以width,height为左上角绘制我们的Bitmap,由于图片的下半部分在裁剪画布之外所以不显示
canvas.drawBitmap(mBmp, width, height, mPaint);
canvas.restore();
}
结果如下所示,工作过程已经在代码的注释里写的很清楚了。
6. 其他小技巧
避免在自定义控件的onDraw()方法中创建大量的临时对象,比如String,因为该方法可能会被频繁调用,所以可能会频繁的触发GC。
为了控制篇幅,将一些看了让人感到惊艳的布局优化小技巧总结分享到了布局性能优化的一些技巧(二),希望可以帮助到你~
最后希望各位看官老爷们多点赞支持~