前言
在 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
掃描下方二維碼關注公衆號,獲取更多技術乾貨。