源码分析android layoutinflater的使用-kb88凯时官网登录

来自:网络
时间:2023-05-29
阅读:

layoutinflater

开头先附一段layoutinflater类的注释简介

/**
 * instantiates a layout xml file into its corresponding {@link android.view.view}
 * objects. it is never used directly. instead, use
 * {@link android.app.activity#getlayoutinflater()} or
 * {@link context#getsystemservice} to retrieve a standard layoutinflater instance
 * that is already hooked up to the current context and correctly configured
 * for the device you are running on.
 *
 * to create a new layoutinflater with an additional {@link factory} for your
 * own views, you can use {@link #cloneincontext} to clone an existing
 * viewfactory, and then call {@link #setfactory} on it to include your
 * factory.
 *
 * for performance reasons, view inflation relies heavily on pre-processing of
 * xml files that is done at build time. therefore, it is not currently possible
 * to use layoutinflater with an xmlpullparser over a plain xml file at runtime;
 * it only works with an xmlpullparser returned from a compiled resource
 * (r.something file.)
 */

这是layoutinflater开头的一段介绍,我们能看到几个重要的信息:

  • layoutinfalter的作用是把xml转化成对应的view对象,需要用activity#getlayoutinflater()或者getsystemservice获取,会绑定当前的context
  • 如果需要使用自定义的factory查看替换加载信息,需要用cloneincontext去克隆一个viewfactory,然后调用setfactory设置自定义的factory
  • 性能原因,inflate会在build阶段进行,无法用来加载一个外部文本xml文件,只能加载r.xxx.xxx这种处理好的资源文件。
//layoutinflater.java
    public view inflate(@layoutres int resource, @nullable viewgroup root) {
	//root是否为null来决定attachtoroot是否为true。
        return inflate(resource, root, root != null);
    }
    public view inflate(xmlpullparser parser, @nullable viewgroup root) {
        return inflate(parser, root, root != null);
    }
    public view inflate(@layoutres int resource, @nullable viewgroup root, boolean attachtoroot) {
        final resources res = getcontext().getresources();
        ...
        final xmlresourceparser parser = res.getlayout(resource);
        try {
            return inflate(parser, root, attachtoroot);
        } finally {
            parser.close();
        }
    }
//三个inflate方法最终都会调用到下面这个三个参数的inflate方法。
    /**
     * parser xml节点包含了view的层级描述
     * root 需要attached到的根目录,如果attachtoroot为true则root必须不为null。
     * attachtoroot 加载的层级是否需要attach到rootview,
     * return attachtoroot为true,就返回root,反之false就返回加载的xml文件的根节点view。
     */
    public view inflate(xmlpullparser parser, @nullable viewgroup root, boolean attachtoroot) {
        synchronized (mconstructorargs) {
            final context inflatercontext = mcontext;
            final attributeset attrs = xml.asattributeset(parser);
            context lastcontext = (context) mconstructorargs[0];
            mconstructorargs[0] = inflatercontext;
            view result = root;
            try {
                // look for the root node.
                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();
...
                if (tag_merge.equals(name)) {
                    if (root == null || !attachtoroot) {
                        throw new inflateexception(" can be used only with a valid "   "viewgroup root and attachtoroot=true");
                    }
                    rinflate(parser, root, inflatercontext, attrs, false);
                } else {
                    // temp is the root view that was found in the xml
                    final view temp = createviewfromtag(root, name, inflatercontext, attrs);
                    viewgroup.layoutparams params = null;
                    if (root != null) {
                        // create layout params that match root, if supplied
                        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);
                        }
                    }
                    rinflatechildren(parser, temp, attrs, true);
...
                    // we are supposed to attach all the views we found (int temp) to root. do that now.
                    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.
                    if (root == null || !attachtoroot) {
                        result = temp;
                    }
                }
            } catch (xmlpullparserexception e) {
...
            }
            return result;
        }
    }

inflate方法使用xmlpullparser解析xml文件,并根据得到的标签名执行不同的逻辑:

  • 首先如果是merge标签,会走rinflate方法,方法前面带r的说明是recurse递归方法
  • 如果不是merge标签,执行createviewfromtag,根据传入的nameattrs获取到name对应的rootview并且添加到root里面。

针对merge标签,如果是merge标签必须有root并且必须attachtoroot==true,否则直接抛异常,所以我们得知merge必须作为root标签使用,并且不能用在子标签中①,rinflate方法中也会针对merge标签进行检查,保证merge标签不会出现在子标签中,后面会有介绍。
检查通过则调用rinflate(parser, root, inflatercontext, attrs, false)方法,递归遍历root的层级,解析加载childrenview挂载到parentview下面,rinflate详细解析可以看。

如果不是merge标签则调用createviewfromtag(root, name, inflatercontext, attrs),这个方法的作用是加载名字为name的view,根据name反射方式创建对应的view,根据传入的attrs构造params设置给view,返回创建好的view。
当然这只是创建了一个view,需要再调用rinflatechildren(parser, temp, attrs, true),这个方法也是一个递归方法,它的作用是根据传入的parser包含的层级,加载此层级的子view并挂载到temp下面。

