美文网首页
LayoutInflate inflate深入理解

LayoutInflate inflate深入理解

作者: 小马要加油 | 来源:发表于2022-09-15 14:04 被阅读0次

    一、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。

    相关文章

      网友评论

          本文标题:LayoutInflate inflate深入理解

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