LayoutInflate inflate深入理解

一、inflate的基本使用

inflate方法非常基础且常用,但是好像很多人都用错了,比如说自定义view的时候多了一层父布局等。刚好再处理inflate的优化,所以总结一下我理解的inflate()方法,(如有内容错误,还麻烦指出,大家一起进步~)

好像除了activity的onCreate()方法内可以调用setContentView()之外,加载一个布局都需要使用Inflate()方法。

LayoutInflater.from(context).inflate(@LayoutRes int resource, @Nullable ViewGroup root)
View.inflate(Context context, @LayoutRes int resource, ViewGroup root)
Activity.setContentView(@LayoutRes int layoutResID)

其实这三个方法底层逻辑都是LayoutInflater#inflate方法。

二、inflate 详细解析

 public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
   ...
}

2.1参数解释

resource:加载的layoutId

rootattachToRoot 结合起来理解:当root 可以为null,表示直接加载layout,不做任何处理,attachToRoot没有任何意义。如果不为null 且 attachToRoot为true,那么就会把解析的layout添加到root里面。如果attachToRoot 为false,那么只是限制了这个根节点的部分属性(换句话说xml中根节点的属性不一定全部都会生效,具体要看root支持哪些)

接下来一行一行看代码:

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) + ")");
    }
    //这个方案android还不支持,具体可以看我之前的一篇分析。所以这个view一定是null
    View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
    if (view != null) {
        return view;
    }
    //拿到parser 进入关键函数
    XmlResourceParser parser = res.getLayout(resource);
    try {
        return inflate(parser, root, attachToRoot);
    } finally {
        parser.close();
    }
}

这里面有两个知识点

三、涉及知识点

1、tryInflatePrecompiled(resource, res, root, attachToRoot)

这个方案android还不支持,具体可以看我之前的一篇分析。所以这个view一定是null

2、XmlResourceParser

XmlResourceParser是一个xml解析工具,通过res.getLayout(resource)获取,通过调用next()方法遍历XmlResourceParser,可以获取xml中所有内容。parser内部有个类似于指针的东西,执行一次next()方法后,指针就会指向下一个节点,通过demo验证,他是一个一个标签深度遍历的。

具体可以看这篇文章https://www.jianshu.com/p/d3c801584f8f

[图片上传失败...(image-5f8f2c-1663308292830)]

接下来看最重要的方法inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)

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

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

        try {
            //直接进入根节点,因为一份xml可能存在一些其他标签,执行这个之后parser指针指向根节点
            advanceToRootNode(parser);
            //拿到根节点的标签名
            final String name = parser.getName();

            if (DEBUG) {
                System.out.println("**************************");
                System.out.println("Creating root view: "
                        + name);
                System.out.println("**************************");
            }
            //根节点是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");
                }
                //4:解析根节点为merge的layout
                rInflate(parser, root, inflaterContext, attrs, false);
            } else {
                // Temp is the root view that was found in the xml
                //5:实例化根节点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
                    //6:获取根节点的attr
                    params = root.generateLayoutParams(attrs);
                    if (!attachToRoot) {
                        // Set the layout params for temp if we are not
                        // attaching. (If we are, we use addView, below)
                        //设置根节点的params
                        temp.setLayoutParams(params);
                    }
                }

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

                // Inflate all children under temp against its context.
                //7:解析根节点内部的子view
                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) {
                    //将根节点添加到root上,使用的布局参数是layout中定义的。
                    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(
                    getParserStateDescription(inflaterContext, attrs)
                            + ": " + 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;
    }
}

这个方法很重要,只要稍微看漏一点就理解错了。

3、final AttributeSet attrs = Xml.asAttributeSet(parser)

public static AttributeSet asAttributeSet(XmlPullParser parser) {
    return (parser instanceof AttributeSet)
            ? (AttributeSet) parser
            : new XmlPullAttributes(parser);
}

这个方法其实返回的就是他自己。但是AttributeSet相对与XmlResourceParser来说,少了next()方法,通过这个attr,只能获取xml中某个节点。这边需要了解的一点是attrs 和parse是同一个对象,当parse执行next()方法时,通过attr解析的节点就不是同一个了。

4、merge标签

解析根节点有两种情况,一种是以<merge>开头的,一种是其他类型的。

1、<merge>开头的 root 不能为 null 且attachToRoot要为true

merge简单来理解就是跳过根节点标签,将子view全部添加到root里。

