美文网首页我爱编程Android技术知识Android开发
Layout inflate方法解析:由Xml文件生成View

Layout inflate方法解析:由Xml文件生成View

作者: joeey_y | 来源:发表于2018-04-23 23:46 被阅读0次

    太长了,不想看?

    LayoutInflater.inflate()方法能将Xml格式的布局文件转换成以父子关系组合的一系列View,转换后的结构也称为View Hierarchy。

    我们通常向inflate()方法传入三个参数:布局资源的resId,可为空的ViewGroup:root,以及布尔值attachToRoot。许多初学者对于后两个参数的使用比较生硬,缺乏理解。

    我理解的引入root的原因是为了读取Xml文件中最外层标签的布局属性。布局属性就是我们常看到的以<layout->开头的属性,他表示一个View希望在父布局中获得的尺寸以及位置,因此布局属性是提供给父布局读取以便计算尺寸、位置的。布局属性体现在View中就是mLayoutParams变量,这个变量的类型是ViewGroup.LayoutParams。不同的ViewGroup的子类实现了不同的LayoutParams类,用以从Xml中读取自己关心的布局属性,一个View的mLayoutParams的类型必须与其父布局实现的LayoutParams相一致。

    对于Xml文件的最外层标签,他所转换成的View并不知道自己的父布局会是什么类型的,因此他不会生成mLayoutParams变量,此时该标签的所有<layout-XXX>属性都没有应用到View中。为了避免这种情况,我们需要告知最外层的View他的父布局是什么类型的,生成对应的LayoutParams储存布局属性。root就能帮助我们生成LayoutParams,而如果root正是我们希望的父布局,那么我们就将attachToRoot设为true,这样我们通过Xml生成的View Hierarchy可以直接加入到root中,我们不需要手动做这步操作了。

    如果我们将Xml文件的嵌套结构看作是树状结构的话,逐个标签的解析其实就是树的深度优先遍历,我们在遍历的同时生成了一棵以View为节点,使用父子关系关联的树。

    View Hierarchy中每个View的生成有四步:

    1. 由标签生成一个View
    2. 根据View的父布局的类型生成对应的LayoutParams,并将LayoutParams设置给View
    3. 生成View的所有Children
    4. 将View加入他的父布局中

    由一个标签转换成一个View的过程其实就是通过ClassLoader加载出标签对应的View.Class文件并获得构造器,相当于调用View(Context context, @Nullable AttributeSet attrs)构造一个实例。因此我们的自定义控件需要实现该构造方法才能在Xml中使用,随后从attrs变量中获得Xml中的属性。

    inflate方法介绍

    Android开发中,我们使用LayoutInflater.inflate()方法将layout目录下Xml格式的资源文件转换为一个View Hierarchy,并返回一个View对象。

    View Hierarchy直译大概就是视图层次,指的是以父子关系关联的一系列View,我们通过View Hierarchy的根节点(root view)可以获得该结构中所有的View对象。

    如果让我们自己去设计一个方法将布局文件转换为View Hierarchy,我们可能会想到逐行读取Xml中的标签,利用标签的信息生成一个新的View/ViewGroup,当Xml中出现嵌套关系就意味我们需要使用父子关系关联两个View。而inflate()方法做的就是这么一件事。

    我们可以使用Activity.getLayoutInflater()等方法获得一个LayoutInflater的实例,这些方法本质上都是通过getSystemService(Context.LAYOUT_INFLATER_SERVICE)获得一个系统服务,这个方法最终会返回一个PhoneLayoutInflater的实例。

    inflate有几种重载方法,但最终都会走到

    public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)
    

    第一个参数我们看着比较陌生,但是大部分情况下我们只需要传入一个layout资源文件,Framework会帮我们根据资源获得一个Parser。需要注意的是为了提升使用时的效率,Android会在编译时就去解析layout资源文件并生成Parser,因此我们想通过inflate()方法在使用过程中使用一个单纯的Xml文件(非布局资源)去生成View是不可行的。

    后面两个参数会在源码解析过程中介绍。

    我们看下官方对这几个参数的定义:

    参数 意义
    parser XmlPullParser:以Pull的方式解析Xml文件,通过Parser对象方便我们操作
    root ViewGroup:可选项。当attachToRoot为true时,他将作为生成出来的View层级的parent。如果attachToRoot为false,那root仅仅为View层级树的根节点提供LayoutParams的值
    attachToRoot 配合root使用

    inflate()方法的返回值是一个View,如果root不会为空且attachToRoot为true,返回root。否则返回Xml生成的View Hierarchy的根View。

    Inflate 方法源码解析

    看下inflate阶段的核心代码

    public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        // 解析根节点
        final String name = parser.getName();
        
        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);
        } else {
            // 将Xml最外层的标签解析为View对象,记为temp
            final View temp = createViewFromTag(root, name, inflaterContext, attrs);
            
            if (root != null) {
                // 当root不为空时,创建与root相匹配的LayoutParams
                params = root.generateLayoutParams(attrs);
                if (!attachToRoot) {
                    // 仅将params提供给temp
                    temp.setLayoutParams(params);
                }
            }
            
            // 通过inflate生成temp的所有chiildre
            rInflateChildren(parser, temp, attrs, true);
            
            // root不为空且attachToRoot为true,将temp加入到root中,且用到params
            if (root != null && attachToRoot) {
                root.addView(temp, params);
            }
            
            if (root != null && attachToRoot) 
                return root;
            }else {
                return temp;
            }
        }
    }
    

    先不考虑<merge>标签,inflate()方法执行的流程:

    1. 将最外层的标签转换为一个View,记为temp。
    2. 当root不为空时,利用root生成的temp的LayoutParams。
    3. 解析Xml并生成temp的所有子View。
    4. 当root不为空且attachToRoot为true时,将temp添加为root的一个child。
    5. 当root不为空且attachToRoot为true时,返回root,否则返回temp。

    这个流程包含了几个细节:

    1. 由Xml标签生成View对象
    2. 根据Xml嵌套结构生成View父子结构
    3. 应用root及attachToRoot

    下面,我们由表及里的解析这几个细节。首先来看一下传参中root与attachToRoot两个参数的作用。

    root与attachToRoot

    root在inflate()方法中的第一个作用就是生成一个LayoutParams。

    if (root != null) {
        // 当root不为空时,创建与root相匹配的LayoutParams
        params = root.generateLayoutParams(attrs);
    }
    

    LayoutParams保存的是一个控件的布局属性。那我们来看下为什么需要利用root生成LayoutParams。

    布局属性与LayoutParams

    在Xml文件中,有许多前缀为layout_的属性,比如我们最熟悉的layout_width/layout_height,我们称其为布局属性。View使用布局属性来告知父布局它所希望的尺寸与位置。不同类型的父布局读取的布局属性不同,比如layout_centerInParent属性,父布局为RelativeLayout时会起作用,而父布局为LinearLayout时则无法使用。

    Xml中的布局属性保存到View中就是mLayoutParams变量,它的类型是ViewGroup.LayoutParams。实际上ViewGroup的子类都会实现一个扩展自ViewGroup.LayoutParams的嵌套类,这个LayoutParams类决定了他会读取哪些布局属性。我们看下RelativeLayout.LayoutParams源码的一部分:

    public LayoutParams(Context c, AttributeSet attrs) {
        super(c, attrs);
    
        TypedArray a = c.obtainStyledAttributes(attrs,
                com.android.internal.R.styleable.RelativeLayout_Layout);
        ...
        
        final int N = a.getIndexCount();
        for (int i = 0; i < N; i++) {
            int attr = a.getIndex(i);
            switch (attr) {
                ...
                case com.android.internal.R.styleable.RelativeLayout_Layout_layout_toLeftOf:
                    rules[LEFT_OF] = a.getResourceId(attr, 0);
                    break;
                case com.android.internal.R.styleable.RelativeLayout_Layout_layout_toRightOf:
                    rules[RIGHT_OF] = a.getResourceId(attr, 0);
                    break;
                case com.android.internal.R.styleable.RelativeLayout_Layout_layout_above:
                    rules[ABOVE] = a.getResourceId(attr, 0);
                    break;
                case com.android.internal.R.styleable.RelativeLayout_Layout_layout_centerInParent:
                    rules[CENTER_IN_PARENT] = a.getBoolean(attr, false) ? TRUE : 0;
                    break;
                ...
            }
        }
        ...
        a.recycle();
    }
    

    这段代码跟我们自定义控件时读取Xml中的自定义属性是一样的做法,我们看到RelativeLayout.LayoutParams在创建时读取了一系列布局属性并存储,比如layout_centerInParent,其他的LayoutParams不会读取该属性。

    如果对AttributeSet、TypedArray不熟悉可以参考这里:https://blog.csdn.net/lmj623565791/article/details/45022631

    我们能在RelativeLayout.onMeasure()方法中找到对LayoutParams的使用。

    // RelativeLayout.java
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        ...
        int count = views.length;
        for (int i = 0; i < count; i++) {
            View child = views[i];
            if (child.getVisibility() != GONE) {
                ...
                LayoutParams params = (LayoutParams) child.getLayoutParams();
                int[] rules = params.getRules(layoutDirection);
                ...
            }
        }
        ...
    }
    

    当计算每个子View的位置时,需要读取他们的布局属性,此时会将View的LayoutParams强制类型转换为RelativeLayout.LayoutParams,这里可能会报出无法转换类型的错误,所以我们需要保证加入到RelativeLayout的View的LayoutParams类型都是RelativeLayout.LayoutParams

    我们总结一下LayoutParams的核心知识点:

    1. LayoutParams保存了Xml中的layout_开头的布局属性
    2. ViewGroup子类通常会实现一个LayoutParams类,用于读取他们需要的布局属性
    3. View的LayoutParams类型必须与其父布局的类型相匹配,否则会在onMeasure过程中报错

    回到root与attachToRoot

    当我们解析Xml中最外层的标签,也就是View Hierarchy的根View时,程序并不知道它的父布局会是什么类型的,因此不会生成LayoutParams。这时最外层标签中的所有布局属性,包括layout_width/layout_height都不会被记录到View对象中,也就是俗称的“属性失效了”。

    但在实际使用中,我们通常能知道Xml生成的View Hierarchy所要加入的父布局或是要加入的父布局的类型。这时候我们传入一个root参数,根据root的类型去读取根View的布局属性并生成对应的LayoutParams。这段代码如下:

    if (root != null) {
        // root不为空时,生成与root对应的LayoutParams
        params = root.generateLayoutParams(attrs);
    }
    
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        // 不同的ViewGroup生成LayoutParams的细节不同
        return new LayoutParams(getContext(), attrs);
    }
    

    attachToRoot则用于判断root是直接作为parent使用还是仅需要他的类型信息。

    if (root != null) {
        if (attachToRoot) {
            root.addView(temp, params);
        }else{
            // Set the layout params for temp if we are not attaching. 
            temp.setLayoutParams(params);
        }
    }
    

    递归处理Xml中的所有标签

    inflate方法中,除了根标签以外所有剩余标签的解析只使用了一个方法:

    public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        ...
        // Inflate all children under temp against its context.
        rInflateChildren(parser, temp, attrs, true);            
        ...
    }
    
    final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
            boolean finishInflate) throws XmlPullParserException, IOException {
        rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
    }
    

    这个方法仅仅是调用rInflate方法,没有其他额外的行为。rInflate方法的中r的含义是Recursive,即递归,我们可以猜测这个方法是使用递归的方式去处理Xml中的所有嵌套关系。我们看下rInflate核心部分:

    这段方法涉及到XmlPullParser的知识,他将Xml文件转换为一个对象。通过next()方法获得下一个事件,一共有五个事件START_DOCUMENT、START_TAG、TEXT、END_TAG、END_DOCUMENT。并且通过depth取得当前元素嵌套的深度,未读取到START_TAG时depth为0,每次读取到START_TAG时depth加1。细节参考 http://www.xmlpull.org/

    void rInflate(XmlPullParser parser, View parent, Context context,
                AttributeSet attrs, boolean finishInflate) {
        
        final int depth = parser.getDepth();
        // while的结束条件:找到该depth的END_TAG(或者文件结束)
        while (((type = parser.next()) != XmlPullParser.END_TAG
            || parser.getDepth() > depth) 
            && type != XmlPullParser.END_DOCUMENT) {
                
            // 只有遇到START_TAG时进行解析
            if (type != XmlPullParser.START_TAG) {
                continue;
            }
        
            final String name = parser.getName();
        
            if (TAG_REQUEST_FOCUS.equals(name)) {
                ...
            } 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)) {
                throw new InflateException("<merge /> must be the root element");
            } else {
                // 将当前Tag转换为View
                final View view = createViewFromTag(parent, name, context, attrs);
                // 根据父布局类型读取布局属性
                final ViewGroup viewGroup = (ViewGroup) parent;
                final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
                // 嵌套处理,解析当前View的所有children
                rInflateChildren(parser, view, attrs, true);
                // 将View加入到parent中
                viewGroup.addView(view, params);
            }
        }
    }
    

    我们配合例子分析下rInflate方法的流程:

                                   <!-- depth -->
    <LayoutA >                       <!-- 1 -->
        <ViewB />                    <!-- 2 -->
        <LayoutC >                   <!-- 2 -->
            <ViewC1 />               <!-- 3 -->
        </LayoutC >                  <!-- 2 -->
    </LayoutA >                      <!-- 1 -->
    
    1.首先记下当前的depth
    2.取Xml中的下一个事件,直到到达文件的结尾,或者到达了当前depth的END_TAG

    参照例子,如果rInflate开始时解析到了<LayoutC>,那么当解析到</LayoutC>时就会从while跳出,本次执行结束。

    3. while循环中,遇到START_TAG时解析

    只解析STAST_TAG是保证每个Tag都只生成一个View,比如在解析到<LayoutC>时生成一个View,解析到</LayoutC>时则不需要。

    解析普通的TAG的逻辑如下:

    // 将当前Tag转换为View
    final View view = createViewFromTag(parent, name, context, attrs);
    // 根据父布局类型读取布局属性
    final ViewGroup viewGroup = (ViewGroup) parent;
    final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
    // 嵌套处理,解析当前View的所有children
    rInflateChildren(parser, view, attrs, true);
    // 将View加入到parent中
    viewGroup.addView(view, params);
    

    这里需要关注的就是那个嵌套的rInflateChildren()方法,他也会走到rInflate,以当前解析出的View作为Parent,解析下一层的内容。

    inflate过程推演

    我们还是以上面的例子将inflate方法的执行流程推演一遍。

                                   <!-- depth -->
    <LayoutA >                       <!-- 1 -->
        <ViewB />                    <!-- 2 -->
        <LayoutC >                   <!-- 2 -->
            <ViewC />                <!-- 3 -->
        </LayoutC >                  <!-- 2 -->
    </LayoutA >                      <!-- 1 -->
    
    • inflate方法,取到<LayoutA>,解析出LayoutA对象
    • 通过rInflate方法解析LayoutA的所有children,开始时depth为1
      • 取到<ViewB />,解析出ViewB对象,ViewB无children,它的rInflateChildren会读到ViewB的END_TAG并结束,将ViewB加入到LayoutA中
      • 取到<LayoutC>,解析出LayoutC对象,调用rInflate方法解析Children
        • 取到<ViewC />,解析出ViewC对象,无Children,将ViewC加入到LayoutC中
        • 取到<LayoutC />,LayoutC的rInflateChildren过程结束
        • 将LayoutC对象加入到LayoutA中
      • 取到</LayoutA>,为END_TAG且depth为1,rInflate方法结束
    • View Hierarchy解析完成,配合root及attachToRoot做些处理便可返回

    整个流程自上而下的解析了Xml文件中的所有Tag,并生成了对应的View Hierarchy,嵌套关系顺利转换成了父子关系。生成的View Hierarchy是一个树状结构,生成过程跟树的深度优先遍历有相似的感觉。

    merge标签解析

    解析完rInflate方法后,我们再来看下<merge>标签的解析:

    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);
    }
    

    其实就是忽略<merge>这一层,将<merge>的所有内容都直接加入到root中。

    由Tag生成对应的View

    前面我们介绍了layout Xml如何转换为View Hierarchy,这个过程中的最后一个细节就是单个Tag如何转化成View对象

    无论是inflate方法对根View的解析还是rInflate中的嵌套解析,都是调用createViewFromTag()方法,我们看下这个方法的核心部分。

    View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
                boolean ignoreThemeAttr) {
        
        // <view>标签中可以使用class来标注类        
        if (name.equals("view")) {
            name = attrs.getAttributeValue(null, "class");
        }
        
        // 除了<include>以外,ignoreThemeAttr总为ture,读取Xml中的theme并通过Context作用到View上
        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();
        }
    
        // 就当是彩蛋吧,let's party
        if (name.equals(TAG_1995)) {
            // Let's party like it's 1995!
            return new BlinkLayout(context, attrs);
        }
        
        // 生成View
        try {
            View view;
            if (mFactory2 != null) {
                view = mFactory2.onCreateView(parent, name, context, attrs);
            } else if (mFactory != null) {
                view = mFactory.onCreateView(name, context, attrs);
            } else {
                view = null;
            }
        
            if (view == null && mPrivateFactory != null) {
                view = mPrivateFactory.onCreateView(parent, name, context, attrs);
            }
        
            if (view == null) {
                final Object lastContext = mConstructorArgs[0];
                // 用于构建View的参数
                // View(Context context, @Nullable AttributeSet attrs)
                // 第一个传参是context
                attrs, int defStyleAttr)
                mConstructorArgs[0] = context;
                try {
                    // 使用系统控件时我们可以不带着命名空间,此时name中不包含"."
                    if (-1 == name.indexOf('.')) {
                        view = onCreateView(parent, name, attrs);
                    } else {
                        view = createView(name, null, attrs);
                    }
                } finally {
                    mConstructorArgs[0] = lastContext;
                }
            }
        
            return view;
        }
        ...
    }    
    

    直接看到生成View的部分

    1. 由Factory2生成
    2. 由Factory生成
    3. 由mPrivateFactory生成
    4. 由onCreateView/createView方法生成

    这里的生成方法有先后顺序,View一旦生成就不用走后面的方法。Factory2及Factory可以看做是给我们hook代码的,允许我们按自己的期望去将Tag转换为View,二者的区别是工厂方法的传参不同。

    如果没有设置工厂

    if (-1 == name.indexOf('.')) {
        view = onCreateView(parent, name, attrs);
    } else {
        view = createView(name, null, attrs);
    }
    

    Xml中使用系统控件可以不加上命名空间,因此name中没有“.”,在onCreateView方法中会为系统控件加上前缀“"android.view."”并调用createView方法。而我们前面提到了我们实际使用的LayoutInflater通常是PhoneLayoutInflater,他重写了onCreateView方法:

    /// PhoneLayoutInflater.java
    
    @Override protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
        for (String prefix : sClassPrefixList) {
            try {
                View view = createView(name, prefix, attrs);
                if (view != null) {
                    return view;
                }
            } catch (ClassNotFoundException e) {
                // In this case we want to let the base class take a crack
                // at it.
            }
        }
    
        return super.onCreateView(name, attrs);
    }
    
    private static final String[] sClassPrefixList = {
        "android.widget.",
        "android.webkit.",
        "android.app."
    };
    
    /// LayoutInflater.java
    
    protected View onCreateView(String name, AttributeSet attrs)
            throws ClassNotFoundException {
        return createView(name, "android.view.", attrs);
    }
    
    

    这里的操作都是为系统控件补全命名空间,具体的生成View的工作由createView完成,核心代码如下:

    public final View createView(String name, String prefix, AttributeSet attrs)
                throws ClassNotFoundException, InflateException {
        ...
        // 使用ClassLoader获得构造器
        clazz = mContext.getClassLoader().loadClass(prefix != null ? (prefix + name) : name).asSubclass(View.class);
        constructor = clazz.getConstructor(mConstructorSignature);
        constructor.setAccessible(true);  
        
        // View的构造器的传参
        Object[] args = mConstructorArgs;
        args[1] = attrs;
        
        // 获得View实例    
        final View view = constructor.newInstance(args);
        return view;
        ...
    }
    

    先通过ClassLoader加载类,获得构造器,然后实例化View的子类。具体的构造方法是View(Context context, @Nullable AttributeSet attrs),因此我们的自定义控件也需要实现这个构造方法才能在Xml中正确使用。
    `

    相关文章

      网友评论

        本文标题:Layout inflate方法解析:由Xml文件生成View

        本文链接:https://www.haomeiwen.com/subject/gstdlftx.html