在 Android 开发的过程中,对性能进行优化是必不可少的一步。而在布局上的优化并不像其他优化方式那么复杂,通过 Android SDK 提供的 Hierarchy Viewer 可以很直接地看到冗余的层级,去除这些多余的层级将使我们的UI变得更加流畅。
如何使用 Hierarchy Viewer ?
在 Android studio 菜单栏上,点击 Tools --> Android --> Android Device Monitor
在 Android Device Monitor 菜单栏上,点击 Window --> Open perspective --> Hierarchy Viewer 即可。
然而,一般在真机上无法使用 Hierarchy Viewer,只能在运行开发版 Android 系统的设备进行交互(一般来说,使用模拟器上即可),着实有点遗憾。但是,也可以在真机上通过 root 等一系列操作之后,便可以使用 Hierarchy Viewer 。这部分教程就需要大家在网上另行查阅了。
下面是一些常用的布局优化方式:
一、include 布局
当多个页面公用了一些UI组件时,就可以使用 include 布局。Android 提供了 include 标签,让我们可以将子布局引入到一个布局文件中,这样一来,公用布局就可以独立成为一个布局 xml,其他页面只要 include 引用这个布局xml即可。
下面以一个自定义标题栏为例
<?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="wrap_content">
<ImageButton
android:id="@+id/title_back_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:src="@drawable/back" />
<TextView
android:id="@+id/title_textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Title" />
</RelativeLayout>
将该标题栏引入一个Activity的xml中
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.jerry.myapplication.MainActivity">
<include
android:id="@+id/top_title"
layout="@layout/common_title" />
<TextView
android:id="@+id/username_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
通过 include 标签引入 common_title 这个布局(注意:include 布局中指定布局 xml 是使用 layout 属性,而不是 android:layout 属性)。这样一来,就复用了 common_title 的标题栏效果,不必在每个页面中重复定义标题栏布局。大大降低了我们维护 xml 的成本,也提升了代码复用率。
include 标签的原理:
在解析 xml 布局时,如果检测到 include 标签,那么就直接把该布局下的根视图添加到 include 所在的父视图中。对于布局 xml 的解析最终都会调用到 LayoutInflater 的 inflate 方法,该方法最终又会调用 rInflate 方法。这个方法就是遍历 xml 中的所有元素,然后逐个进行解析。
如何获取 include 布局里的控件?
以上面例子为例,获取标题栏内的 Textview
private TextView mTextView;
mTextView = findViewById(R.id.top_title).findViewById(R.id.title_textview);
二、merge 标签
merge 标签适用于子布局的根视图与它的父视图是同一类型。它的作用是合并UI布局,降低UI布局的嵌套层次。使用场景是存在多层使用同一种布局类的嵌套视图,这种情况下用merge标签作为子视图的顶级视图来解决多余的层级。
如上图,child_view 与 parent_view 都是FrameLayout类型,那么 child_view 下的两个控件可以直接使用 parent_view 来布局,这样就可以去除 child_view 这个层级。
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</FrameLayout>
上面这个布局相当于是 child_view,而 Activity 内容视图的顶层布局也 FrameLayout,因此产生了视图冗余,可以用 merge 标签去掉这层冗余。
下面是screen_title.xml文件的源代码:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:fitsSystemWindows="true">
<!-- Popout bar for action modes -->
<ViewStub android:id="@+id/action_mode_bar_stub"
android:inflatedId="@+id/action_mode_bar"
android:layout="@layout/action_mode_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="?attr/actionBarTheme" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="?android:attr/windowTitleSize"
style="?android:attr/windowTitleBackgroundStyle">
<TextView android:id="@android:id/title"
style="?android:attr/windowTitleStyle"
android:background="@null"
android:fadingEdge="horizontal"
android:gravity="center_vertical"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
<!-以下就是开发人员设置的布局所填充的位置-->
<FrameLayout android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="0dip"
android:layout_weight="1"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>
<?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">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</merge>
与 include 一样,merge 的解析也在 LayoutInflater 的 inflate() 函数中。在 inflate() 函数中循环解析 xml 中 的 tag,如果解析到 merge 标签则会调用 rinflate 函数。
注意事项:
merge 必须作为布局文件的根节点标签。
merge 并不是一个 ViewGroup,也不是一个 View,它相当于声明了一些视图,等待被添加。
因为 merge 标签不是 View,所以对 merge 标签设置的所有属性都是无效的。
因为 merge 标签不是 View,所以在通过 LayoutInflate.inflate 方法渲染的时候,第二个参数必须指定一个父容器,且第三个参数必须为 true,也就是必须为 merge 下的视图指定一个父亲节点。
如果 Activity 的布局文件根节点是 FrameLayout,可以替换为 merge 标签,这样,执行 setContentView 之后,会减少一层 FrameLayout 节点。
三、ViewStub 视图
ViewStub 是什么?
ViewStub 是一个不可见的和能在运行期间延迟加载目标视图的、宽高都为0的 View (ViewStub 继承 View)。当对一个 ViewStub 调用 inflate() 方法或设置它可见时,系统会加载在 ViewStub 标签中指定的布局,然后将这个布局的根视图添加到 ViewStub 的父视图中。换句话说,在对 ViewStub 调用 inflate() 方法或者设置 visible 之前,它是不占用布局空间和系统资源的,它只是为目标视图占了一个位置而已。
何时使用 ViewStub?
当我们只需要在某些情况下才加载一些耗资源的布局时,ViewStub 就成为我们实现这个功能的重要手段。
例如:有一个显示九宫格图片的 GridView 视图,我们想根据网络返回的数据来判断是否加载该 GridView ,因为默认加载的话会造成资源浪费,系统加载成本较高。这时使用 ViewStub 标签就可以很方便实现延迟加载。
以下是一个小小的演示,在一个 Activity 中使用 ViewStub 来动态加载
GridView。
<?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
android:id="@+id/top_title"
layout="@layout/common_title" />
<Button
android:id="@+id/show_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="显示图片" />
<ViewStub
android:id="@+id/comment_stub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout="@layout/layout_image_gv" />
</RelativeLayout>
布局的最后使用 ViewStub 来加载 layout_image_gv.xml 布局,下面是 layout_image_gv.xml 的代码:
<?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"
android:background="#aaaaaa">
<TextView
android:id="@+id/image_desc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="显示九宫格GridView" />
<GridView
android:id="@+id/image_gc"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/image_desc"
android:numColumns="3">
</GridView>
</RelativeLayout>
public class StubLayoutActivity extends AppCompatActivity {
private ViewStub gvStub;
private Button mButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_activity_stub);
gvStub = findViewById(R.id.comment_stub);
mButton = findViewById(R.id.show_btn);
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//加载目标视图,不能多次调用,否则会引发异常
gvStub.inflate();
//gvStub.setVisibility(View.VISIBLE);
}
});
}
}
ViewStub 小结
当用户手动调用 ViewStub 的 inflate 或者 setVisibility 函数(实际上也是调用 inflate 函数)时,会将 ViewStub 自身从父控件中移除,并且加载目标布局,然后将目标布局添加到 ViewStub 的父控件中,这样就完成视图的动态替换,也就是延迟加载功能。
四、减少视图树层级
为什么要减少视图层级?
每一个视图在显示时会经历测量、布局、绘制的过程,如果我们的布局中嵌套的视图层次过多,就会造成额外测量、布局等工作,使得UI变得卡顿,影响用户的使用体验。
例如一个简单的列表 Item
<?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="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
而使用 RelativeLayout 来布局这个 item view 就可以减少一层 LinearLayout 的渲染。
<?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">
<ImageView
android:id="@+id/profile_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/name_textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/profile_image" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/profile_image" />
</RelativeLayout>
五、总结
在 Android UI 布局过程中,需要遵守的原则有以下几点:
尽量多使用 RelativeLayout ,不要使用绝对布局 AbsoluteLayout;
在 ListView 等列表组件中尽量避免使用 LinearLayout 的 layout_weight 属性(子视图使用了 layout_weight 属性的 LinearLayout 会对它的子视图进行两次测量。);
将可复用的组件抽取出来并通过 <include/> 标签使用;
使用 <ViewStub/> 标签来加载一些不常用的布局;
使用 <merge/> 标签来减少布局的嵌套层次。