ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

Android开发知识(二十二)LayoutInflater装载xml布局过程的源码解析

2019-03-07 11:51:20  阅读:247  来源: 互联网

标签:xml null LayoutInflater name 源码 attrs View view


文章目录

前言

本篇讲解的是LayoutInflater的装载过程,其中会涉及到include、merge、ViewStub标签的源码解析。
我们对LayoutInflater的使用是再熟悉不过了,日常操作都是先调用from方法然后调用inflate方法。而对LayoutInflater是如何把一个xml布局文件加载到界面上的过程可能不是很了解。

LayoutInflater实例

说起LayoutInflater,我们最熟悉的写法是:

LayoutInflater.from(context).inflate(R.layout.xx,null);

LayoutInflater的from方法源码是:

public static LayoutInflater from(Context context) {
        LayoutInflater LayoutInflater =
                (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        if (LayoutInflater == null) {
            throw new AssertionError("LayoutInflater not found.");
        }
        return LayoutInflater;
    }

可以看出实际上是调用context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

而LayoutInflater本身是一个抽象类,它的具体实现类是在ContextImpl的getSystemService方法中被实例化。

通常在getSystemService方法得到的Object都是来自于SystemServiceRegistry中的static代码块中去registerService的:

   registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
                new CachedServiceFetcher<LayoutInflater>() {
            @Override
            public LayoutInflater createService(ContextImpl ctx) {
                return new PhoneLayoutInflater(ctx.getOuterContext());
            }});

由此可以知道,我们用的LayoutInflater实例都是来自于PhoneLayoutInflater,并且from方法并不会创建一个新的实例。

但是一看PhoneLayoutInflater并没有几行代码,由此判断基本逻辑都是在LayoutInflater这个抽象类中,我们只要关注LayoutInflater就行了

LayoutInflater的装载过程

那它是怎么装载布局的呢?我们调用inflate方法,经过重载后会到它是三个参数的重载方法:

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
        final Resources res = getContext().getResources();
        if (DEBUG) {
            Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
                    + Integer.toHexString(resource) + ")");
        }
        //载入布局文件
        final XmlResourceParser parser = res.getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }

经过这个方法把xml文件载入进来,XmlResourceParser继承了XmlPullParser,可以看出解析xml布局是基于Pull解析的(不了解XML几个解析方式的最好先自行百度下),最终重载调用到这个inflate方法(只举例部分重要代码):

  public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    ...
    final Context inflaterContext = mContext;
    //XmlResourceParser也是实现了AttributeSet接口,转成AttributeSet后相当于对属性的一种封装,方便属性的获取。
    final AttributeSet attrs = Xml.asAttributeSet(parser);
    View result = root;
    ...
    final String name = parser.getName();
    //根标签是否是merge标签
    if (TAG_MERGE.equals(name)) {
    //merge标签的布局必须传入父view,并且attachToRoot=true,不然xml中零零散散的view不知道要被添加到哪里
    if (root == null || !attachToRoot) {
        throw new InflateException("<merge /> can be used only with a valid "
                + "ViewGroup root and attachToRoot=true");
    }
    //解析带merge标签的布局,注意最后一个参数是传递false,传的Context是inflaterContext,也就是LayoutInflater的mConext,实际上就是LayoutInflater.from(context)时传的context
    rInflate(parser, root, inflaterContext, attrs, false);
    } else{
        //创建布局的根结点,createViewFromTag方法里会解析出对应的类名,然后调用createView方法反射创建出来
         final View temp = createViewFromTag(root, name, inflaterContext, attrs);
         //生成根布局的布局参数对象
         params = root.generateLayoutParams(attrs);
         //解析子View布局,注意最后一个参数是传递true
          rInflateChildren(parser, temp, attrs, true);
         //如果我们传入了一个parent View,那么就把根结点附着到这个parent上
        if (root != null && attachToRoot) {
               root.addView(temp, params);
        }
    }
    ...
  }

接下来rInflateChildren,用来装载子布局的,实际是调用了rInflate方法:

  final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
            boolean finishInflate) throws XmlPullParserException, IOException {
        //注意这里的context有区别于merge标签时传的context了
        rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
    }

