主頁 > 移動端開發 > Android 布局優化方案

Android 布局優化方案

2020-12-29 11:15:52 移動端開發

前言

在 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> 標簽需要注意一下幾點:

  1. 因為 <merge> 并不是 View ,所以在通過 LayoutInflate.inflate() 方法渲染的時候,第二個引數必須指定一個父容器,而且第三個引數必須設定為 true ,也就是必須為 <merge> 下的視圖指定一個父節點,

  2. 因為 <merge> 并不是View,所以在 <merge> 中設定的所有屬性都是無效的,

  3. <merge> 標簽必須使用在根布局,

  4. <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參考布局的根節點

注意

  1. 這里我對 ViewStub 的實體進行了一個非空判斷,這是因為 ViewStub 在 XML 中定義的 id 只在一開始有效,一旦 ViewStub 中指定的布局加載之后,這個 id 也就失效了,那么此時 findViewById() 得到的值也會是空,

  2. 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 TreeLayout 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 TreeLayout 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通用

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more