Android LayoutInflater加载布局详解及实例代码
android layoutinflater加载布局详解
对于有一定android开发经验的同学来说,一定使用过layoutinflater.inflater()来加载布局文件,但并不一定去深究过它的原理,比如
1.layoutinflater为什么可以加载layout文件?
2.加载layout文件之后,又是怎么变成供我们使用的view的?
3.我们定义view的时候,如果需要在布局中使用,则必须实现带attributeset参数的构造方法,这又是为什么呢?
既然在这篇文章提出来,那说明这三个问题都是跟layoutinflater脱不了干系的。在我们的分析过程中,会对这些问题一一进行解答。
我们一步一步来,首先当我们需要从layout中加载view的时候,会调用这个方法
layoutinflater.from(context).inflater(r.layout.main_activity,null);
1.如何创建layoutinflater?
这有什么值得说的?如果你打开了layoutinflater.java你自然就明白了,layoutinflater是一个抽象类,而抽象类是不能直接被实例化的,也就是说我们创建的对象肯定是layoutinflater的某一个实现类。
我们进入layoutinflater.from方法中可以看到
public static layoutinflater from(context context) { layoutinflater layoutinflater = (layoutinflater) context.getsystemservice(context.layout_inflater_service); if (layoutinflater == null) { throw new assertionerror("layoutinflater not found."); } return layoutinflater; }
好吧,是获取的系统服务!是从context中获取,没吃过猪肉还没见过猪跑么,一说到context对象十有八九是说得contextimpl对象,于是我们直接去到contextimpl.java中,找到getsystemservice方法
@override public object getsystemservice(string name) { return systemserviceregistry.getsystemservice(this, name); }
额。。。又要去systemserviceregistry.java文件中
/** * gets a system service from a given context. */ public static object getsystemservice(contextimpl ctx, string name) { servicefetcher<?> fetcher = system_service_fetchers.get(name); return fetcher != null ? fetcher.getservice(ctx) : null; }
由代码可知,我们的service是从system_service_fetchers这个hashmap中获得的,而稍微看一下代码就会发现,这个hashmap是在static模块中赋值的,这里注册了很多的系统服务,什么activityservice,什么alarmservice等等都是在这个hashmap中。从layoutinflater.from方法中可以知道,我们找到是context.layout_inflater_service对应的service
registerservice(context.layout_inflater_service, layoutinflater.class, new cachedservicefetcher<layoutinflater>() { @override public layoutinflater createservice(contextimpl ctx) { return new phonelayoutinflater(ctx.getoutercontext()); }});
好啦,主角终于登场了——phonelayoutinflater,我们获取的layoutinflater就是这个类的对象。
那么,这一部分的成果就是我们找到了phonelayoutinflater,具体有什么作用,后面再说。
2.inflater方法分析
这个才是最重要的方法,因为就是这个方法把我们的layout转换成了view对象。这个方法直接就在layoutinflater抽象类中定义
public view inflate(@layoutres int resource, @nullable viewgroup root) { return inflate(resource, root, root != null); }
传入的参数一个是layout的id,一个是是否指定parentview,而真正的实现我们还得往下看
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) + ")"); } final xmlresourceparser parser = res.getlayout(resource); try { return inflate(parser, root, attachtoroot); } finally { parser.close(); } }
我们先从context中获取了resources对象,然后通过res.getlayout(resource)方法获取一个xml文件解析器xmlresourceparser(关于在android中的xml文件解析器这里就不详细讲了,免得扯得太远,不了解的同学可以在网上查找相关资料阅读),而这其实是把我们定义layout的xml文件给加载进来了。
然后,继续调用了另一个inflate方法
public view inflate(xmlpullparser parser, @nullable viewgroup root, boolean attachtoroot) { synchronized (mconstructorargs) { final context inflatercontext = mcontext; //快看,view的构造函数中的attrs就是这个!!! final attributeset attrs = xml.asattributeset(parser); //这个数组很重要,从名字就可以看出来,这是构造函数要用到的参数 mconstructorargs[0] = inflatercontext; view result = root; try { // 找到根节点,找到第一个start_tag就跳出while循环, // 比如<textview>是start_tag,而</textview>是end_tag 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)) { if (root == null || !attachtoroot) { throw new inflateexception("<merge /> can be used only with a valid " + "viewgroup root and attachtoroot=true"); } //解析 rinflate(parser, root, inflatercontext, attrs, false); } else { // 这里需要调用到phonelayoutinflater中的方法,获取到根节点对应的view final view temp = createviewfromtag(root, name, inflatercontext, attrs); viewgroup.layoutparams params = null; //如果指定了parentview(root),则生成layoutparams, //并且在后面会将temp添加到root中 if (root != null) { params = root.generatelayoutparams(attrs); if (!attachtoroot) { temp.setlayoutparams(params); } } // 上面解析了根节点,这里解析根节点下面的子节点 rinflatechildren(parser, temp, attrs, true); if (root != null && attachtoroot) { root.addview(temp, params); } if (root == null || !attachtoroot) { result = temp; } } } catch (exception e) { } finally { // don't retain static reference on context. mconstructorargs[0] = lastcontext; mconstructorargs[1] = null; } return result; } }
这个就稍微有点长了,我稍微去除了一些跟逻辑无关的代码,并且添加了注释,如果有耐心看的话应该是能看懂了。这里主要讲两个部分,首先是rinflatechildren这个方法,其实就是一层一层的把所有节点取出来,然后通过createviewfromtag方法将其转换成view对象。所以重点是在如何转换成view对象的。
3.createviewfromtag
我们一层层跟进代码,最后会到这里
view createviewfromtag(view parent, string name, context context, attributeset attrs, boolean ignorethemeattr) { ...... ...... try { ...... 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 (inflateexception e) { throw e; ...... } }
为了方便理解,将无关的代码去掉了,我们看到其实就是调用的createview方法来从xml节点转换成view的。如果name中不包含'.' 就调用oncreateview方法,否则直接调用createview方法。
在上面的phonelayoutinflater中就复写了oncreateview方法,而且不管是否重写,该方法最后都会调用createview。唯一的区别应该是系统的view的完整类名由oncreateview来提供,而如果是自定义控件在布局文件中本来就是用的完整类名。
4. createview方法
public final view createview(string name, string prefix, attributeset attrs) throws classnotfoundexception, inflateexception { //1.通过传入的类名,获取该类的构造器 constructor<? extends view> constructor = sconstructormap.get(name); class<? extends view> clazz = null; try { if (constructor == null) { 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); constructor.setaccessible(true); sconstructormap.put(name, constructor); } else { if (mfilter != null) { boolean allowedstate = mfiltermap.get(name); if (allowedstate == null) { clazz = mcontext.getclassloader().loadclass( prefix != null ? (prefix + name) : name).assubclass(view.class); boolean allowed = clazz != null && mfilter.onloadclass(clazz); mfiltermap.put(name, allowed); if (!allowed) { failnotallowed(name, prefix, attrs); } } else if (allowedstate.equals(boolean.false)) { failnotallowed(name, prefix, attrs); } } } //2.通过获得的构造器,创建view实例 object[] args = mconstructorargs; args[1] = attrs; final view view = constructor.newinstance(args); if (view instanceof viewstub) { final viewstub viewstub = (viewstub) view; viewstub.setlayoutinflater(cloneincontext((context) args[0])); } return view; } catch (nosuchmethodexception e) { ...... } }
这段代码主要做了两件事情
第一,根据classname将类加载到内存,然后获取指定的构造器constructor。构造器是通过传入参数类型和数量来指定,这里传入的是mconstructorsignature
static final class<?>[] mconstructorsignature = new class[] { context.class, attributeset.class};
即传入参数是context和attributeset,是不是猛然醒悟了!!!这就是为什么我们在自定义view的时候,必须要重写view(context context, attributeset attrs)则个构造方法,才能在layout中使用我们的view。
第二,使用获得的构造器constructor来创建一个view实例。
5.回答问题
还记得上面我们提到的三个问题吗?现在我们来一一解答:
1.layoutinflater为什么可以加载layout文件?
因为layoutinflater其实是通过xml解析器来加载xml文件,而layout文件的格式就是xml,所以可以读取。
2.加载layout文件之后,又是怎么变成供我们使用的view的?
layoutinflater加载到xml文件中内容之后,通过反射将每一个标签的名字取出来,并生成对应的类名,然后通过反射获得该类的构造器函数,参数为context和attributeset。然后通过构造器创建view对象。
3.我们定义view的时候,如果需要在布局中使用,则必须实现带attributeset参数的构造方法,这又是为什么呢?
因为layoutinflater在解析xml文件的时候,会将xml中的内容转换成一个attributeset对象,该对象中包含了在xml文件设定的属性值。需要在构造函数中将这些属性值取出来,赋给该实例的属性。
感谢阅读,希望能帮助到大家,谢谢大家对本站的支持!