理解 ViewStub 原理

本人只是Android小菜一个,写技术文档只是为了总结本身在最近学习到的知识,历来不敢为人师,若是里面有些不正确的地方请你们尽情指出,谢谢!java

本文基于原生Android 9.0源码来解析 ViewStub的实现原理android

android/view/ViewStub.java
android/view/View.java
复制代码

1. 概述

在进行Android程序开发时,除了要实现基本功能外,还要关注程序的性能,例如使用更少的内存、消耗更少的电量、更快地响应用户操做以及更快地启动显示等等。这个特色注定在咱们平时工做中,有很大一部分精力都在进行性能优化,其中一个优化方向就是让程序在尽量短的时间内启动并显示,让用户感受不到延迟,保证良好的用户体验。canvas

“懒加载”就是为了让程序尽量快地启动而提出的一个优化策略,即让那些对用户不重要或者不须要当即显示的布局控件作延迟加载,只在须要显示的时候才进行加载,这样就可让程序在启动显示的过程当中加载更少的控件,占用更少的内存空间,从而更快启动并显示。“懒加载”策略的具体实现方式多种多样,Android系统也提供了一种用于实现布局控件懒加载的工具ViewStub,它可以让相关布局在显示时再进行加载,从而提高程序启动速度。性能优化

本文先简单介绍ViewStub的使用方法,再介绍其实现“懒加载”的原理,以帮助你们加深对它的理解。app

2. ViewStub 使用方法

在讲解ViewStub的使用方法前,按照惯例,咱们仍是先来看看它的声明:框架

/** * A ViewStub is an invisible, zero-sized View that can be used to lazily inflate * layout resources at runtime. * * When a ViewStub is made visible, or when {@link #inflate()} is invoked, the layout resource * is inflated. The ViewStub then replaces itself in its parent with the inflated View or Views. * Therefore, the ViewStub exists in the view hierarchy until {@link #setVisibility(int)} or * {@link #inflate()} is invoked. * ...... */
public final class ViewStub extends View { ... }
复制代码

从这段介绍中能够知道ViewStub是一个不可见而且大小为0的控件,其做用就是用来实现布局资源的“懒加载”,当调用setVisibility()或者inflate()时,和ViewStub相关的布局资源就会被加载并在控件层级结构中替代ViewStub,同时ViewStub会从控件层级结构中移除,再也不存在。async

如今再来看下它的使用方法,首先须要在布局文件配置:ide

<ViewStub android:id="@+id/view_stub_id" android:layout="@layout/view_stub" android:inflatedId="@+id/view_stub_id" android:layout_width="200dp" android:layout_height="50dp" />
复制代码

因为ViewStub是直接继承自View的,因此它在xml里的基本使用方法和其余控件是同样的,只是有一些重要属性须要注意,其中android:layout指的是真正须要加载的布局资源,android:inflatedId指的是布局资源被加载后的View ID,总结以下:函数

属性 含义/做用 属性级别 是否可选
android:id ViewStub 在布局文件中的ID,用于在代码中访问。 View 共有 必写
android:layout 在显示 ViewStub 时真正加载并显示的布局文件 ViewStub 特有 必写
android:inflatedId 真正布局文件加载后建立的控件ID ViewStub 特有 可选

xml中定义ViewStub后就能够在代码里直接使用并根据具体业务逻辑在须要显示的时候对其进行加载:工具

ViewStub viewStub = (ViewStub) findViewById(R.id.view_stub_id);
if (viewStub != null) {
    // 调用 inflate 加载真正的布局资源并返回建立的 View 对象
    View inflatedView = viewStub.inflate();
    // 在获得真正的 View 对象后,就能够和直接加载的控件同样使用了。
    TextView textView = inflatedView.findViewById(R.id.view_stub_textview);
}
复制代码

ViewStub的“懒加载”能起多大效果,取决因而否能在最合适的时机显示它,因为每一个模块的业务逻辑不一样,其最合适的显示时机也不相同,但基本的原则就是在使用它的前一刻进行加载,这才能使ViewStub的"懒加载"做用最大化,也才能使性能最好。

3. ViewStub 原理解析

