Android 布局文件加载 LayoutInflater 源码解析

概述

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内存当中以及整个视图树的加载(这个以了解为目标,知道就可以)。
主要以通过分析源码的方式来理解整个流程,这样会比通过几个案例的结果来分析印象会更加的深刻;

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,439评论 25 707
  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 6,357评论 0 17
  • 用了Axure有一段日子了,没有那么精通但是基本操作已经算是很熟悉了。 这个软件很无脑~但是很多东西真的不容易找~...
    奈何晨曦阅读 570评论 0 0
  • 用一场音乐会来结束一周的工作日无疑是件非常享受的事。今晚是余隆先生执棒上海交响乐团演绎贝多芬和勃拉姆斯,非常期待。...
    花间星事阅读 708评论 3 21
  • 你看天上云的形状 看阳光下绿叶眨动眼睛 春风十里和暖阳 空调西瓜和冰激凌 棉麻布裙和银杏叶 帽子手套围巾飘雪和热水...
    埃莉诺小姐阅读 142评论 0 0