void rInflate(XmlPullParser parser, View parent, Context context,
              AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
    //获取parser的深度
    final int depth = parser.getDepth();
    int type;
    boolean pendingRequestFocus = false;

    //while循环遍历xml:当下一个节点不是</> 或者 当前指针指向的节点在内部
    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();
        //4.1 遍历节点
        if (TAG_REQUEST_FOCUS.equals(name)) {
            pendingRequestFocus = true;
            consumeChildElements(parser);
        } 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 {
            //初始化name所对应的节点view
            final View view = createViewFromTag(parent, name, context, attrs);
            final ViewGroup viewGroup = (ViewGroup) parent;
            //根据viewGroup解析该节点上的attr生成params设置给view
            final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
            //解析子view
            rInflateChildren(parser, view, attrs, true);
            viewGroup.addView(view, params);
        }
    }

4.1遍历节点

从merge这边过来,parser.next()之后就已经指向第二个节点了。节点开始的标签支持三个特殊标签"requestFocus"、"tag"、"include"和其他标签。其中"include"场景使用较多。

其他标签只得就是view了 就是这三步骤

  • 初始化name所对应的节点view
  • 使用viewGroup解析该节点上的attr生成params设置给view,generateLayoutParams会放到后面重点说明
  • 使用递归方式解析子view

5、其他标签

其他标签就是view的了,直接看createViewFromTag。

    View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
                           boolean ignoreThemeAttr) {
        //如果标签名为view,name真实的值为class所对应的值。
        //ps:好像很少看到这样的写法
        if (name.equals("view")) {
            name = attrs.getAttributeValue(null, "class");
        }

        // Apply a theme wrapper, if allowed and one is specified.
        //ignoreThemeAttr = false 解析xml中的theme标签
        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();
        }

        try {
            //5.1:对外暴露的钩子
            View view = tryCreateView(parent, name, context, attrs);

            if (view == null) {
                final Object lastContext = mConstructorArgs[0];
                mConstructorArgs[0] = context;
                try {
                    //5.2:原生view,比如ImageView
                    if (-1 == name.indexOf('.')) {
                        view = onCreateView(context, parent, name, attrs);
                    } else {
                        //5.3:自定义及第三方view
                        view = createView(context, name, null, attrs);
                    }
                } finally {
                    mConstructorArgs[0] = lastContext;
                }
            }

            return view;
        } catch (InflateException e) {
            throw e;

        } catch (ClassNotFoundException e) {
            final InflateException ie = new InflateException(
                    getParserStateDescription(context, attrs)
                            + ": Error inflating class " + name, e);
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw ie;

        } catch (Exception e) {
            final InflateException ie = new InflateException(
                    getParserStateDescription(context, attrs)
                            + ": Error inflating class " + name, e);
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw ie;
        }
    }

5.1 tryCreateView(parent, name, context, attrs);

    public final View tryCreateView(@Nullable View parent, @NonNull String name,
                                    @NonNull Context context,
                                    @NonNull AttributeSet attrs) {
        if (name.equals(TAG_1995)) {
            // Let's party like it's 1995!
            //一个不停闪烁的view,比如时钟上的:闪烁,可以用这个。
            //但是只要被添加到窗口,就会开始闪烁,无法控制他的开始和暂停等,如果需要更多功能的,可以模仿他写一个自定义的。
            return new BlinkLayout(context, attrs);
        }

        View view;
        if (mFactory2 != null) {
            //mFactory2和mFactory是可以由有外部传入的,这个也是对外暴露的方法。
            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);
        }

        return view;
    }

factory有暴露接口设置进来,但是factory只能被设置一次,使用AppCompatActivity都有设置,详情可参考:

https://blog.51cto.com/u_15064646/2575022

factory处理这些方法create方法有几个好处:

  • 一个是不需要走到系统的方法再通过反射去创建view,如果找到相关的view,直接new。
  • 可以创建view的时候统一处理一下,比如xml定义了一个<TextView>,使用AppCompatActivity都会给转成<AppCompatTextView>,也可以改改背景等,网易云之前的换肤方案用的就是这个。
  • 可以打印onCreateView的时间。
@Override
    public void installViewFactory() {
        LayoutInflater layoutInflater = LayoutInflater.from(mContext);
        if (layoutInflater.getFactory() == null) {
            LayoutInflaterCompat.setFactory2(layoutInflater, this);
        } else {
            if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
                Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
                        + " so we can not install AppCompat's");
            }
        }
    }

ps:我使用的appcompat 1.5.0版本,已经是setFactory2了。

5.2 createView(context, name, null, attrs)

如果view为null,就会走原生的方式解析view。view只有前面的factory没有匹配上时为null。

原生处理方式分两种:-1 == name.indexOf('.'),表示name标签没有.,也就是那些不用写包名的控件。其实这个在后面会自动加上前缀:

protected View onCreateView(String name, AttributeSet attrs)
        throws ClassNotFoundException {
    return createView(name, "android.view.", attrs);
}
public final View createView(@NonNull Context viewContext, @NonNull String name,
                             @Nullable String prefix, @Nullable AttributeSet attrs)
        throws ClassNotFoundException, InflateException {
    Objects.requireNonNull(viewContext);
    Objects.requireNonNull(name);
    //先从缓存里找是否有已经有name对应的view
    Constructor<? extends View> constructor = sConstructorMap.get(name);
    if (constructor != null && !verifyClassLoader(constructor)) {
        constructor = null;
        sConstructorMap.remove(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
            //反射找到对应的view
            clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
                    mContext.getClassLoader()).asSubclass(View.class);

            if (mFilter != null && clazz != null) {
                boolean allowed = mFilter.onLoadClass(clazz);
                if (!allowed) {
                    failNotAllowed(name, prefix, viewContext, attrs);
                }
            }
            constructor = clazz.getConstructor(mConstructorSignature);
            constructor.setAccessible(true);
            sConstructorMap.put(name, constructor);
        } else {
            // If we have a filter, apply it to cached constructor
            if (mFilter != null) {
                // Have we seen this name before?
                Boolean allowedState = mFilterMap.get(name);
                if (allowedState == null) {
                    // New class -- remember whether it is allowed
                    clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
                            mContext.getClassLoader()).asSubclass(View.class);

                    boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
                    mFilterMap.put(name, allowed);
                    if (!allowed) {
                        failNotAllowed(name, prefix, viewContext, attrs);
                    }
                } else if (allowedState.equals(Boolean.FALSE)) {
                    failNotAllowed(name, prefix, viewContext, attrs);
                }
            }
        }

        Object lastContext = mConstructorArgs[0];
        mConstructorArgs[0] = viewContext;
        Object[] args = mConstructorArgs;
        args[1] = attrs;

        try {
            final View view = constructor.newInstance(args);
            if (view instanceof ViewStub) {
                // Use the same context when inflating ViewStub later.
                final ViewStub viewStub = (ViewStub) view;
                viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
            }
            return view;
        } finally {
            mConstructorArgs[0] = lastContext;
        }
    } catch (NoSuchMethodException e) {
        final InflateException ie = new InflateException(
                getParserStateDescription(viewContext, attrs)
                        + ": Error inflating class " + (prefix != null ? (prefix + name) : name), e);
        ie.setStackTrace(EMPTY_STACK_TRACE);
        throw ie;

    } catch (ClassCastException e) {
        // If loaded class is not a View subclass
        final InflateException ie = new InflateException(
                getParserStateDescription(viewContext, attrs)
                        + ": Class is not a View " + (prefix != null ? (prefix + name) : name), e);
        ie.setStackTrace(EMPTY_STACK_TRACE);
        throw ie;
    } catch (ClassNotFoundException e) {
        // If loadClass fails, we should propagate the exception.
        throw e;
    } catch (Exception e) {
        final InflateException ie = new InflateException(
                getParserStateDescription(viewContext, attrs) + ": Error inflating class "
                        + (clazz == null ? "<unknown>" : clazz.getName()), e);
        ie.setStackTrace(EMPTY_STACK_TRACE);
        throw ie;
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}

走原生的方式就是通过反射去实例化name对应的view,从mConstructorSignature可以看出来,调用的构造方法是两个参数的。到这里,view就创建完成了。

static final Class<?>[] mConstructorSignature = new Class[] {
            Context.class, AttributeSet.class};

6、root.generateLayoutParams(attrs)

这个方法见过很多次了。这里需要理解的有两个点

  • attr所对应的内容不是固定的,他随着parse指针的变化,获取到的attr也是变化的。按照上面的流程,可以确定attr和当前name随对应的节点是一一对应的。

  • attr中的属性并不是所有的都会生效,取决于root的generateLayoutParams方法,root支持解析哪些属性,那么就只有那些属性会生效。

    比如FrameLayout只会解析宽高layout_gravity、layout_width、layout_height,LinearLayout还会解析layout_weight,其他的viewGroup解析的就更多了。

     public LayoutParams(@NonNull Context c, @Nullable AttributeSet attrs) {
                super(c, attrs);
    
                final TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.FrameLayout_Layout);
                gravity = a.getInt(R.styleable.FrameLayout_Layout_layout_gravity, UNSPECIFIED_GRAVITY);
                a.recycle();
            }
    
    public LayoutParams(Context c, AttributeSet attrs) {
        super(c, attrs);
        TypedArray a =
                c.obtainStyledAttributes(attrs, com.android.internal.R.styleable.LinearLayout_Layout);
    
        weight = a.getFloat(com.android.internal.R.styleable.LinearLayout_Layout_layout_weight, 0);
        gravity = a.getInt(com.android.internal.R.styleable.LinearLayout_Layout_layout_gravity, -1);
    
        a.recycle();
    }
    

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

嵌套调用解析子view。

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