Android 布局优化方案
前言
在 Android 开发中,UI 布局可以说是每个 App 使用频率很高的,随着 UI 越来越多,布局的重复性、复杂度也会随之增长,这样使得 UI布局的优化,显得至关重要,UI 布局不慎,就会引起过度绘制,从而造成 UI 卡顿的情况,本篇文章就来总结一下 UI 布局优化的相关技巧。
说明: 本文的源码都是基于 Android API 30 进行分析。
一、布局优化标签的使用
1.1 <include> 标签
include 标签常用于将布局中的公共部分提取出来供其他 layout 共用,以实现布局模块化,这在布局编写方便提供了大大的便利。
我们项目的 UI 中很多页面都会有一个TitleBar 部分,所以使用 <include> 标签进行复用,以便于统一管理,
我们写一个TitleBar (title_bar_layout.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="50dp">
<ImageView
android:id="@+id/iv_back"
android:layout_width="50dp"
android:layout_height="match_parent"
android:padding="15dp"
android:src="@drawable/back" />
<TextView
android:id="@+id/tv_title"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:layout_toRightOf="@+id/iv_back"
android:layout_toLeftOf="@+id/tv_sure"
android:gravity="center"
android:text="我是标题"
android:textSize="16sp" />
<TextView
android:id="@+id/tv_sure"
android:layout_width="50dp"
android:layout_height="match_parent"
android:gravity="center"
android:layout_alignParentRight="true"
android:text="确定"
android:textSize="16sp" />
</RelativeLayout>
然后我们在 activity_main.xml 中使用 <include> 标签引入上面定义的 TitleBar 布局,代码如下是所示:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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=".MainActivity">
<include
layout="@layout/title_bar_layout" />
<View
android:id="@+id/view_head_line"
android:layout_width="match_parent"
android:layout_height="1px"
android:layout_marginTop="50dp"
android:background="@color/black" />
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/view_head_line"
android:gravity="center"
android:text="我是内容" />
</RelativeLayout>
运行效果如下图所示:
<include>
标签唯一需要的属性是 layout 属性,用来指定需要包含的布局文件。也可以定义 android:id 和 android:layout_* 属性来覆盖被引入布局根节点的对应属性值。
注意的问题
使用 <include> 最常见的问题就是 findViewById 查找不到目标控件,这个问题出现的前提是在 <include> 标签中设置了 android:id 属性导致子布局根节点的 android:id失效了,而在 findViewById 时却用了被 <include> 进来的布局的根元素 android:id 中设置的值。
例如上述例子中,设置 TitleBar (title_bar_layout.xml) 的根节点的 android:id 属性值为 child_title_bar,代码如下所示:
<?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="50dp"
android:id="@+id/child_title_bar">
// 里面的内容省略,更上面提供的布局一样。
// ...
</RelativeLayout>
然后在 activity_main.xml 中通过 <include> 应用TitleBar 子布局,然后设置 android:id 属性为 mian_title_bar,代码如下所示:
<include
android:id="@+id/main_title_bar"
layout="@layout/title_bar_layout" />
此时如果通过 findViewById 来找 child_title_bar 这个控件,然后再查找 child_title_bar 下的子控件则会抛出空指针。代码如下 :
// 此时 titleBar 为空,找不到
View titleBar = findViewById(R.id.child_title_bar);
// 此时空指针
TextView tvTitle = titleBar.findViewById(R.id.tv_title);
tvTitle.setText("new Title");
其正确的使用形式应该如下:
View titleBar = findViewById(R.id.main_title_bar);
TextView tvTitle = titleBar.findViewById(R.id.tv_title);
tvTitle.setText("new Title");
或者更简单的直接查找他的子控件
TextView tvTitle = findViewById(R.id.tv_title);
tvTitle.setText("new Title");
但是当 activity_main.xml 中有多个 <include> 标签时,而且标签中有相同的 android:id 属性值时,就不能使用上述简单的直接查找方式了。如果直接通过 android:id 属性值去查找子控件的话,他是查到到第一个 <include> 应用的布局中的子控件。
验证:
我们把 activity_main.xml 修改为以下形式:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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=".MainActivity">
<include
layout="@layout/title_bar_layout" />
<View
android:id="@+id/view_head_line"
android:layout_width="match_parent"
android:layout_height="1px"
android:layout_marginTop="50dp"
android:background="@color/black" />
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/view_head_line"
android:layout_above="@+id/view_foot_line"
android:gravity="center"
android:text="我是内容" />
<View
android:id="@+id/view_foot_line"
android:layout_width="match_parent"
android:layout_height="1px"
android:layout_alignParentBottom="true"
android:layout_marginBottom="50dp"
android:background="@color/black" />
<include
layout="@layout/title_bar_layout"
android:layout_height="50dp"
android:layout_width="match_parent"
android:layout_alignParentBottom="true"/>
</RelativeLayout>
然后在 MainActivity 中使用直接查找的方式使用控件,代码如下所示:
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView tvTitle = findViewById(R.id.tv_title);
tvTitle.setText("new Title");
}
}
运行结果如下所示:
我们发现只有第一个 <include> 标签中 tv_title 修改了。
所以,多个 <include> 标签的正确使用方法是,每个 <include> 标签都设置 android:id 属性,然后查找的时候根据 <include> 中设置的 android:id 属性值找到它应用的子布局的跟节点,再根据根节点查找根节点的子控件,代码如下所示:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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=".MainActivity">
<include
android:id="@+id/head_title_bar"
layout="@layout/title_bar_layout" />
<View
android:id="@+id/view_head_line"
android:layout_width="match_parent"
android:layout_height="1px"
android:layout_marginTop="50dp"
android:background="@color/black" />
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/view_foot_line"
android:layout_below="@+id/view_head_line"
android:gravity="center"
android:text="我是内容" />
<View
android:id="@+id/view_foot_line"
android:layout_width="match_parent"
android:layout_height="1px"
android:layout_alignParentBottom="true"
android:layout_marginBottom="50dp"
android:background="@color/black" />
<include
android:id="@+id/foot_title_bar"
layout="@layout/title_bar_layout"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_alignParentBottom="true" />
</RelativeLayout>
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
View headTitleBar = findViewById(R.id.head_title_bar);
TextView tvHeadTitle = headTitleBar.findViewById(R.id.tv_title);
tvHeadTitle.setText("new head Title");
View footTitleBar = findViewById(R.id.foot_title_bar);
TextView tvFootTitle = footTitleBar.findViewById(R.id.tv_title);
tvFootTitle.setText("new foot Title");
}
}
运行结果如下:
下面我们分析 <include> 设置了 android:id 属性,然后我们在使用 findViewById 传入子布局中根节点设置的android:id 时,找不到根节点的原因。
对于布局文件的解析,最总都会调用到 LayoutInflater 的 inflate 方法,该方法最终又会调用 RInflate 方法,我们就从这个方法开始分析。
rInflate 方法代码如下所示:
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
final int depth = parser.getDepth();
int type;
boolean pendingRequestFocus = false;
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
if (type != XmlPullParser.START_TAG) {
continue;
}
final String name = parser.getName();
if (TAG_REQUEST_FOCUS.equals(name)) {
pendingRequestFocus = true;
consumeChildElements(parser);
} else if (TAG_TAG.equals(name)) {
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
// ... 2
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
throw new InflateException("<merge /> must be the root element");
} else {
// ... 1
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflateChildren(parser, view, attrs, true);
viewGroup.addView(view, params);
}
}
if (pendingRequestFocus) {
parent.restoreDefaultFocus();
}
if (finishInflate) {
parent.onFinishInflate();
}
}
这个方法其实就是遍历 xml 中的所有元素,然后挨个进行解析,例如解析到一个控件类型的标签,就会通过注释1处的代码,根据用户设置的 layout_*、andriod:id 、android:backage 等属性来够着一个 View 对象,然后添加到它的父控件(ViewGroup)中。<include> 标签也是一样,就会通过注释2处代码来解析 <include> 标签,主要是通过 parseInclude 方法解析,代码如下所示:
private void parseInclude(XmlPullParser parser, Context context, View parent,
AttributeSet attrs) throws XmlPullParserException, IOException {
int type;
// <include> 标签必须使用在 ViewGroup 中
if (!(parent instanceof ViewGroup)) {
throw new InflateException("<include /> can only be used inside of a ViewGroup");
}
// ......
// <include> 标签中必须要设置 layout 属性,否则会抛出异常。
if (layout == 0) {
final String value = attrs.getAttributeValue(null, ATTR_LAYOUT);
throw new InflateException("You must specify a valid layout "
+ "reference. The layout ID " + value + " is not valid.");
}
final View precompiled = tryInflatePrecompiled(layout, context.getResources(),
(ViewGroup) parent, /*attachToRoot=*/true);
if (precompiled == null) {
final XmlResourceParser childParser = context.getResources().getLayout(layout);
try {
final AttributeSet childAttrs = Xml.asAttributeSet(childParser);
while ((type = childParser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty.
}
if (type != XmlPullParser.START_TAG) {
throw new InflateException(getParserStateDescription(context, childAttrs)
+ ": No start tag found!");
}
// 解析 <include> 应用的子布局中的第一个元素
final String childName = childParser.getName();
// 如果第一个元素是 <merge> 标签,那么调用 rInflate 方法解析
if (TAG_MERGE.equals(childName)) {
// The <merge> tag doesn't support android:theme, so
// nothing special to do here.
rInflate(childParser, parent, context, childAttrs, false);
} else {
// 我们例子中的情况会走到这一步,首先根据 include 的属性集创建被 include 进来的xml布局的根 view
// 这里的根 view 对应为 title_bar_layout.xml中的 LinearLayout
final View view = createViewFromTag(parent, childName,
context, childAttrs, hasThemeOverride);
final ViewGroup group = (ViewGroup) parent;
final TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.Include);
// 获取 <include> 标签中设置的 id。
final int id = a.getResourceId(R.styleable.Include_id, View.NO_ID);
final int visibility = a.getInt(R.styleable.Include_visibility, -1);
a.recycle();
// We try to load the layout params set in the <include /> tag.
// If the parent can't generate layout params (ex. missing width
// or height for the framework ViewGroups, though this is not
// necessarily true of all ViewGroups) then we expect it to throw
// a runtime exception.
// We catch this exception and set localParams accordingly: true
// means we successfully loaded layout params from the <include>
// tag, false means we need to rely on the included layout params.
ViewGroup.LayoutParams params = null;
try {
// 获取布局属性
params = group.generateLayoutParams(attrs);
} catch (RuntimeException e) {
// Ignore, just fail over to child attrs.
}
if (params == null) {
params = group.generateLayoutParams(childAttrs);
}
view.setLayoutParams(params);
// Inflate all children. 解析所有的子控件
rInflateChildren(childParser, view, childAttrs, true);
// 如果 <include> 中设置了 id,就将此 id 设置给 include 子布局的根节点。
if (id != View.NO_ID) {
view.setId(id);
}
switch (visibility) {
case 0:
view.setVisibility(View.VISIBLE);
break;
case 1:
view.setVisibility(View.INVISIBLE);
break;
case 2:
view.setVisibility(View.GONE);
break;
}
// 将 include 进来的根节点加入到ViewGroup 中。
group.addView(view);
}
} finally {
childParser.close();
}
}
LayoutInflater.consumeChildElements(parser);
}
所以结论就是: 如果 <include> 标签中设置了andrid:id属性,那么就通过 <include> 标签中设置 android:id 属性值来查找被 include 布局根元素的 View;如果 <include> 标签中没有设置 android:id 属性, 而被 include 的布局的根元素设置了 android:id 属性,那么通过该根元素的 id 来查找该 View 即可。拿到根元素后查找其子控件都是一样的。
1.2 <merge> 标签
<merge> 标签主要用户辅助 <include> 标签,在使用 <include> 标签之后可能导致布局嵌套过多,多余的 layout 节点会导致解析变慢,不必要的节点和嵌套可以通过 Layout Inspector (下面会介绍) 或者通过设置中的显示布局边界查看,还可以通过 hierarchy viewer 查看布局边界,但是 hierarchy viewer 已经弃用,如果使用的是Android Studio 3.1 或更高版本,则应在运行时改用布局检查器以检查应用的视图层次结构。如需分析应用布局的渲染速度,请使用 Window.OnFrameMetricsAvailableListener**。
<merge> 标签可用于两种典型的情况:
(1) 布局根节点是 FrameLayout 且不需要设置 background 或者 padding 等属性,可以用 <merge> 标签代替,因为 Activity 内容视图的 parent View 就是一个 FrameLayout ,所以可以使用 <merge> 标签消除一个,减少布局嵌套,降低过度绘制。
(2) 某布局作为子布局被其他布局 include 时,使用merge当做该布局的根节点,这样在被引入时根节点就会自动被忽略,而将其子节点全部合并到主布局中。
还是以上面 TitleBar (title_bar_layout.xml)布局为例,在 activity_mian.xml 引用如下所示:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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=".MainActivity">
<include
android:id="@+id/head_title_bar"
layout="@layout/title_bar_layout" />
<View
android:id="@+id/view_head_line"
android:layout_width="match_parent"
android:layout_height="1px"
android:layout_marginTop="50dp"
android:background="@color/black" />
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/view_head_line"
android:gravity="center"
android:text="我是内容" />
</RelativeLayout>
运行之后,我们通过 Layout Inspector 查看 activity_mian 布局如下图所示:
可以发现多了一层没有必要的 RelativeLayout ,将 TitleBar (title_bar_layout.xml) 中的 RelativeLayout 替换为 merge ,代码如下所示:
title_bar_layout.xml
当使用了 <merge> 标签之后,子控件的宽高如果使用 match_parent 属性时,它是相对于 <include> 的父控件 ViewGroup 来配置的。
<?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="50dp"
android:id="@+id/child_title_bar">
<ImageView
android:id="@+id/iv_back"
android:layout_width="50dp"
android:layout_height="50dp"
android:padding="15dp"
android:src="@drawable/back" />
<TextView
android:id="@+id/tv_title"
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_weight="1"
android:layout_toRightOf="@+id/iv_back"
android:layout_toLeftOf="@+id/tv_sure"
android:gravity="center"
android:text="我是标题"
android:textSize="16sp" />
<TextView
android:id="@+id/tv_sure"
android:layout_width="50dp"
android:layout_height="50dp"
android:gravity="center"
android:layout_alignParentRight="true"
android:text="确定"
android:textSize="16sp" />
</merge>
再次运行之后,我们通过 Layout Inspector 查看 activity_mian 布局如下图所示:
使用 <merge> 标签需要注意一下几点:
-
因为 <merge> 并不是 View ,所以在通过 LayoutInflate.inflate() 方法渲染的时候,第二个参数必须指定一个父容器,而且第三个参数必须设置为 true ,也就是必须为 <merge> 下的视图指定一个父节点。
-
因为 <merge> 并不是View,所以在 <merge> 中设置的所有属性都是无效的。
-
<merge> 标签必须使用在根布局。
-
<ViewStub> 标签中的 layout 布局不能使用 <merge> 标签。
1.3 <ViewStub> 标签
<ViewStub> 标签与 <include> 标签一样可以用来引入一个外部布局,不同的是,<ViewStub> 引入的布局默认不会扩张,既不会占用显示也不会占用位置,从而在解析 layout 文件时节省 CPU 和 内存。
<ViewStub> 标签最大的优点是当需要时才会加载,使用它并不会影响 UI 初始化时的性能,各种不常用的布局像进度条、网络错误等都可以使用 <ViewStub> 标签,以减少内存的使用,加快渲染速度, <ViewStub> 是一个不可见的,实际上是把宽高设置为0的 View 。
官方文档:
https://developer.android.google.cn/training/improving-layouts/loading-ondemand.html
下面我们以显示网络错误提示页面为例来分析 <ViewStub> 标签的使用。
我们新建一个 network_error.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:id="@+id/root_network_error"
android:gravity="center">
<ImageView
android:id="@+id/iv_network_error"
android:layout_width="150dp"
android:layout_height="150dp"
android:src="@drawable/network_error" />
<Button
android:id="@+id/btn_reload"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:layout_below="@+id/iv_network_error"
android:layout_marginTop="20dp"
android:text="重新加载" />
</RelativeLayout>
在 activity_main.xml 通过 <ViewStub> 标签引用 network_error 布局,代码如下所示:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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=".MainActivity">
<Button
android:id="@+id/btn_show_network_error"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:text="显示网络异常提示" />
<Button
android:id="@+id/btn_hide_network_error"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_marginRight="10dp"
android:text="隐藏网络异常提示" />
<ViewStub
android:id="@+id/vs_network_error"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/btn_hide_network_error"
android:layout="@layout/network_error" />
</RelativeLayout>
在 MainActivity 中通过 findViewById(vs_network_error) 找到 ViewStub,通过stub.inflate() 展开 ViewStub,然后得到子 View,如下:
public class MainActivity extends Activity implements View.OnClickListener {
private View networkErrorView;
private ViewStub viewStub;
private Button btnReload;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
viewStub = findViewById(R.id.vs_network_error);
findViewById(R.id.btn_show_network_error).setOnClickListener(this);
findViewById(R.id.btn_hide_network_error).setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_show_network_error:
showNetworkError();
break;
case R.id.btn_hide_network_error:
hideNetworkError();
break;
case R.id.btn_reload:
Toast.makeText(this, "重新加载", Toast.LENGTH_SHORT).show();
break;
}
}
public void showNetworkError() {
// networkErrorView == null 的时候表示还没调用 ViewStub 的 inflate 方法。
if (networkErrorView == null && viewStub != null) {
// 调用 ViewStub 的 inflate 方法渲染 View,这个方法只用调用一次即可,
// 说明:当调用了 ViewStub 的 inflate 方法之后,ViewStub 的内容就会展开。
// 在需要的时候在调用,减少 xml 解析时间,节省内存
// 这里获取的 networkErrorView 就是 <ViewStub> 标签引用布局的额根节点(这里是 RelativeLayout )
networkErrorView = viewStub.inflate();
btnReload = networkErrorView.findViewById(R.id.btn_reload);
btnReload.setOnClickListener(this);
}
if (networkErrorView != null) {
networkErrorView.setVisibility(View.VISIBLE);
}
}
public void hideNetworkError() {
if (networkErrorView != null) {
networkErrorView.setVisibility(View.GONE);
}
}
}
在上面 showNetworkError() 中展开了 ViewStub,同时我们对 networkErrorView 进行了保存,这样下次不用继续 inflate,减少不必要的 infalte 。
上面展开 ViewStub 部分代码如下:
viewStub = findViewById(R.id.vs_network_error);
networkErrorView = viewStub.inflate(); // 展开 ViewStub布局,并返回其引用布局的根节点
也可以写成下面的形式:
viewStub = findViewById(R.id.vs_network_error);
viewStub.setVisibility(View.VISIBLE);// 展开ViewStub布局
networkErrorView = findViewById(R.id.root_network_error);// 获取ViewStub引用布局的根节点
注意
-
这里我对 ViewStub 的实例进行了一个非空判断,这是因为 ViewStub 在 XML 中定义的 id 只在一开始有效,一旦 ViewStub 中指定的布局加载之后,这个 id 也就失效了,那么此时 findViewById() 得到的值也会是空。
-
View 的可见性设置为 gone 后,在 inflate 时,这个View 及其子 View 依然会被解析的。使用 ViewStub 就能避免解析其中指定的布局文件,从而节省布局文件的解析时间,及内存的占用。
二、布局调优工具
2.1 Layout Inspector
使用 Android Studio 中的布局检查器,您可以将应用布局与设计模型进行比较、显示应用的放大视图或 3D 视图,以及在运行时检查应用布局的细节。如果布局是在运行时(而不是完全在 XML 中)构建的并且布局行为出现异常,该工具会非常有用。
使用布局验证,您可以在不同的设备和显示配置(包括可变字体大小或用户语言)上同时预览布局,以便轻松测试各种常见的布局问题。
下面基于 Android Studio 4.1.1 分析 Layout Inspector 的基本使用。
1. 打开Layout Inspector
(1) 在连接的设备或模拟器上运行应用。
(2) 一次点击 Tools -> Layout Inspector。
(3) 在显示的 Layout Inspector 对话框中,选择想要检查的应用进程。
Layout Inspector 显示内容说明
视图层次结构(Component Tree):显示当前界面的布局层次结构,支持折叠、收起、选中、右键调试视图等。
工具栏:调试进程选择,视图边界,实时更新等。
屏幕截图(Layout Display):按照应用布局在设备或模拟器上的显示效果呈现布局,并显示每个视图的布局边界。支持点击选中视图、右键调整视图、放大/缩小视图、3D视角等。
布局属性(Attributes):所选视图的布局属性。
2. 选择视图
如要选择某个视图,请在 Component Tree 或 Layout Display 中点击该视图。所选视图的所有布局属性都会显示在 Attributes 面板中。
如果布局包含重叠的视图,您可以选择不在最前面的视图,方法是在 Component Tree 中点击该视图,或者旋转布局(3D视图)并点击所需视图。
3. 隐藏布局边界 & 隐藏布局模板
Show Borders:显示/隐藏 布局的边界(也就是 View 的区域边界线),就像我们在开发者模式中打开了 View 绘制边界 一样。
Show View Label:显示布局的布局标签,比如上图的 "tvl" 它的布局标签就是TextView
。
4. 将应用布局与参考图叠加层进行比较
如需将应用布局与参考图像(如界面模型)进行比较,您可以在布局检查器中加载位图图像叠加层。
-
如需加载叠加层,请点击布局检查器顶部的 Load Overlay 图标 。系统会缩放叠加层以适合布局。
-
如需调整叠加层的透明度,请使用 Overlay Alpha 滑块。
-
如需移除叠加层,请点击 Clear Overlay 图标 。
5. 实时布局检查器
实时布局检查器可以在应用被部署到搭载 API 级别 29 或更高版本的设备或模拟器时,提供应用界面的完整实时数据分析
如需启用实时布局检查器,请依次转到 File > Settings > Experimental,勾选 Enable Live Layout Inspector 旁边的框,然后点击 Layout Display 上方 Live updates 旁边的复选框。如下图所示:
实时布局检查器包含动态布局层次结构,可随着设备上视图的变化更新 Component Tree 和 Layout Display。
此外,使用属性值解析堆栈,您可以调查资源属性值在源代码中的来源位置,并按照属性窗格中的超链接导航到其位置。如下图所示:
6. 3D视图
这个看起来很酷炫,可是很遗憾,我的设备并不支持。
3D 视图查看需要 API >= 29 .
下面摘抄 Google 官方文档描述3D视图的使用。
Layout Display 可在运行时对应用的视图层次结构进行高级 3D 可视化。如需使用该功能,只需在实时布局检查器窗口中点击相应布局,然后拖动鼠标旋转该布局即可。如需展开或收起布局的图层,请使用 Layer Spacing 滑块。
2.2 调试GPU过度绘制(Overdraw)
UI界面被多次不必要的重绘,就叫 overdraw。这是对 GPU 的浪费,在低端手机还有可能造成界面卡顿。
1. 如何检测是否发生了 overdraw
(1)在您的设备上,转到 Settings(设置) 并选择 Developer Options(开发者选项)。
(2)向下滚动到 Hardware accelerated rendering (硬件)部分,并选择 Debug GPU Overdraw(调试 GPU 过度绘制)。
(3) 在 Debug GPU overdraw (调试 GPU 过度绘制)对话框中,选择 Show overdraw areas(展示过度绘制区域)。
然后查看你的UI页面是否有下面的颜色块,不同颜色代表不同的绘制次数
2. overdraw 解决办法
-
移除不必要的 background,这是一种快速提升渲染性能的方式。
-
减少布局层级。
-
减少使用透明视图。
2.3 Hierarchy Viewer
Hierarchy Viewer 工具提供了一个可视化界面显示布局的层次结构,让我们可以进行调试,从而优化界面布局结构。
由于 Google 已经弃用该工具,这里就不做讲解,想了解的同学可以通过 Google 官方文档查看其使用教程。
https://developer.android.google.cn/studio/profile/hierarchy-viewer.html
2.4 Lint
Android Studio 提供了一个名为 lint 的代码扫描工具,可帮助您发现并更正代码结构质量的问题,而无需您实际执行应用,也不必编写测试用例。系统会报告该工具检测到的每个问题并提供问题的描述消息和严重级别,以便您可以快速确定需要优先进行的关键改进。此外,您还可以降低问题的严重级别以忽略与项目无关的问题,或者提高严重级别以突出特定问题。
这个工具也可以用来检测布局中存在的问题。
Google 官方文档地址:https://developer.android.google.cn/studio/write/lint?hl=zh_cn
扫描下方二维码关注公众号,获取更多技术干货。
本文地址:https://blog.csdn.net/lixiong0713/article/details/111867839