Android中LayoutInflate解析xml布局文件生成View树的过程(一)

学习过自定义 View 的都知道,ViewGroup 的事件分发或者绘制都涉及到子 View 的遍历,在看ViewGroup 的源码的过程中发现了这个我们一直见到的 ViewGroup 的子 View 的集合,在这里是以数组的形式存储的:View[] mChildren。我就想,这个 mChildren 是在哪里赋值的呢?或者说是怎么被赋值的呢?这时候我想到,在平时我们想往一个 ViewGroup 中添加子 View 的时候,往往是调用的这个 ViewGroup 的 addView(View child) 方法,那么我们只要找到调用 addView(View child) 的地方,自然也就找到了。但是直接找这个方法的调用,可不是一件容易的事,看来我们要换种思路了。不急,这里我们先深入 addView(View child) 方法里看看具体是怎么赋值的,也算是给自己记下笔记吧。

ViewGroup 的 addView(View child)过程,即 mChildren 的赋值

用一个图来表示吧:

clipboard.png

addView(View child) 中调用了 addViewInner(child, index, params, false) ,在
addViewInner(child, index, params, false) 中又调用了 addInArray(child, index),在该方法中才是具体的实现了对 mChildren 的赋值(通过引用直接修改),下面贴下该方法的源码:

private void addInArray(View child, int index) {
        View[] children = mChildren;
        final int count = mChildrenCount;
        final int size = children.length;
        if (index == count) {
            if (size == count) {
                mChildren = new View[size + ARRAY_CAPACITY_INCREMENT];
                System.arraycopy(children, 0, mChildren, 0, size);
                children = mChildren;
            }
            // 注意这里,这里就是对 mChildren 进行赋值的地方
            children[mChildrenCount++] = child;
        } else if (index < count) {
            if (size == count) {
                mChildren = new View[size + ARRAY_CAPACITY_INCREMENT];
                System.arraycopy(children, 0, mChildren, 0, index);
                System.arraycopy(children, index, mChildren, index + 1, count - index);
                children = mChildren;
            } else {
                System.arraycopy(children, index, children, index + 1, count - index);
            }
            children[index] = child;
            mChildrenCount++;
            if (mLastTouchDownIndex >= index) {
                mLastTouchDownIndex++;
            }
        } else {
            throw new IndexOutOfBoundsException("index=" + index + " count=" + count);
        }
    }

代码很简单,可以看到这里也是搞了一个不断增长的数组来更新 mChildren,每次增长的长度为:ARRAY_CAPACITY_INCREMENT,为常量 12,这里对该方法我们不过多讲解,毕竟,我们的重点不在这。

布局文件 xxx.xml 是怎么生成 View 树,并显示出来的呢?

我们已经习惯了直接在一个 activity 中的 onCreate(...) 方法中通过 setContentView(R.layout.main) 这种方式来给我们的 activity 指定布局,然后我们就可以通过修改布局文件,来决定activity的显示内容。显然,在 setContentView(...) 这个方法内部肯定有我们想要的答案,在其内部,一定涉及到 xml 布局文件与具体的 View 之间的转换,也就是 xml 文件的解析和 View 树的生成。那么就让我们深入到该方法里一探究竟。

注意:这里我们创建的activity直接继承自 Activity.Java 而非 AppCompatActivity.java , 至于为什么强调这个,在后面的文章里我们会具体讲解(涉及到LayoutInflate.Factory 以及兼容性的处理策略)。

按着 Ctrl 键,鼠标左键点击我们创建的 activitysetContentView(...) 方法,将会进入到 Activity.javasetContentView(@LayoutRes int layoutResID) 方法:

    public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }

可以看到,这里又调用了 Window 的 setContentView(layoutResID) 方法,而 Window 是一个抽象类,因为调用的实际上是它的唯一实现类 PhoneWindow.java 里的该方法:

 public void setContentView(int layoutResID) {
        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
        // decor, when theme attributes and the like are crystalized. Do not check the feature
        // before this happens.
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);
        } else {
            // 请注意这句,这句才是真正的加载过程,是关键代码
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }

别看这么多代码,关键的只有一句: mLayoutInflater.inflate(layoutResID, mContentParent),这句才是解析 xml 文件的核心代码,在此, LayoutInflate现身了,经常在代码里使用 LayoutInflate ,这次终于找到系统中使用该方法的地方了吧。那么接下来我们就真正看看 LayoutInflate 的使用以及 LayoutInflate.inflate(...)的具体过程吧。

LayoutInflate 的具体使用

一、如何获取 LayoutInflate

获取 InflateInflate 有三种方式:

  1. LayoutInflater layoutInflater = (LayoutInflater) this.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

  2. LayoutInflater layoutInflater = LayoutInflater.from(context);

  3. activity 中直接调用 LayoutInflater layoutInflater = getLayoutInflater(); 获得

其实,前面两种方式是完全一样的,只不过第二种对第一种进行了封装,使用起来更加方便。

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

二、LayoutInflate.inflate(...) 的过程

知道怎么获取 LayoutInflate 之后,我们继续上面的讲,前面我们通过 深入 setContentView(R.layout.xxx)发现真正解析 xml 布局文件,并且生成 View 树的其实就是 mLayoutInflater.inflate(layoutResID, mContentParent) 这行代码。那么我们就来看看

    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
        return inflate(resource, root, root != null);
    }

