性能优化-布局

2022-04-12  本文已影响0人  慎独静思

布局优化是我们开发过程中最常见和最容易改善的地方,所以我们先从它开始入手。
开始详细分析之前,我们先说一下对布局优化的认识。

布局优化的目的是为了尽量减少布局层级,常用的方法包括:推荐使用相对布局,约束布局等构建布局文件,ViewStub, Merge, include。

减少布局层级的目的是什么呢?是为了减少View树的深度,相应的也就减少了测量和绘制的工作量。

如果布局中既可以使用LinearLayout也可以使用RelativeLayout,那么就采用LinearLayout,因为RelativeLayout功能复杂,布局需要花费更多CPU。如果功能需要嵌套才能完成,还是建议使用RelativeLayout。

<include>

开发过程中,我们可以提取布局文件中可复用的部分独立成一个xml文件,使用include进行复用。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/app_bg"
    android:gravity="center_horizontal">

    <include layout="@layout/titlebar"/>

    <TextView android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:text="@string/hello"
              android:padding="10dp" />

    ...

</LinearLayout>

include支持的属性不多,如果要声明其他属性,需要同时指定layout_width和layout_height,如果include标签不指定属性,则默认采用要include 布局的root view的属性。
如果include标签指定属性,则include的属性会覆盖 root view的属性。
比如:root view是gone的,而include指定为visible,则布局是visible。
具体原理可参考源码LayoutInflater#parseInclude

<merge>

merge标签在被引入布局中时会被忽略,也就是说相比于直接include普通布局文件,会少一层。
但是merge标签中的子控件,会被直接放在布局中,用来替换include。

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
    <Button
        android:layout_height="wrap_content"
        android:layout_width="wrap_content"
        android:text="btn"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>
</merge>

其中tools:parentTag可以让你在预览时查看它在父布局中显示样式。

ViewStub

ViewStub是一种延时加载的技术,只有在真正需要的时候才去加载布局文件。对于复杂且很少使用的布局来说,此种方式很有效,比如网络异常布局。

<ViewStub
    android:id="@+id/stub_import"
    android:inflatedId="@+id/panel_import"
    android:layout="@layout/progress_overlay"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="bottom" />

通过inflate或者setVisibility可以加载ViewStub声明的布局。

findViewById(R.id.stub_import).setVisibility(View.VISIBLE);
// or
View importPanel = ((ViewStub) findViewById(R.id.stub_import)).inflate();

一旦setVisibility或inflate调用,ViewStub会被inflated布局替换,并且inflatedId就是inflated布局文件root view的ID,它会覆盖布局文件中定义的root view ID。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(0, 0);
    }

    @Override
    public void draw(Canvas canvas) {
    }

从源码中看ViewStub的宽高为0,且不参与绘制过程。

    public View inflate() {
        final ViewParent viewParent = getParent();

        if (viewParent != null && viewParent instanceof ViewGroup) {
            if (mLayoutResource != 0) {
                final ViewGroup parent = (ViewGroup) viewParent;
                final View view = inflateViewNoAdd(parent);
                replaceSelfWithView(view, parent);

                mInflatedViewRef = new WeakReference<>(view);
                if (mInflateListener != null) {
                    mInflateListener.onInflate(this, view);
                }

                return view;
            } else {
                throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
            }
        } else {
            throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
        }
    }

ViewStub必须在ViewGroup中使用。
ViewStub的inflate方法只能被触发一次,否则会抛异常。
因为ViewStub#inflate从ViewGroup中移除了,导致第二次inflate时ViewGroup为空。

    private View inflateViewNoAdd(ViewGroup parent) {
        final LayoutInflater factory;
        if (mInflater != null) {
            factory = mInflater;
        } else {
            factory = LayoutInflater.from(mContext);
        }
        final View view = factory.inflate(mLayoutResource, parent, false);

        if (mInflatedId != NO_ID) {
            view.setId(mInflatedId);
        }
        return view;
    }

可以看到,使用parent inflate布局之后会重新set id为inflatedId。

    private void replaceSelfWithView(View view, ViewGroup parent) {
        final int index = parent.indexOfChild(this);
        parent.removeViewInLayout(this);

        final ViewGroup.LayoutParams layoutParams = getLayoutParams();
        if (layoutParams != null) {
            parent.addView(view, index, layoutParams);
        } else {
            parent.addView(view, index);
        }
    }

使用 inflated view 替换ViewStub在parent view中的位置。

    public void setVisibility(int visibility) {
        if (mInflatedViewRef != null) {
            View view = mInflatedViewRef.get();
            if (view != null) {
                view.setVisibility(visibility);
            } else {
                throw new IllegalStateException("setVisibility called on un-referenced view");
            }
        } else {
            super.setVisibility(visibility);
            if (visibility == VISIBLE || visibility == INVISIBLE) {
                inflate();
            }
        }
    }

setVisibility如果设置为VISIBLE或INVISIBLE实际也是调用的inflate方法。

在DataBinding中使用

ViewStub viewStub = binding.viewStub.getViewStub();
viewStub.inflate();
上一篇下一篇

猜你喜欢

热点阅读