3.1 构造过程

前面在ViewStub的声明中看到它是一个不可见且大小为0的控件,那么是如何作到这点的呢?首先看下它的构造过程:

public final class ViewStub extends View {
    // 在 xml 中定义的 android:inflatedId 值,用于加载后的 View Id。
    private int mInflatedId;
    // 在 xml 中定义的 android:layout 值,是须要真正加载的布局资源。
    private int mLayoutResource;
    // 保存布局建立的 View 弱引用,方便在 setVisibility() 函数中使用。
    private WeakReference<View> mInflatedViewRef;

    // 布局加载器
    private LayoutInflater mInflater;
    // 布局加载回调接口,默认为空。
    private OnInflateListener mInflateListener;

    public ViewStub(Context context) {
        this(context, 0);
    }

    /** * Creates a new ViewStub with the specified layout resource. * * @param context The application's environment. * @param layoutResource The reference to a layout resource that will be inflated. */
    public ViewStub(Context context, @LayoutRes int layoutResource) {
        this(context, null);

        mLayoutResource = layoutResource;
    }

    public ViewStub(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ViewStub(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public ViewStub(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context);

        final TypedArray a = context.obtainStyledAttributes(attrs,
                R.styleable.ViewStub, defStyleAttr, defStyleRes);
        // 获取 xml 中定义的 android:inflatedId 和 android:layout 属性值
        mInflatedId = a.getResourceId(R.styleable.ViewStub_inflatedId, NO_ID);
        mLayoutResource = a.getResourceId(R.styleable.ViewStub_layout, 0);
        // 获取 ViewStub 在 xml 中定义的 id 值
        mID = a.getResourceId(R.styleable.ViewStub_id, NO_ID);
        a.recycle();

        // 设置 ViewStub 控件的显示属性,直接设置为不显示。
        setVisibility(GONE);
        // 设置 ViewStub 不进行绘制
        setWillNotDraw(true);
    }
    
    ...
    
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 设置宽高都为 0
        setMeasuredDimension(0, 0);
    }

    @Override
    public void draw(Canvas canvas) {
        // 空方法,不进行任何绘制。
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
    }
}
复制代码

ViewStub的构造过程比较简单,相信很容易看懂,小菜也在代码的关键点都增长了注释,从中能够发现几点关键信息:

  1. ViewStub的不可见性:在ViewStub的构造函数中,利用setVisibility(GONE)将可见性设置为不可见,因此不管在 xml里如何设置,都是不可见的。
  2. ViewStub的不绘制性:在 ViewStub的构造函中,利用setWillNotDraw(true)使其不进行绘制而且把draw()实现为空方法,这些都保证了ViewStub在加载的时候并不会进行实际的绘制工做。
  3. ViewStub的零大小性:在onMeasure()中把宽高都直接指定为0,保证了其大小为0。

正是因为ViewStub的这些特性,其加载过程几乎不须要时间,能够认为它的存在不会对相关程序的启动产生影响。

3.2 懒加载过程

前面提到:在调用inflate()或者setVisibility()时,ViewStub才会加载真正的布局资源并在控件层级结构中替换为真正的控件,同时ViewStub从控件层级结构中移除,这是“懒加载”的核心思想,是如何实现的呢?既然是调用inflate()setVisibility(),就直接分析相关代码。

首先看下inflate()函数代码:

/** * Inflates the layout resource identified by {@link #getLayoutResource()} * and replaces this StubbedView in its parent by the inflated layout resource. * * @return The inflated layout resource. * */
public View inflate() {
    // 获取 ViewStub 在控件层级结构中的父控件。
    final ViewParent viewParent = getParent();

    if (viewParent != null && viewParent instanceof ViewGroup) {
        if (mLayoutResource != 0) {
            final ViewGroup parent = (ViewGroup) viewParent;
            // 根据 android:layout 指定的 mLayoutResource 加载真正的布局资源,渲染成 View 对象。
            final View view = inflateViewNoAdd(parent);
            // 在控件层级结构中把 ViewStub 替换为新建立的 View 对象。
            replaceSelfWithView(view, parent);

            // 保存 View 对象的弱引用,方便其余地方使用。
            mInflatedViewRef = new WeakReference<>(view);
            // 渲染回调,默认不存在。
            if (mInflateListener != null) {
                mInflateListener.onInflate(this, view);
            }
            // 返回新建立的 View 对象
            return view;
        } else {
            // 若是没有在 xml 指定 android:layout 会走到这个路径,因此 android:layout 是必须指定的。
            throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
        }
    } else {
        // 在第一次调用 inflate() 后,ViewStub 会从控件层级结构中移除,再也不有父控件,
        // 此后再调用 inflate() 会走到这个路径,因此 inflate() 只能调用一次。
        throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
    }
}