在该方法里又调用了含有三个参数的重载方法 : inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot),这就是我们经常使用 LayoutInfalte 时候传递三个参数或者传递两个参数的两种情况,关于这两种情况的区别,其实就是是否保留最外层View的 layout_xxx 属性,具体细节可以在此不再赘述,我们进入该方法继续看
` 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 {
        // 注意这里,这里调用的是另一个重载方法,注意第一个参数的类型为 XmlResourceParser 
        return inflate(parser, root, attachToRoot);
    } finally {
        parser.close();
    }
}`

需要注意,这里又调用了inflate(...)的另一个重载方法:public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot),仍然是三个参数,但要注意,第一个参数类型变为了 XmlResourceParser,这里就设计 xml 文档的 pull 解析了:

    public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");

            final Context inflaterContext = mContext;
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            Context lastContext = (Context) mConstructorArgs[0];
            mConstructorArgs[0] = inflaterContext;
            View result = root;

            try {
                // Look for the root node.
                int type;
                while ((type = parser.next()) != XmlPullParser.START_TAG &&
                        type != XmlPullParser.END_DOCUMENT) {
                    // Empty
                }

                if (type != XmlPullParser.START_TAG) {
                    throw new InflateException(parser.getPositionDescription()
                            + ": No start tag found!");
                }

                final String name = parser.getName();
                
                if (DEBUG) {
                    System.out.println("**************************");
                    System.out.println("Creating root view: "
                            + name);
                    System.out.println("**************************");
                }

                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");
                    }
                    // 关键代码1,调用含有四个参数的 inflate(...)
                    rInflate(parser, root, inflaterContext, attrs, false);
                } else {
                    // Temp is the root view that was found in the xml
                    // 关键代码2,根据传进来的各种参数,生成对应的 View
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                    ViewGroup.LayoutParams params = null;

                    if (root != null) {
                        if (DEBUG) {
                            System.out.println("Creating params from root: " +
                                    root);
                        }
                        // Create layout params that match root, if supplied
                        params = root.generateLayoutParams(attrs);
                        if (!attachToRoot) {
                            // Set the layout params for temp if we are not
                            // attaching. (If we are, we use addView, below)
                            temp.setLayoutParams(params);
                        }
                    }

                    if (DEBUG) {
                        System.out.println("-----> start inflating children");
                    }

                    // Inflate all children under temp against its context.
                    // 关键代码3,递归的解析出以 temp 为父节点的所有子 View 或者子 ViewGroup
                    rInflateChildren(parser, temp, attrs, true);

                    if (DEBUG) {
                        System.out.println("-----> done inflating children");
                    }

                    // We are supposed to attach all the views we found (int temp)
                    // to root. Do that now.
                    if (root != null && attachToRoot) {
                        // 关键代码4,将temp添加到根View:root中
                        root.addView(temp, params);
                    }

                    // Decide whether to return the root that was passed in or the
                    // top view found in xml.
                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }

            } catch (XmlPullParserException e) {
                final InflateException ie = new InflateException(e.getMessage(), e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
            } catch (Exception e) {
                final InflateException ie = new InflateException(parser.getPositionDescription()
                        + ": " + e.getMessage(), e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
            } finally {
                // Don't retain static reference on context.
                mConstructorArgs[0] = lastContext;
                mConstructorArgs[1] = null;

                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }

            return result;
        }
    }

看着代码那么多,是不是傻眼了,哈哈,不过不用着急,只需要看上面我汉语注释了的那几句代码,下面我们结合图来具体说明,相信你也会更加明白。

clipboard.png

方法 rInflateChildren(...)顾名思义就是递归的解析出所有的子View,我们看他的代码,里面是直接的调用了 rInflate(...)方法的

    final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
            boolean finishInflate) throws XmlPullParserException, IOException {
        rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
    }

我们也直接在这里粘贴出来的源码,一起进行比较。

   void rInflate(XmlPullParser parser, View parent, Context context,
            AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {

        final int depth = parser.getDepth();
        int type;

        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)) {
                throw new InflateException("<merge /> must be the root element");
            } else {
                // 重点注意以下几行
                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);
            }
        }

        // 此处是进行加载完成之后的回调,目前为止传进来的参数 finishInflate 均为 true ,因而当所有的子Viewz都加载完毕的时候,就会回调父容器的 onFinishInflate 方法,在 ViewGroup 中,该方法为空实现
        if (finishInflate) {
            parent.onFinishInflate();
        }
    }

上面这么多代码看起来很乱,但是我们只要重点抓住几条,就可以让思路变得更加清晰,上面那么多代码,核心只有下面几句:

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

然后就只是递归的问题了。可以总结为下面的几句:

  1. createViewFromTag(...)负责根据指定的 name 生成具体的 View

  2. rInflateChildren(...)负责递归调用 rInflate(...),使得该过程反复被执行,直到最后遍历完所有的View,不再进入While循环,直接调用parent.onFinishInflate(),表明遍历完结,然后就会在栈中一步步的向上调用 parent.onFinishInflate()方法来通知父容器,本View已经解析完成.

  3. viewGroup.addView(view, params),该句负责把每次解析出来的 View 都添加进本次的根 ViewGroup 中,这样最终会将其下所有的子 View 都添加进来。

自此,我们就将 LayoutInflate 解析 xml ,从而生成 View树 的过程讲解完了,当然,具体的内容还有很多,接下来我会一步步的去讲解,在本篇中的一些迷惑,你也会一一解开,那么下一篇,就让我们从 createViewFromTag(...)讲起吧!

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值