ViewStub源码分析

  ViewStub是一种特殊的View,Android官方给出的解释是:一种不可见的(GONE)、size是0的占位view,多用于运行时html

延迟加载的,也就是说真正须要某个view的时候。在实际项目中,我发现它试用的场景大致有2种:android

1. 某种只第一次须要显示的view,好比某个介绍性的东西,好比用户触发了某些操做,给他弹出一个使用介绍,canvas

这种介绍只出现一次,以后的操做中不会再出现,因此这种状况下能够先用ViewStub来占个位,等介绍的view真正服务器

须要的时候才inflate,通常状况下这种view的建立也比较expensive;less

2. 某种直到运行时某一刻的时候才知道应该加载哪一个view,好比你可能须要根据服务器返回的某些数据来决定是显示View 1ide

仍是显示View 2,这种状况下,也能够先用ViewStub占位,直到能够作决定了在inflate相应的view;布局

为了接下来的分析方便,这里我先贴一段ViewStub的xml代码,以下:性能

<ViewStub 
    android:id="@+id/stub"
    android:inflatedId="@+id/realView" // 可设置,不过通常意义不大
    android:layout="@layout/my_real_view"
    android:visibility="GONE" // really no need, don't write useless code
    android:layout_width="match_parent"
    android:layout_height="40dip" />

在代码里你一样能够经过findViewById(R.id.stub)来找到此ViewStub,当真正的"my_real_view"布局被inflate以后,优化

ViewStub这个控件就被从view层次结构中删除了,取而代之的就是"my_real_view"表明的布局。注意下,这里的全部this

layout_width/height都是用来控制inflate以后的view的,而不是给ViewStub用的,这个多是Android系统里惟一的例外。

在咱们的Java代码里使用的方式,官方推荐的作法是:

ViewStub stub = (ViewStub) findViewById(R.id.stub);
View inflated = stub.inflate();

但这里有一点须要留意的就是findViewById(R.id.stub),由于咱们知道stub在第一次被inflate以后就会从view层次结构中

删除,因此你应该要确保这样的findViewById只会被调用一次(通常都是这样的状况),但在有些你没想到的case下

可能也会被调用屡次。前几天咱们在项目里就遇到了这个状况,出现了NPE,最终调查发现是stub的findViewById被又调用

了一次,致使了NPE(由于层次结构中已经找不到这个id了)。好的作法是好比把这种findViewById的放在onCreate相似

