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(" 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标签

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

1、开头的 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(" 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 {
            //初始化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定义了一个,使用AppCompatActivity都会给转成,也可以改改背景等,网易云之前的换肤方案用的就是这个。
  • 可以打印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 constructor = sConstructorMap.get(name);
    if (constructor != null && !verifyClassLoader(constructor)) {
        constructor = null;
        sConstructorMap.remove(name);
    }
    Class 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 ? "" : 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。

你可能感兴趣的:(LayoutInflate inflate深入理解)