createviewfromtag

    view createviewfromtag(view parent, string name, context context, attributeset attrs,
            boolean ignorethemeattr) {
        if (name.equals("view")) {
            name = attrs.getattributevalue(null, "class");
        }
        // apply a theme wrapper, if allowed and one is specified.
        // 如果传入的attr中包含theme属性,则使用此attr中的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();
        }
        if (name.equals(tag_1995)) {
            // let's party like it's 1995!
            return new blinklayout(context, attrs);
        }
        try {
            view view;
            if (mfactory2 != null) {
                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);
            }
            if (view == null) {
                final object lastcontext = mconstructorargs[0];
                mconstructorargs[0] = context;
                try {
                    if (-1 == name.indexof('.')) {
                        view = oncreateview(parent, name, attrs);
                    } else {
                        view = createview(name, null, attrs);
                    }
                } finally {
                    mconstructorargs[0] = lastcontext;
                }
            }
            return view;
        } catch (exception e) {
            ...
        }
    }

先看当前标签的attr属性里面是否设置了theme,如果设置了就用当前标签的theme属性,绑定到context上面。 这里很有意思的是特殊判断了一个tag_1995,也就是blink,一个将包裹的内容每隔500ms显示隐藏的一个标签,怎么看都像个彩蛋~

然后调用mfactory2oncreateview,如果没有设置mfactory2就尝试mfactory,否则调用mprivatefactory,mfactory2和mfactory后面再说,这里先往后走。

如果还是没有加载到view,先判断name,看名字里是不是有.,如果没有就表明是android原生的view,最终都会调用到createview方法,oncreateview最终会调用到createview(name, "android.view.", attrs);,会在view名字天面添加"android.view."前缀。

下面是默认的createview的实现:

    @nullable
    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);
        constructor constructor = sconstructormap.get(name);
        if (constructor != null && !verifyclassloader(constructor)) {
            constructor = null;
            sconstructormap.remove(name);
        }
        class clazz = null;
        try {
            if (constructor == null) {
                // class not found in the cache, see if it's real, and try to add it
                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 (exception e) {
            ...
        } finally {
            trace.traceend(trace.trace_tag_view);
        }
    }

这个方法可以看到view是怎么创建出来的,用类的全限定名拿到class信息,有一个sconstructormap缓存类的constructor,如果能拿到有效的构造器就不再重复创建来提升效率,如果没有缓存的构造器,就反射得到构造器并添加到sconstructormap中以便后面使用。这里有个mfilter来提供自定义选项,用户可以自定义哪些类不允许构造。

拿到构造器之后,实际上newinstance是调用了两view个参数的构造方法。第一个参数是context,第二个参数是attrs,这样我们就得到了需要加载的view。

这里可以结合layoutinflater.factory2一起来看,activity实际上是实现了layoutinflater.factory2接口的:

//activity.java
    public view oncreateview(@nonnull string name, @nonnull context context,
            @nonnull attributeset attrs) {
        return null;
    }

所以我们可以直接在activity里面重写oncreateview方法,这样就可以根据view的名字来实现我们的一些操作,比如换肤的操作,比如定义一个名字来表示某种自定义view。可以看这样一个用法:

    

然后我们在重写的oncreateview里面判断name:

    @override
    public view oncreateview(string name, context context, attributeset attrs) {
        if ("placeholder".equals(name)) {
            return new textview(this, attrs);
        }
        return super.oncreateview(name, context, attrs);
    }

这样其实就不拘泥于名字可以自己创建对应的view,这样其实可以用在多个module依赖的时候,如果在modulea中得不到moduleb的某个自定义view,可以使用一个这样的方式来在moudlea中暂时的用来做一个占位标记,在moduleb中做一个判断。

同样的,通过上面的代码我们知道layoutinflater是通过反射拿到构造方法来创建view的,那众所周知反射是有性能损耗的,那么我们可以在oncreateview方法中判断名字直接new出来,当然也可以跟appcompatactivity里面做的一样,做一些兼容的操作来替换成不同版本的view:

public final view createview(view parent, final string name, @nonnull context context,
        view view = null;
        switch (name) {
            case "textview":
                view = new appcompattextview(context, attrs);
                break;
            case "imageview":
                view = new appcompatimageview(context, attrs);
                break;
            case "button":
                view = new appcompatbutton(context, attrs);
                break;
            case "edittext":
                view = new appcompatedittext(context, attrs);
                break;
            case "spinner":
                view = new appcompatspinner(context, attrs);
                break;
            case "imagebutton":
                view = new appcompatimagebutton(context, attrs);
                break;
            ...
        }
...
        return view;
    }

还没有展开说rinflate,篇幅限制,放到另外一篇文章中去分析,。

流程图如下:

总结

  • layoutinfalter的作用是把xml转化成对应的view对象,需要用activity#getlayoutinflater()或者getsystemservice获取
  • 加载时先判断是否是merge标签,merge标签走递归方法rinflate,否则走createviewfromtag
  • createviewfromtag作用是根据xml标签的名字去加载对应的view,使用的是反射的方法
  • layoutinflater.factory2是设计出来灵活构造view的接口,可以用来实现换肤或者替换view的功能,同时也是appcompatactivity用来做兼容和版本替换的接口

以上就是源码分析android layoutinflater的使用的详细内容,更多关于android layoutinflater的资料请关注其它相关文章!

返回顶部
顶部
网站地图