的只会被执行一次的地方,不然你就应该格外当心,本身加些保护处理。

  说了这么多,接下来能够切入正题了,废话很少说,从ctor开始上代码:

    public ViewStub(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        TypedArray a = context.obtainStyledAttributes(
                attrs, com.android.internal.R.styleable.ViewStub, defStyleAttr, defStyleRes);

        mInflatedId = a.getResourceId(R.styleable.ViewStub_inflatedId, NO_ID); // 从layout文件中读取inflateId的值,若是有的话
        mLayoutResource = a.getResourceId(R.styleable.ViewStub_layout, 0); // 一样的,读取真正的layout文件

        a.recycle();

        a = context.obtainStyledAttributes(
                attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);
        mID = a.getResourceId(R.styleable.View_id, NO_ID); // 读取View_id属性
        a.recycle();

        initialize(context);
    }

    private void initialize(Context context) {
        mContext = context;
        setVisibility(GONE); // 看到这里,你就明白了我前面xml文件中提到的不必手动指定android:visibility="GONE"的缘由
        setWillNotDraw(true); // 由于ViewStub只是个临时占位的,它不作任何绘制操做,因此打开这个标志位以优化绘制过程,提升性能
    }

  接下来,咱们看几个相关的比较简单的方法,以下:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(0, 0); // 实现很简单,写死的大小为0,0
    }

    @Override
    public void draw(Canvas canvas) { // 不作任何绘制
    }

    @Override
    protected void dispatchDraw(Canvas canvas) { // 没啥须要dispatch的
    }

    /**
     * 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
    public void setVisibility(int visibility) { // setVisibility作了些特殊处理
        if (mInflatedViewRef != null) { // 真正View的弱引用
            View view = mInflatedViewRef.get();
            if (view != null) {
                view.setVisibility(visibility);
            } else {
                throw new IllegalStateException("setVisibility called on un-referenced view");
            }
        } else { // 当还没inflate的时候,若是调了setVisibility方法,若是不是GONE的状况会自动在这种状况下调inflate的
            super.setVisibility(visibility);
            if (visibility == VISIBLE || visibility == INVISIBLE) {
                inflate(); // 非GONE的状况,会多调用inflate()方法,因此你拿到ViewStub的实例后既能够主动调
            }              // inflate()方法也能够经过setVisiblity(VISIBLE/INVISIBLE)来触发对inflate的间接调用
        }
    }

  最后,咱们来看看真正关键的方法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() {
        final ViewParent viewParent = getParent();

        if (viewParent != null && viewParent instanceof ViewGroup) {
            if (mLayoutResource != 0) {
                final ViewGroup parent = (ViewGroup) viewParent;
                final LayoutInflater factory;
                if (mInflater != null) {
                    factory = mInflater;
                } else {
                    factory = LayoutInflater.from(mContext);
                }
                final View view = factory.inflate(mLayoutResource, parent,
                        false); // 注意这行,实际上仍是调用了LayoutInflater的inflate方法,注意最后一个参数是false,因此这里返回的
                                // view就是真正layout文件里的root view而不是parent了。
                if (mInflatedId != NO_ID) {
                    view.setId(mInflatedId); // 若是layout文件里有给真正的view指定id就在这里设置上
                }

                final int index = parent.indexOfChild(this);
                parent.removeViewInLayout(this); // 找到ViewStub在parent中的位置并删除ViewStub,看到了,在inflate的过程当中
                                                 // ViewStub真的是被删掉了!!!
                final ViewGroup.LayoutParams layoutParams = getLayoutParams(); // 获取LayoutParams,就是你在xml文件中写的
// layout_xxx这样的属性,虽然其实是属于ViewStub的,
if (layoutParams != null) { // 但下面紧接着就用这个LayoutParams来添加真正的view了 parent.addView(view, index, layoutParams); // 注意下添加的位置就是原先ViewStub待的位置,
// 因此等于就是完彻底全替换了 }
else { parent.addView(view, index); // 若是layoutParams是null的case } mInflatedViewRef = new WeakReference<View>(view); // 用view初始化弱引用 if (mInflateListener != null) { // 经过mInflateListener将inflate完成事件通知出去。。。 mInflateListener.onInflate(this, view); } return view; // 返回真正的view } else { // ViewStub必须指定真正的layout文件!!! throw new IllegalArgumentException("ViewStub must have a valid layoutResource"); } } else { // ViewStub也必须有个parent view的!!! throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent"); } } /** * Specifies the inflate listener to be notified after this ViewStub successfully * inflated its layout resource. * * @param inflateListener The OnInflateListener to notify of successful inflation. * * @see android.view.ViewStub.OnInflateListener */ public void setOnInflateListener(OnInflateListener inflateListener) { mInflateListener = inflateListener; // 技术上要想响应inflate完成事件,有2种途径,这里经过设置listener是一种方式, } // 也能够经过更generic的方式:override真正view的onFinishInflate方法。 /** * Listener used to receive a notification after a ViewStub has successfully * inflated its layout resource. * * @see android.view.ViewStub#setOnInflateListener(android.view.ViewStub.OnInflateListener) */ public static interface OnInflateListener { /** * Invoked after a ViewStub successfully inflated its layout resource. * This method is invoked after the inflated view was added to the * hierarchy but before the layout pass. * * @param stub The ViewStub that initiated the inflation. * @param inflated The inflated View. */ void onInflate(ViewStub stub, View inflated); } }

  ViewStub的使用也被Android归到提升性能里面,之因此这么说一方面是由于ViewStub有延迟加载(直到须要的时候才inflate

真正的view)、先占位的做用,另一方面也是因为对measure、layout、draw这3个关键过程的优化,由于ViewStub的这3个过程

很简单,或者根本就是do nothing,因此性能很好。综上,Android才建议咱们用ViewStub来优化layout性能,固然也是真正合适使用

ViewStub的时机,而不是要你在全部的layout文件中都这么作,这个时机在开始的时候我也提到了,另外也能够参考下Android官方的文章:

http://developer.android.com/training/improving-layouts/loading-ondemand.html

http://developer.android.com/reference/android/view/ViewStub.html

 

P.S. 这篇文章原本应该是在上周末完成的,这周末去千岛湖outing,在酒店里实在无聊,发现还有篇未完成的文章就在这里补充下,enjoy。。。 

相关文章
相关标签/搜索