private View inflateViewNoAdd(ViewGroup parent) {
    // 获取布局渲染器
    final LayoutInflater factory;
    if (mInflater != null) {
        factory = mInflater;
    } else {
        factory = LayoutInflater.from(mContext);
    }
    // 把真正须要加载的布局资源渲染成 View 对象。
    final View view = factory.inflate(mLayoutResource, parent, false);
    // 若是在 xml 中指定 android:inflatedId 就设置到新建立的 View 对象中,能够不指定。
    if (mInflatedId != NO_ID) {
        view.setId(mInflatedId);
    }
    return view;
}

private void replaceSelfWithView(View view, ViewGroup parent) {
    final int index = parent.indexOfChild(this);
    // 把 ViewStub 从控件层级中移除。
    parent.removeViewInLayout(this);

    // 把新建立的 View 对象加入控件层级结构中,而且位于 ViewStub 的位置,
    // 而且在这个过程当中,会使用 ViewStub 的布局参数,例如宽高等。
    final ViewGroup.LayoutParams layoutParams = getLayoutParams();
    if (layoutParams != null) {
        parent.addView(view, index, layoutParams);
    } else {
        parent.addView(view, index);
    }
}
复制代码

inflate()函数详细地展现了如何渲染布局资源以及如何在控件层级结构中把ViewStub替换为新建立的View对象,代码比较简单,小菜也在关键地方增长了注释。

再来看下setVisibility()函数代码:

/** * When visibility is set to {@link #VISIBLE} or {@link #INVISIBLE}, * {@link #inflate()} is invoked and this StubbedView is replaced in its parent * by the inflated layout resource. After that calls to this function are passed * through to the inflated view. * * @param visibility One of {@link #VISIBLE}, {@link #INVISIBLE}, or {@link #GONE}. * * @see #inflate() */
@Override
@android.view.RemotableViewMethod(asyncImpl = "setVisibilityAsync")
public void setVisibility(int visibility) {
    if (mInflatedViewRef != null) {
        // 若是已经调用过 inflate() 函数,mInflatedViewRef 会保存新建立 View 对象的弱引用,
        // 此时直接更新其可见性便可。
        View view = mInflatedViewRef.get();
        if (view != null) {
            view.setVisibility(visibility);
        } else {
            throw new IllegalStateException("setVisibility called on un-referenced view");
        }
    } else {
        // 若是没有调用过 inflate() 函数就会走到这个路径,会在设置可见性后直接调用 inflate() 函数。
        super.setVisibility(visibility);
        if (visibility == VISIBLE || visibility == INVISIBLE) {
            inflate();
        }
    }
}
复制代码

setVisibility()的代码逻辑也很简单:若是inflate()已经被调用过就直接更新控件可见性,不然更新可见性并调用inflate()加载真正的布局资源,渲染成 View 对象。

4. 总结

一些布局控件在开始时并不须要显示,在程序启动后再根据业务逻辑进行显示,一般的作法是在 xml中将其定义为不可见,而后在代码中经过setVisibility()更新其可见性,可是这样作会对程序性能产生不利影响,由于虽然该控件的初始状态是不可见,但仍然会在程序启动时进行建立和绘制,增长了程序的启动时间。正是因为这种状况的存在,Android系统提供了ViewStub框架,可以很容易实现“懒加载”以提高程序性能,本文从“使用方法”和“实现原理”两个方面对其进行讲解,但愿能对你们有所帮助。

相关文章
相关标签/搜索