到这里有个重要的发现,就是在inflate中判断了是不是merge布局,最终都会走向rInflate方法,

不同的是 如果是merge布局,那么rInflate方法中的第二个参数传递的是root,也就是来自我们调用的inflate(R.layout.xx,root,true);,然后最后一个参数finishInflate是false,而且context是来自于from(context)

如果不是merge布局。那么的第二个参数传递的是temp,也就是调用createViewFromTag后解析出根布局的view,最后一个参数finishInflate传的是true,Context传的是View的getContext()

那么rInflate方法如下:

    void rInflate(XmlPullParser parser, View parent, Context context,
            AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
        //获取xml标签的层级
        final int depth = parser.getDepth();
        int type;
        //pull解析结束条件
        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)) {
                parseRequestFocus(parser, parent);
            } 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");
                }
                parseInclude(parser, context, parent, attrs);
            } else if (TAG_MERGE.equals(name)) {
                //merge标签只能作为xml的根标签,也就是在另外一个文件中
                throw new InflateException("<merge /> must be the root element");
            } else {
                //递归解析创建View or ViewGroup
                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);
            }
        }
        //xml装载完成后每个view都会收到onFinishInflate方法的回调
        if (finishInflate) {
            parent.onFinishInflate();
        }
    }

我们关注else分支的代码,这里面也就是一个递归,不断的调用createViewFromTag反射创建出View,然后把创建的View添加到传进来的Parent View上,
在前面我们说的如果是merge布局的话则需要传一个root,因为merge布局文件中是没有父布局的,需要有个父布局可以来装载View

而如果不是merge的话,则传进来的是xml布局的根布局view。

include 标签解析

在里面中,判断遇到标签,则判断parser.getDepth() >0 就调用parseInclude方法

parser.getDepth()方法说明一下,指的也就是标签的层级(深度)
比如有下面的XML片段:

<root>
    <parent>
            <child>
            <child/>
    <parent/>
<root/>

那么root就是第一层 parent就是第二层 child就是第三层

这里代码判断parser.getDepth()==0要抛出异常,说明 标签是不能作为XML中的根标签的,换一种意思就是,include标签外面必须有包含的ViewGroup(当然这里还没体现出来一定得是ViewGroup,但在parseInclude方法里就判断了这个)

不过这里有点废话了 ,我们使用include一定是嵌在某个父布局里,要是XML要想就单单使用include的内容的话,那还不如直接引用那个xml文件了 何必新增个xml文件

在parseInclude方法与inflate方法有很大类似的逻辑,因为本身标签也是去加载另外一个xml布局

parseInclude方法中比较长,我们挑几个重要的流程代码:

   private void parseInclude(XmlPullParser parser, Context context, View parent,
            AttributeSet attrs) throws XmlPullParserException, IOException {

     //当然 能用inclde标签的一定是ViewGroup
     if (parent instanceof ViewGroup) {
     ...
     int layout = attrs.getAttributeResourceValue(null, ATTR_LAYOUT, 0);
       if (layout == 0) {
            final String value = attrs.getAttributeValue(null, ATTR_LAYOUT);
            //如果include中没有带layout标签,则抛出异常。没有layout标签的include是没有灵魂的,连肉体都没有
            if (value == null || value.length() <= 0) {
                throw new InflateException("You must specify a layout in the"
                        + " include tag: <include layout=\"@layout/layoutID\" />");
            }
               ...
     }else{
        //跟inflate方法类似,会吧layout读取进来
         final XmlResourceParser childParser = context.getResources().getLayout(layout);

        //剩下的代码与inflate过程有惊人的相似,这里不再说明
          ...
     }

     }
 }

另外注意rInflate方法最下面的:

 if (finishInflate) {
            parent.onFinishInflate();
        }

如果finishInflate为true,则会调用View的onFinishInflate方法。

因为rInflate方法是递归调用的,由此得知onFinishInflate方法的调用时机就是:在整个XML布局解析完毕之后会回调每个View的onFinishInflate方法

