前言
在 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://www.uj5u.com/yidong/241870.html
標籤:其他
上一篇:安卓學期總結
下一篇:android 使用Kotlin operator 泛型屬性委托配合DataBinding,實作2個委托類,全域binding通用
