美文网首页Android源码分析我爱编程
Android 布局文件加载 LayoutInflater 源码

Android 布局文件加载 LayoutInflater 源码

作者: hewenyu | 来源:发表于2018-05-25 12:57 被阅读89次

概述

Android 开发的过程中,我们肯定会使用到布局加载,无论是Activity、Fragment的创建,还是 ListView 适配器中 convertView 视图的加载,最常用的方式都是将一个布局文件加载成一系列的 View 对象,然后才会继续进行相应的操作;
这时我们就会想,Android 系统是如何将我们的布局文件加载成一系列的View对象、加载布局文件时传入的其它参数有什么作用;


@A@

布局文件的加载方式

我们先来看下Activity中加载视图的方法:

public void setContentView(int layoutResID) {
    getWindow().setContentView(layoutResID);
    initActionBar();
}

可以看出Activity的 setContentView() 方法实际上调用的是 Window 类里面的方法,而 window 类是一个抽象类且只有一个实现类 PhoneWindow (这里不清楚的可以网上找下资料),所以我们直接来看 PhoneWindow 里面的具体实现方法:

 @Override
public void setContentView(int layoutResID) {
    if (mContentParent == null) {
        installDecor();
    } else {
        mContentParent.removeAllViews();
    }
    mLayoutInflater.inflate(layoutResID, mContentParent);
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
}

我们可以看到,我们传进来的布局文件最终是交给了 LayoutInflater 对象来加载,同时传入了一个 mContentParent 对象,从上面的代码我们可以分析出,mContentParent 对象是一个 ViewGroup 类型的对象;

接下来我们看一下另外一种常见的视图加载方式,通过 View 的静态方法 inflate() 加载:

public static View inflate(Context context, @LayoutRes int resource, ViewGroup root) {
    LayoutInflater factory = LayoutInflater.from(context);
    return factory.inflate(resource, root);
}

很明显此方法也是通过 LayoutInflater 对象来加载布局文件;

LayoutInflater 源码解析

根据上面的解析,我们知道,不管是 Activity 中的 setContentView() 还是通过View.inflate() ,最终都是调用了 LayoutInflater.inflate() 来加载布局文件;
我们直接来看inflate()主要几个重载方法的源码:

public View inflate(XmlPullParser parser, ViewGroup root) {
    return inflate(parser, root, root != null);
}

public View inflate(int resource, ViewGroup root, boolean attachToRoot) {
    // 将布局文件交给 xml 解析器
    XmlResourceParser parser = getContext().getResources().getLayout(resource);
    try {
        return inflate(parser, root, attachToRoot);
    } finally {
        parser.close();
    }
}

public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
    synchronized (mConstructorArgs) {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
        
        final AttributeSet attrs = Xml.asAttributeSet(parser);
        Context lastContext = (Context)mConstructorArgs[0];
        // 上下文对象
        mConstructorArgs[0] = mContext;
        // 前面说到过的传递进来的 ViewGroup(可以为null)
        View result = root;

        try {
            // Look for the root node.
            // 寻找布局的根节点(最外层的View)
            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();
            // merge 标签
            if (TAG_MERGE.equals(name)) {
                ... 省略代码 ...
                rInflate(parser, root, attrs, false);
            } else {
              
                // 布局中的根视图
                View temp;
                if (TAG_1995.equals(name)) {
                    temp = new BlinkLayout(mContext, attrs);
                } else {
                    // 根据 tag 创建根视图
                    temp = createViewFromTag(root, name, attrs);
                }
                ViewGroup.LayoutParams params = null;
                if (root != null) {
                    // Create layout params that match root, if supplied
                    // 如果传递进来的ViewGroup不为空,则在这里添加 LayoutParam 参数
                    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);
                    }
                }
                
                // Inflate all children under temp
                // 加载子控件
                rInflate(parser, temp, attrs, true);

                // We are supposed to attach all the views we found (int temp)
                // to root. Do that now.
                // 如果传进来的ViewGroup不为null,同时第三个参数为true,
                // 则将加载完成的View作为child添加到 ViewGroup 中
                // 同时返回的 View 为原来的ViewGroup
                if (root != null && attachToRoot) {
                    root.addView(temp, params);
                }

                // Decide whether to return the root that was passed in or the
                // top view found in xml.
                // 如果传递进来的ViewGroup为空或者第三个参数为false
                // 则将从布局文件中加载的View返回
                if (root == null || !attachToRoot) {
                    result = temp;
                }
            }

        } catch (XmlPullParserException e) {
            ... 省略 catch ...
        } finally {
            // Don't retain static reference on context.
            mConstructorArgs[0] = lastContext;
            mConstructorArgs[1] = null;
        }
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        return result;
    }
}