通常我们自定义View的时候时就可以用到onFinishInflate方法来监听布局是否已经加载完成,以确保我们可以读取到View和属性,比如在XML使用自定义ViewGroup时,它的getChildAt()、findViewById()一定是在onFinishInflate方法之后使用

merge 标签解析

标签解析并没有太多与其他xml不同的地方。

上面代码注释说了如果是带merge标签的xml则在进入rInflate方法时传了finishInflate为false,这不是不让标签里的View回调onFinishInflate
因为递归调用rInflateChildren时都一样是传true

这里传的false只是针对于载入merge标签的父布局,也就是root。为什么要为false呢?

因为使用merge标签有两种情况:

第一种是我们自定义组合视图时,比如LinearLayout。在里面进行inflate时一般会采用merge标签的xml,因为本身就是一个父布局了

另外一种就是XML中与标签组合使用,比如我们定义一个标签的视图(注意此时是没有根布局的),然后在其它xml里面要引用这个的话
可以使用把该xml包含进来,这样就达到了复用带标签里的布局内容,又不会因为要include一个xml进来时而多了一层父布局。

在第一种情况下,我们其实用不到finishInflate这个回调,因为调用完inflate方法后我们就可以使用findViewById了

但是第二种情况下,对于merge的父布局来讲本身就会调用被一次finishInflate方法了,那么在遇到merge时,对rInflate传入这个父布局要是再调用就重复了。

attachToRoot参数解析

到这里,还有个东西没说,就是我们的inflate方法:

LayoutInflater.from(context).inflate(R.layout.root,false);

第三个参数是啥作用?或许你已经明白,但是我们还是从源码的角度来解释它的作用

在上面,我并没有贴出inflate方法中关于第三个参数的相关代码,是因为想单独出来说,下面我们抽出这个方法涉及到这个参数的代码:

//这里我们讲过,merge标签必须要attachToRoot为true,为什么呢
if (TAG_MERGE.equals(name)) {
        if (root == null || !attachToRoot) {
            throw new InflateException("<merge /> can be used only with a valid "
                    + "ViewGroup root and attachToRoot=true");
        }

        rInflate(parser, root, inflaterContext, attrs, false);
}


 params = root.generateLayoutParams(attrs);
 //如果attachToRoot不为tue,则xml根布局才会设置LayoutParams
    if (!attachToRoot) {
        // Set the layout params for temp if we are not
        // attaching. (If we are, we use addView, below)
        temp.setLayoutParams(params);
    }



   //如果我们有指定传入一个父布局,并且attachToRoot=true ,那么会把xml根布局添加到这个我们指定的父布局中
  if (root != null && attachToRoot) {
        root.addView(temp, params);
    }

    //如果我们没指定一个父布局 或者attachToRoot=false,则inflate方法解析完成后返回的view就是这个xml的根布局
    if (root == null || !attachToRoot) {
        result = temp;
    }

经过上面对几种情况的注释,相信已经差不多了解attachToRoot的影响了吧。

attachToRoot的作用也就是要不要附属到指定的父布局上

如果我们传入的root不为空,并且attachToRoot为true,则会把布局内容添加到root容器里

如果我们传入的root不为空,并且attachToRoot为false,那么同样不会添加到root容器里,但是也不会设置自身的LayoutParams

如果我们传入的root为空,那么attachToRoot为true,布局内容不会被添加到其他地方去,但是会设置自身的LayoutParams

如果我们传入的root为空,那么attachToRoot为false,布局内容不会被添加到其他地方去,同时也不会设置自身的LayoutParams

View创建过程

到这里我们已经对LayoutInflater在装载View的过程有个大概的了解了。

我们在其中一个叫做createViewFromTag的方法在上面都是一笔带过,这里挑出来专门大概讲一下。

