Android源码分析ViewStub源码解析

源码基于安卓8.0分析结果

  • ViewStub是一种不可见的并且大小为0的试图,它可以延迟到运行时才填充inflate 布局资源,当Viewstub设为可见或者是inflate的时候,就会填充布局资源,这个布局和普通的试图就基本上没有任何区别,比如说,加载网络失败,或者是一个比较消耗性能的功能,需要用户去点击才可以加载!从而这样更加的节约了性能。对安卓布局很友好!
  • ViewStub用法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <ViewStub
    android:padding="10dp"
    android:background="@color/colorPrimary"
    android:layout_gravity="center"
    android:inflatedId="@+id/view_stub_inflateid"
    android:id="@+id/view_stub"
    android:layout="@layout/view_stub_imageview"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />
  • 这篇文章安卓代码、图片、布局、网络和电量优化说如果这个根布局是个View,比如说是个ImagView,那么找出来的id为null,得必须注意这一点 —–2018.6.7修正这个说法,以前我说的是错误的,根本上的原因是ViewStub设置了 inflateid ,这才是更本身的原因

  • 在这里记住一点,如果在 ViewStub标签中设置了android:inflatedId="@+id/view_stub_inflateid",在layout布局中的根布局在设置android:id="@+id/view_stub_layout",这个id永远找出来都是为null的,原因会在下面说明

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <?xml version="1.0" encoding="utf-8"?>
    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:padding="10dp"
    android:id="@+id/view_stub_layout"
    android:src="@drawable/ic_launcher_background"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
    android:text="如果这个根布局是个View,比如说是个ImagView,那么找出来的id为null,得必须注意这一点 -----2018.6.7修正这个说法,以前我说的是错误的,根本上的原因是ViewStub设置了 inflateid ,这才是更本身的原因"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />
    <ImageView
    android:layout_marginTop="20dp"
    android:id="@+id/imageview"
    android:padding="10dp"
    android:src="@drawable/ic_launcher_background"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>
    </FrameLayout>
  • activity或者是fragment中的使用,mViewStub.getParent()==null就是说明没有被填充,需要填充,如果填充了,那么它的parent不会为null,具体的骚操作,后续我介绍View的绘制流程的时候在详细说明。

  • 第一种使用的方法

    1
    2
    3
    4
    5
     mViewStub = findViewById(R.id.view_stub);
    if (null!=mViewStub.getParent()){
    View inflate = mViewStub.inflate();
    ....
    }
  • 第二种方式:mViewStub.setVisibility(View.VISIBLE);inflate()方法一样。

    1
    2
    3
    4
    5
     mViewStub = findViewById(R.id.view_stub);
    if (null!=mViewStub.getParent()){
    mViewStub.setVisibility(View.VISIBLE);
    ....
    }
  • 第三种方式,my_title_parent_idlayout的根布局的id

    1
    2
    3
    4
    5
    6
    7
    8
    9
       mViewStub = findViewById(R.id.view_stub);
    // 成员变量commLv2为空则代表未加载 commLv2 的id为ViewStub中的根布局的id
    View commLv2=findViewById(R.id.my_title_parent_id);
    if ( commLv2 == null ) {
    // 加载评论列表布局, 并且获取评论ListView,inflate函数直接返回ListView对象
    commLv2 = (View)mViewStub.inflate();
    } else {
    // ViewStub已经加载
    }
  • ViewStub构造方法,注意获取了几个值mInflatedId就是android:inflatedId="@+id/find_view_stub"这个值, mLayoutResource就是layout的resId,ViewStub的 mIDid。可以看出ViewStubView的子类.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public final class ViewStub extends View {
    public ViewStub(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context);
    final TypedArray a = context.obtainStyledAttributes(attrs,
    R.styleable.ViewStub, defStyleAttr, defStyleRes);
    // TODO: 2018/5/23 ViewStub 中设置的标签id 如果设置了 这里就一定有值 mInflatedId!=NO_Id
    mInflatedId = a.getResourceId(R.styleable.ViewStub_inflatedId, NO_ID);
    mLayoutResource = a.getResourceId(R.styleable.ViewStub_layout, 0);
    mID = a.getResourceId(R.styleable.ViewStub_id, NO_ID);
    a.recycle();
    //不可见
    setVisibility(GONE);
    // 设置不绘制
    setWillNotDraw(true);
    }
    }
  • 在构造方法中:同时注意不可见 setVisibility(GONE); ,设置不绘制setWillNotDraw(true);,同时通过下面的方法看出,ViewStub 是一个大小为0的视图。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // 宽高都为0 onMeasure的时候 宽高都为0
    setMeasuredDimension(0, 0);
    }
    //todo 为啥这个控件 是个大小为0的控件 ,那是因为他妈的这里更不就没有画
    @Override
    public void draw(Canvas canvas) {
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
    }
  • 关于inflate()方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    public View inflate() {
    // 1、获取ViewStub的parent view,也是目标布局根元素的parent view
    final ViewParent viewParent = getParent();

    if (viewParent != null && viewParent instanceof ViewGroup) {
    if (mLayoutResource != 0) {
    final ViewGroup parent = (ViewGroup) viewParent;
    /// 2、加载目标布局 牛逼的方法
    final View view = inflateViewNoAdd(parent);
    // 3、将ViewStub自身从parent中移除
    replaceSelfWithView(view, parent);

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

    return view;
    } else {
    // TODO: 2018/5/23 必须设置布局的文件
    throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
    }
    } else {
    // TODO: 2018/5/23 iewParent instanceof ViewGroup 不属于的话,就好比在一个TextView创建一个ViewStub直接爆炸
    throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
    }
    }
  • 第一点,ViewStup也只能在ViewGroup中使用,不能在View中去使用,要不然会抛出异常IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");

  • 第二点,也必须设置layout属性,要不然也会抛出异常throw new IllegalArgumentException("ViewStub must have a valid layoutResource");;
  • 关于方法inflateViewNoAdd(parent);

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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);
    //和 LayoutInflater一个道理,设置了,ViewStub 引用进来的根布局的id找出来为null 非常有些意思
    if (mInflatedId != NO_ID) {
    view.setId(mInflatedId);
    }
    return view;
    }
  • 第一点,底层调用的还是LayoutInflater.from(mContext).inflate(mLayoutResource, parent, false);,Android源码分析(LayoutInflater.from(this).inflate(resId,null);源码解析),这里不做过多的说明

  • 第二点,又看到这个方法,似曾相识,对,这也是为什么ViewStub找不到根布局id的原因,因为mInflatedId != NO_ID,就会view.setId(mInflatedId);

    1
    2
    3
      if (mInflatedId != NO_ID) {
    view.setId(mInflatedId);
    }
  • 将ViewStub自身从parent中移除replaceSelfWithView(view, parent);,具体的原因,这里不做分析,因为有点小复杂,这里就大概明白就行,对于理解这个ViewStub不困难,哈哈

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    private void replaceSelfWithView(View view, ViewGroup parent) {
    final int index = parent.indexOfChild(this);
    // 3、将ViewStub自身从parent中移除
    parent.removeViewInLayout(this);

    final ViewGroup.LayoutParams layoutParams = getLayoutParams();
    if (layoutParams != null) {
    // 4、将目标布局的根元素添加到parent中,有参数
    parent.addView(view, index, layoutParams);
    } else {
    // 5、将目标布局的根元素添加到parent中
    parent.addView(view, index);
    }
    }
  • 这里使用到了弱引用,只有弱引用指向的对象的生命周期更短,当垃圾回收器扫描到只有具有弱引用的对象的时候,不论当前空间是否不足,都会对弱引用对象进行回收,当然弱引用也可以和一个队列配合着使用,为了更好地释放内存,安卓代码、图片、布局、网络和电量优化这篇文章有很好的解释,而且这个mInflatedViewRef只在这里初始化,如果说没有调用inflate的方法的话,这个对象一定为null;

    1
    2
    3
    4
    5
    6
    //更好的释放内存
    private WeakReference<View> mInflatedViewRef;
    mInflatedViewRef = new WeakReference<>(view);
    if (mInflateListener != null) {
    mInflateListener.onInflate(this, view)
    }
  • 为啥setVisibility(View.VISIBLE)等同于inflate,原因是ViewStub进行了重写。可以看出代码的逻辑,只要没有调用过,inflate()方法,setVisibility(VISIBLE )setVisibility(INVISIBLE)这个两个参数走的方法一样,只不过,一个看不到,实际上的位置已经确定了(INVISIBLE)。但是如果调用多次的话setVisibility()记得也得判断下null!=mViewStub.getParent()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @Override
    @android.view.RemotableViewMethod(asyncImpl = "setVisibilityAsync")
    public void setVisibility(int visibility) {
    // TODO: 2018/5/23 弱引用的使用
    //如果已经加载过则只设置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();// 调用inflate来加载目标布局
    }
    }
    }
  • 贴出全部的代码,有空的话,可以研究下。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    @RemoteView
    public final class ViewStub extends View {
    private int mInflatedId;
    private int mLayoutResource;
    // TODO: 2018/5/23 弱引用:弱引用是比软引用更弱的一种的引用的类型,
    // 只有弱引用指向的对象的生命周期更短,当垃圾回收器扫描到只有具有弱引用的对象的时候,
    // 不敢当前空间是否不足,都会对弱引用对象进行回收,当然弱引用也可以和一个队列配合着使用

    //更好的释放内存
    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);
    // TODO: 2018/5/23 ViewStub 中设置的标签id 如果设置了 这里就一定有值 mInflatedId!=NO_Id
    mInflatedId = a.getResourceId(R.styleable.ViewStub_inflatedId, NO_ID);
    mLayoutResource = a.getResourceId(R.styleable.ViewStub_layout, 0);
    mID = a.getResourceId(R.styleable.ViewStub_id, NO_ID);
    a.recycle();
    //不可见
    setVisibility(GONE);
    // 设置不绘制
    setWillNotDraw(true);
    }

    /**
    * Returns the id taken by the inflated view. If the inflated id is
    * {@link View#NO_ID}, the inflated view keeps its original id.
    *
    * @return A positive integer used to identify the inflated view or
    * {@link #NO_ID} if the inflated view should keep its id.
    *
    * @see #setInflatedId(int)
    * @attr ref android.R.styleable#ViewStub_inflatedId
    */
    @IdRes
    public int getInflatedId() {
    return mInflatedId;
    }

    /**
    * Defines the id taken by the inflated view. If the inflated id is
    * {@link View#NO_ID}, the inflated view keeps its original id.
    *
    * @param inflatedId A positive integer used to identify the inflated view or
    * {@link #NO_ID} if the inflated view should keep its id.
    *
    * @see #getInflatedId()
    * @attr ref android.R.styleable#ViewStub_inflatedId
    */
    @android.view.RemotableViewMethod(asyncImpl = "setInflatedIdAsync")
    public void setInflatedId(@IdRes int inflatedId) {
    mInflatedId = inflatedId;
    }

    /** @hide **/
    public Runnable setInflatedIdAsync(@IdRes int inflatedId) {
    mInflatedId = inflatedId;
    return null;
    }

    /**
    * Returns the layout resource that will be used by {@link #setVisibility(int)} or
    * {@link #inflate()} to replace this StubbedView
    * in its parent by another view.
    *
    * @return The layout resource identifier used to inflate the new View.
    *
    * @see #setLayoutResource(int)
    * @see #setVisibility(int)
    * @see #inflate()
    * @attr ref android.R.styleable#ViewStub_layout
    */
    @LayoutRes
    public int getLayoutResource() {
    return mLayoutResource;
    }

    /**
    * Specifies the layout resource to inflate when this StubbedView becomes visible or invisible
    * or when {@link #inflate()} is invoked. The View created by inflating the layout resource is
    * used to replace this StubbedView in its parent.
    *
    * @param layoutResource A valid layout resource identifier (different from 0.)
    *
    * @see #getLayoutResource()
    * @see #setVisibility(int)
    * @see #inflate()
    * @attr ref android.R.styleable#ViewStub_layout
    */
    @android.view.RemotableViewMethod(asyncImpl = "setLayoutResourceAsync")
    public void setLayoutResource(@LayoutRes int layoutResource) {
    mLayoutResource = layoutResource;
    }

    /** @hide **/
    public Runnable setLayoutResourceAsync(@LayoutRes int layoutResource) {
    mLayoutResource = layoutResource;
    return null;
    }

    /**
    * Set {@link LayoutInflater} to use in {@link #inflate()}, or {@code null}
    * to use the default.
    */
    public void setLayoutInflater(LayoutInflater inflater) {
    mInflater = inflater;
    }

    /**
    * Get current {@link LayoutInflater} used in {@link #inflate()}.
    */
    public LayoutInflater getLayoutInflater() {
    return mInflater;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // 宽高都为0 onMeasure的时候 宽高都为0
    setMeasuredDimension(0, 0);
    }
    //todo 为啥这个控件 是个大小为0的控件 ,那是因为他妈的这里更不就没有画
    @Override
    public void draw(Canvas canvas) {
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
    }

    /**
    * 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) {
    // TODO: 2018/5/23 弱引用的使用
    //如果已经加载过则只设置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();// 调用inflate来加载目标布局
    }
    }
    }

    /** @hide **/
    public Runnable setVisibilityAsync(int visibility) {
    if (visibility == VISIBLE || visibility == INVISIBLE) {
    ViewGroup parent = (ViewGroup) getParent();
    return new ViewReplaceRunnable(inflateViewNoAdd(parent));
    } else {
    return null;
    }
    }

    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);
    //和 LayoutInflater一个道理,设置了,ViewStub 引用进来的根布局的id找出来为null 非常有些意思
    if (mInflatedId != NO_ID) {
    view.setId(mInflatedId);
    }
    return view;
    }

    // TODO: 2018/5/23 关注他
    private void replaceSelfWithView(View view, ViewGroup parent) {
    final int index = parent.indexOfChild(this);
    // 3、将ViewStub自身从parent中移除
    parent.removeViewInLayout(this);

    final ViewGroup.LayoutParams layoutParams = getLayoutParams();
    if (layoutParams != null) {
    // 4、将目标布局的根元素添加到parent中,有参数
    parent.addView(view, index, layoutParams);
    } else {
    // 5、将目标布局的根元素添加到parent中
    parent.addView(view, index);
    }
    }

    /**
    * 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() {
    // 1、获取ViewStub的parent view,也是目标布局根元素的parent view
    final ViewParent viewParent = getParent();

    if (viewParent != null && viewParent instanceof ViewGroup) {
    if (mLayoutResource != 0) {
    final ViewGroup parent = (ViewGroup) viewParent;
    /// 2、加载目标布局 牛逼的方法
    final View view = inflateViewNoAdd(parent);
    // 3、将ViewStub自身从parent中移除
    replaceSelfWithView(view, parent);

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

    return view;
    } else {
    // TODO: 2018/5/23 必须设置布局的文件
    throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
    }
    } else {
    // TODO: 2018/5/23 iewParent instanceof ViewGroup 不属于的话,就好比在一个TextView创建一个ViewStub直接爆炸
    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 ViewStub.OnInflateListener
    */
    public void setOnInflateListener(OnInflateListener inflateListener) {
    mInflateListener = inflateListener;
    }

    /**
    * Listener used to receive a notification after a ViewStub has successfully
    * inflated its layout resource.
    *
    * @see ViewStub#setOnInflateListener(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);
    }

    /** @hide **/
    public class ViewReplaceRunnable implements Runnable {
    public final View view;

    ViewReplaceRunnable(View view) {
    this.view = view;
    }

    @Override
    public void run() {
    replaceSelfWithView(view, (ViewGroup) getParent());
    }
    }
    }
  • 最后做了一张图
    ViewStub源码解析.jpg

  • 说明一下
    • ViewStub的原理很简单!好吧,这个有点皮

 上一篇
Android源码分析LayoutInflater Android源码分析LayoutInflater
源码基于安卓8.0分析结果 在这篇文章Android源码分析(Activity.setContentView源码解析),分析得出,底层走的就是LayoutInflater.from(this),inflate(),如果inflate()传入
2018-06-07 Shiming_Li
下一篇 
Android源码分析(Activity.setContentView源码解析) Android源码分析(Activity.setContentView源码解析)
源码基于安卓8.0分析结果 为什么要写这篇文章?其实是给这个LayoutInflater类铺垫的,要解释这个LayoutInflater源码,就必须知道到底怎么调用的,包括include、merge、ViewStub和原理,如何自己撸一个大
2018-05-29 Shiming_Li
  目录