博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Android中LayoutInflate解析xml布局文件生成View树的过程(一)
阅读量:6965 次
发布时间:2019-06-27

本文共 13389 字,大约阅读时间需要 44 分钟。

学习过自定义 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("
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("
cannot be the root element"); } parseInclude(parser, context, parent, attrs); } else if (TAG_MERGE.equals(name)) { throw new InflateException("
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(...)讲起吧!

转载地址:http://cmwsl.baihongyu.com/

你可能感兴趣的文章
win2008 防火墙 ipsec策略 路由和远程访问nat映射
查看>>
第一讲概述
查看>>
一场版本升级引发的性能血案 - 王者归来
查看>>
httpd 服务器的三大引擎 prefork、worker、event分析
查看>>
schedule和scheduleAtFixedRate
查看>>
golang之runtime.SetFinalizer
查看>>
tomcat 内存溢出
查看>>
操作用户 简介
查看>>
JDK工具(一)–Java编译器javac
查看>>
Cassandra数据模型设计
查看>>
JDBC Java SQL Server 连接
查看>>
Maven部署Struts2环境详解
查看>>
日常记录-js篇
查看>>
使用 Java Native Interface 的最佳实践
查看>>
关于 Perl 与 Python 的起源和特点
查看>>
taobao npm registry
查看>>
jenkins------结合maven将svn项目自动部署到tomcat下
查看>>
我的友情链接
查看>>
MySQL二进制包使用mysql_upgrade版本更新升级MySQL 5.7
查看>>
css3文本溢出显示控制
查看>>