通过分析源码,我们可以知道布局文件是通过 pull解析器 来解析整个布局文件的;
先粗略的看下整个方法中的代码逻辑,从中我们可以分析出inflate() 方法参数的作用,代码中也写了注释,这里做一个归纳:

  1. 如果root(ViewGroup)不为null,attachToRoot(boolean)默认为true;
  2. 如果root(ViewGroup)不为null,且 attachToRoot(boolean)为true,则从布局文件加载上来的View会被当作child加入到root里面,同时将 root 当作返回值(这里就可以理解为什么一定要传入ViewGroup而不是View了);
  3. 如果root(ViewGroup)不为null,且 attachToRoot(boolean)为false,那么从布局文件加载上来的View将会有 layout 相关的属性(layout_width等),同时加载的View将会作为返回值;
  4. 如果root(ViewGroup)为null,那么,attachToRoot(boolean)参数将是一个无效的参数,此时从布局加载上来的顶层View的layout相关属性将会失效,同时加载的View将会作为返回值;

以上几点就是对加载布局文件时,几个参数的理解,此时我们还有一个问题没有解决,就是我们的View是如何从布局文件中创建的还不清楚,只是知道调用了 xml 解析器来解析了我们的布局文件(解析出来的数据还没有转换成Java内存中的对象);

关于View的创建以及ViewGroup的遍历

首先我们来考虑一个问题,就是布局文件中的View是如何转换成我们Java内存中的View的对象,也就是我们的View是如何被创建的,这里我们首先想到了反射,通过反射我们可以在代码运行的时候来创建对象;

@A@
我们可以仔细的阅读一下上面那段代码,可以找都一个很关键的点:temp = createViewFromTag(root, name, attrs);,这里传递进来的参数为ViewGroup、以及我们通过 xml 解析拿到的根视图的 name 和 属性;
View createViewFromTag(View parent, String name, AttributeSet attrs) {
    if (name.equals("view")) {
        name = attrs.getAttributeValue(null, "class");
    }
    try {
        View view;
        // 这些factory参数我们都没有设置过,因此我们直接跳过这里
        if (mFactory2 != null) view = mFactory2.onCreateView(parent, name, mContext, attrs);
        else if (mFactory != null) view = mFactory.onCreateView(name, mContext, attrs);
        else view = null;
        if (view == null && mPrivateFactory != null) {
            view = mPrivateFactory.onCreateView(parent, name, mContext, attrs);
        }
        // 正常情况下我们的view创建走的是这一段代码
        // 这里将会来判断我们的View是系统控件还是我们的自定义控件      
        // 这里也很好的解释了我们的自定义控件为什么需要写全类名,而系统控件只需要写类名称即可
        if (view == null) {
            if (-1 == name.indexOf('.')) {  // 系统控件
                view = onCreateView(parent, name, attrs);
            } else {    // 自定义控件
                view = createView(name, null, attrs);
            }
        }
        return view;
    } catch (Exception e) {
        .....
    }
}

这里如果使用过自定义控件的应该都知道,在布局文件中使用自定义控件,我们需要写入全类名(文件夹之间用 . 来分隔),而如果是使用系统控件,我们只需要写入控件的类名即可,这么设计的原因就在于系统控件的包名是指定的,可以通过拼接的方式拿到全类名,而自定义控件的包路径是由我们自己定义的,因此在布局文件中使用的时候需要指定全类名,我们可以通过分析源码来验证我们上面所说的:

protected View onCreateView(View parent, String name, AttributeSet attrs)
        throws ClassNotFoundException {
    return onCreateView(name, attrs);
}

protected View onCreateView(String name, AttributeSet attrs)
        throws ClassNotFoundException {
    // 返回系统控件的包名
    return createView(name, "android.view.", attrs);
}

在拿到我们系统控件的包名后,同样也是调用了 createView() 方法,显然我们能够根据方法名看出来此方法就是用来创建我们的View的,直接上代码:

public final View createView(String name, String prefix, AttributeSet attrs)
        throws ClassNotFoundException, InflateException {
    // 从缓存中获取我们的构造函数
    Constructor<? extends View> constructor = sConstructorMap.get(name);
    Class<? extends View> clazz = null;

    try {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);
        // 缓存中没有,就通过我们前面拿到的完整包名利用反射来家在
        if (constructor == null) {
            // Class not found in the cache, see if it's real, and try to add it
            // 这里如果是系统控件就拼接包名,如果是自定义控件就name就为完整的包名
            clazz = mContext.getClassLoader().loadClass(
                    prefix != null ? (prefix + name) : name).asSubclass(View.class);
            
            if (mFilter != null && clazz != null) {
                boolean allowed = mFilter.onLoadClass(clazz);
                if (!allowed) {
                    failNotAllowed(name, prefix, attrs);
                }
            }
            constructor = clazz.getConstructor(mConstructorSignature);
            // 这里将我们拿到的构造函数缓存起来
            sConstructorMap.put(name, constructor);
        } else {
            ... 省略代码 ...
        }

        Object[] args = mConstructorArgs;
        args[1] = attrs;
        // 这里就是通过反射来创建我们的View对象;
        final View view = constructor.newInstance(args);
        if (view instanceof ViewStub) {
            // always use ourselves when inflating ViewStub later
            final ViewStub viewStub = (ViewStub) view;
            viewStub.setLayoutInflater(this);
        }
        return view;
    } catch (Exception e) {
        ... 这里我们不看异常 ...         
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}

整个方法大致就是通过反射来创建对应的View对象,如果是第一次加载则会将我们的构造方法缓存起来方便下次使用;到这里我们就能理解我们的View是如何从布局文件中加载到Java内存中的对象,使用的是我们前面所说的Java的反射机制(关于Java的反射机制可以去网上找资料);
我们都知道Android中的视图是以树的形式展现的,这里我们只是加载了我们的根视图,其它子View的加载又是在什么时候,这里细心的朋友可能已经注意到了,我们的 inflate() 方法中还有另外一个关键的方法rInflate(),代码中我也写了注释,接下来我们直接来看下这个方法的源码:

void rInflate(XmlPullParser parser, View parent, final 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_INCLUDE.equals(name)) {  // 解析include标签
            if (parser.getDepth() == 0) {
                throw new InflateException("<include /> cannot be the root element");
            }
            parseInclude(parser, parent, attrs);
        } else if (TAG_MERGE.equals(name)) {    // 解析merge标签
            throw new InflateException("<merge /> must be the root element");
        } else if (TAG_1995.equals(name)) {
           ... 省略代码 ...
        } else {
            // 这里有没有很熟悉,我们前面的根视图也是通过这个方法来加载的
            final View view = createViewFromTag(parent, name, attrs);
            final ViewGroup viewGroup = (ViewGroup) parent;
            final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
            // 通过递归的方式遍历加载整个视图树(深度优先)
            rInflate(parser, view, attrs, true);
            // 将我们解析得到的View添加到我们传递进来的parent中
            viewGroup.addView(view, params);
        }
    }
    if (finishInflate) parent.onFinishInflate();
}

这里同样是根据我们前面所说的createViewFromTag(parent, name, attrs)方法来创建我们的View,然后通过递归的方式,遍历整个视图树(跟以前学习代码的时候,递归遍历windows文件夹系统非常类似),然后将我们新创建的View添加到传进来的ViewGroup中,直至遍历完最后一个View;

到这里我们就可以理解为什么google官方推荐我们布局文件的深度最好不要超过三层,因为每增加一层视图树,都会增加我们遍历的时间,从而影响性能;

总结

本片文章主要是讲解了Android系统中是如何加载布局文件的,首先讲解了不同的加载方式其实都是调用了同一个类对象来加载的,然后就是讲解了布局文件加载中,各个参数的作用(这个在开发中非常有用),最后就是分析了下布局文件中的控件是如何加载到Java内存当中以及整个视图树的加载(这个以了解为目标,知道就可以)。
主要以通过分析源码的方式来理解整个流程,这样会比通过几个案例的结果来分析印象会更加的深刻;

相关文章

网友评论

本文标题:Android 布局文件加载 LayoutInflater 源码

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