createViewFromTag方法最终调用它的5个参数的重载方法:

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
         // 1
        if (name.equals("view")) {
            name = attrs.getAttributeValue(null, "class");
        }

        // 2
        if (!ignoreThemeAttr) {
            final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
            final int themeResId = ta.getResourceId(0, 0);
            if (themeResId != 0) {
                context = new ContextThemeWrapper(context, themeResId);
            }
            ta.recycle();
        }
        //3
        if (name.equals(TAG_1995)) {
            // Let's party like it's 1995!
            return new BlinkLayout(context, attrs);
        }

        try {
            View view;
            //4
            if (mFactory2 != null) {
                view = mFactory2.onCreateView(parent, name, context, attrs);
            } else if (mFactory != null) {
                view = mFactory.onCreateView(name, context, attrs);
            } else {
                view = null;
            }
            //5
            if (view == null && mPrivateFactory != null) {
                view = mPrivateFactory.onCreateView(parent, name, context, attrs);
            }
            //6
            if (view == null) {
                final Object lastContext = mConstructorArgs[0];
                mConstructorArgs[0] = context;
                try {
                    if (-1 == name.indexOf('.')) {
                        view = onCreateView(parent, name, attrs);
                    } else {
                        view = createView(name, null, attrs);
                    }
                } finally {
                    mConstructorArgs[0] = lastContext;
                }
            }

            return view;
        } catch (InflateException e) {
            throw e;

        } catch (ClassNotFoundException e) {
            final InflateException ie = new InflateException(attrs.getPositionDescription()
                    + ": Error inflating class " + name, e);
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw ie;

        } catch (Exception e) {
            final InflateException ie = new InflateException(attrs.getPositionDescription()
                    + ": Error inflating class " + name, e);
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw ie;
        }
    }

我在源码中标注了6个地方,一个一个描述

(1)判断view标签

从源码上看,当标签是view的话,就获取它的一个class属性,然后重新赋值给name

由此我们可以知道,这个view标签的用法应该是这样的:

  <view
        class="LinearLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

想这样的话,name本身是view,而解析了class属性后,name就变成了LinearLayout,与我们定义:

<LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

作用是一样的。这个view标签我们几乎不会用到这样的写法,所以可能很多人还不知道view标签的作用

(2) 主题相关判断

从源码逻辑上看,是获取了theme的相关属性,如果有的话则重新包装一下Context,变成一个带主题的Context,也就是ContextThemeWrapper

这个主题会影响后面关于View的属性样式相关

(3)BlinkLayout判断

标签是一个会自动闪烁的布局容器,会被转化成BlinkLayout。
如果你有兴趣的话可以写个例子:

    <blink
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="我会一直闪烁的文字" />

    </blink>

BlinkLayout是LayoutInflater里面的一个私有内部类,我们并不能直接使用它,而且我们日常也几乎用不到这个blink标签
它的内部也没有几句代码,点击进去看一下只是继承了下FrameLayout

内部封装了一个Handler用于定时调用invalidate、然后用一个不断交替的mBlinkState变量判断是否调用dispatchDraw来完成绘制

(4)Factory接口自定义View的创建规则

这个从代码看上去貌似是用来生产View的工厂,那么mFactory和mFactory2是什么时候被赋值的

在AS上输入查找关键词: mFactory =

可以看到他们分别都有一个set方法:setFactory(Factory factory) 和 setFactory2(Factory2 factory)

setFactory如下:

   public void setFactory(Factory factory) {
        if (mFactorySet) {
            throw new IllegalStateException("A factory has already been set on this LayoutInflater");
        }
        if (factory == null) {
            throw new NullPointerException("Given factory can not be null");
        }
        mFactorySet = true;
        if (mFactory == null) {
            mFactory = factory;
        } else {
            mFactory = new FactoryMerger(factory, null, mFactory, mFactory2);
        }
    }

可以发现setFactory方法并不允许被重复调用的,不然会抛出异常

然后mFactory如果不为空的话,并没有直接赋值传进来的factory变量,而是创建了一个FactoryMerger

看setFactory2方法如下:

public void setFactory2(Factory2 factory) {
        if (mFactorySet) {
            throw new IllegalStateException("A factory has already been set on this LayoutInflater");
        }
        if (factory == null) {
            throw new NullPointerException("Given factory can not be null");
        }
        mFactorySet = true;
        if (mFactory == null) {
            mFactory = mFactory2 = factory;
        } else {
            mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
        }
    }

可以发现setFactory2方法也是不允许被重复调用的,不然会抛出异常

而且逻辑也一样,mFactory如果不为空的话,并没有直接赋值传进来的factory变量,而是创建了一个FactoryMerger。

那么FactoryMerger是啥呢?从名字上看待了merge的字眼,看上去不难猜测出是合并这两个工厂的作用。

根据Factory接口的介绍我们明白了,Factory接口和Factory2接口都是用来给开发者自定义View的创建规则的。

Factory2接口相比Factory接口,则重载了下接口方法,多传了个parent View进来

Factory2接口是在API 11的时候被加入进来的,因为Factory接口造成的问题是我们无法得知它的父 view是谁 从而限制了一些操作。

所以官方已经废弃了Factory接口,我们一般要采用的就是Factory2接口

关于这个Factory接口,谷歌是这么介绍的:

 /**
         * Hook you can supply that is called when inflating from a LayoutInflater.
         * You can use this to customize the tag names available in your XML
         * layout files.
         *
 **/

原来是做了个hook方便我们来自定义View的创建规则,比如说我故意要把该xml布局文件里面的TextView控件都换成Button
那么我们可以在Activity内这么写:

 LayoutInflater.from(this).setFactory2(new LayoutInflater.Factory2() {
            @Override
            public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
                return null;
            }

            @Override
            public View onCreateView(String name, Context context, AttributeSet attrs) {
                if(name.equals("TextView")){
                    Button button = new Button(context,attrs);
                    return button;
                }
                return null;
            }
        });

既然是留给开发者自定义的,那么正常来讲这里factory和factory2都会为空,所以view还是空的

(5)View的默认创建规则

正常来说的话则会走第5步这个流程:

//5
    if (view == null && mPrivateFactory != null) {
        view = mPrivateFactory.onCreateView(parent, name, context, attrs);
    }

mPrivateFactory一看就是私有的工厂 不是给开发者用的。那么这个mPrivateFactory是什么时候被赋值的呢,搜索一下发现:

 /**
     * @hide for use by framework
     */
    public void setPrivateFactory(Factory2 factory) {
        if (mPrivateFactory == null) {
            mPrivateFactory = factory;
        } else {
            mPrivateFactory = new FactoryMerger(factory, factory, mPrivateFactory, mPrivateFactory);
        }
    }

framework会自己调用这个共有方法,而对开发者是隐藏的。

它在Activity的attach方法中被用到:

 final void attach(...){
  mWindow.getLayoutInflater().setPrivateFactory(this);
 }

设置了Activity自己,看来Activity是实现了这个接口了,找了下发现Activity实现了LayoutInflater.Factory2接口

我们看Activity是如何实现这个接口的:

   public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        if (!"fragment".equals(name)) {
            return onCreateView(name, context, attrs);
        }

        return mFragments.onCreateView(parent, name, context, attrs);
    }
@Nullable
public View onCreateView(String name, Context context, AttributeSet attrs) {
    return null;
}
  public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
      return mHost.mFragmentManager.onCreateView(parent, name, context, attrs);
  }

这里一不小心引出了Fragment。

我们可以发现Activity并没有自己根据标签去创建View,而是判断如果是fragment标签的话则应用是走了Fragment的onCreateView生命周期方法

所以我们就可以知道,如果是fragment标签的话,则是Activity自己创建的

如果不是的话,Activity调用3个参数的onCreateView方法,直接返回null

所以默认情况下,还是会教给LayoutInflater自己来创建

(6)View的创建过程

既然我们得出了上面的结论,那么下面这个第6步的代码,默认情况下还是会走

// 6
if (view == null) {
    final Object lastContext = mConstructorArgs[0];
    mConstructorArgs[0] = context;
    try {
        if (-1 == name.indexOf('.')) {
            view = onCreateView(parent, name, attrs);
        } else {
            view = createView(name, null, attrs);
        }
    } finally {
        mConstructorArgs[0] = lastContext;
    }

这里做了个判断,判断标签名字是否有带个小数点,我们不难猜测出,是因为我们往往自定义View的时候,在xml中布局的时候需要写出这个View的全称

所以这里判断是否带了个小数点,判断是否是自定义View。 如果不是自定义的话,走了onCreateView方法后最终也是到createView方法:

 protected View onCreateView(String name, AttributeSet attrs)
            throws ClassNotFoundException {
        return createView(name, "android.view.", attrs);
    }

可以发现,与自定义view调用createView方法不同的是,这里多传了个前缀"android.view."

足以看出,我们在XML中声明的自带的控件,会被系统自己加上个类名的前缀以反射出类名创建对象。

我们看一下createView方法:

  public final View createView(String name, String prefix, AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
       //取得缓存中已经加载过的class
       Constructor<? extends View> constructor = sConstructorMap.get(name);
       ...
        if (constructor == null) {
           // 注意这里,默认prefix则会传进来android.view以构成控件类名全称
           clazz = mContext.getClassLoader().loadClass(
                   prefix != null ? (prefix + name) : name).asSubclass(View.class);
            ...
            constructor = clazz.getConstructor(mConstructorSignature);
            constructor.setAccessible(true);
            //缓存已经加载过的类
            sConstructorMap.put(name, constructor);
        }
        ...
       //反射创建View实例
        final View view = constructor.newInstance(args);
       if (view instanceof ViewStub) {
           // Use the same context when inflating ViewStub later.
           final ViewStub viewStub = (ViewStub) view;
           viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
       }
       ...
       return view;
 }

createView方法被我精简了大量代码,因为那些反射代码流程感觉没有太多需要说明的,这里只有一个重点,就是ViewStub登场了

ViewStub源码解析

我们可以看到,如果判断是ViewStub类的话,也只是克隆了一个LayoutInflater实例,并没有做其他操作。

我们知道ViewStub是可以用来懒加载视图的,当我们在xml布局中不需要一开始就显示的布局,我们可以采用ViewStub,当需要用到的时候再inflate进来

ViewStub类的代码也不多,它同样继承了View,所有也可以有View的特性,可以被其他父布局添加成一个子View

ViewStub类定义的一些重点源码如下:

public final class ViewStub extends View {

private WeakReference<View> mInflatedViewRef;
private LayoutInflater mInflater;

 public ViewStub(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        ...
        setVisibility(GONE);
        setWillNotDraw(true);
    }
  public void setLayoutInflater(LayoutInflater inflater) {
        mInflater = inflater;
    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(0, 0);
    }

    @Override
    public void draw(Canvas canvas) {
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
    }

    public void setVisibility(int visibility) {
        //mInflatedViewRef不为空则说明加载过了,直接调用view自己的setVisibility
        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();
            }
        }
    }
    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");
        }
    }
  //替换坑位
  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);
        }
    }

 //装载资源布局界面
 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;
    }
}

ViewStub的代码很少,我们调了其中最重要的方法,我们可以知道,在创建的时候为ViewStub克隆了一个LayoutInflater实例赋值给了mInflater

我们发现在构建的时候,ViewStub自己便设置了setVisibility(GONE);和setWillNotDraw(true);

然后还在onMeasure里面传了宽高都是0的数值,并且draw和dispathDraw都是空方法。

这一切足以证明,ViewStub像是一个空袋子,与世无争什么都不干,就只在布局中占着个位置。

我们知道,我们要显示ViewStub内容的时候可以用inflate方法或者是setVisibility方法

我们看他的inflate方法流程,它会先调用inflateViewNoAdd方法来把指定的资源布局加载进来,然后用replaceSelfWithView方法来把自己从原先的父布局中的坑位中替换出来,给这个新加载的布局替换进去。

然后setVisibility方法其实也只是做了个判断,如果未装载,则在VISIBLE or INVISIBLE时调用inflate方法。

由此我们可以得知,原来ViewStub的懒加载原理是这样的,先不加载自己指定的xml到布局中,而是在布局中占了个坑位,没宽高也什么都不显示。
当需要加载的时候再加载布局资源进来,替换自己占的坑位

结束语

相信你在读完本篇后,一定会LayoutInfalter的装载过程有了一定的了解。

并且对布局优化的三个标签(include、merge、ViewStub)的原理理解更加深入。

标签:xml,null,LayoutInflater,name,源码,attrs,View,view
来源: https://blog.csdn.net/lc_miao/article/details/88294193

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有