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方法中可以看到
publicstaticLayoutInflaterfrom(Contextcontext){
LayoutInflaterLayoutInflater=
(LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if(LayoutInflater==null){
thrownewAssertionError("LayoutInflaternotfound.");
}
returnLayoutInflater;
}
好吧,是获取的系统服务!是从context中获取,没吃过猪肉还没见过猪跑么,一说到context对象十有八九是说得ContextImpl对象,于是我们直接去到ContextImpl.java中,找到getSystemService方法
@Override
publicObjectgetSystemService(Stringname){
returnSystemServiceRegistry.getSystemService(this,name);
}
额。。。又要去SystemServiceRegistry.java文件中
/**
*Getsasystemservicefromagivencontext.
*/
publicstaticObjectgetSystemService(ContextImplctx,Stringname){
ServiceFetcher<?>fetcher=SYSTEM_SERVICE_FETCHERS.get(name);
returnfetcher!=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,
newCachedServiceFetcher<LayoutInflater>(){
@Override
publicLayoutInflatercreateService(ContextImplctx){
returnnewPhoneLayoutInflater(ctx.getOuterContext());
}});
好啦,主角终于登场了——PhoneLayoutInflater,我们获取的LayoutInflater就是这个类的对象。
那么,这一部分的成果就是我们找到了PhoneLayoutInflater,具体有什么作用,后面再说。
2.inflater方法分析
这个才是最重要的方法,因为就是这个方法把我们的layout转换成了View对象。这个方法直接就在LayoutInflater抽象类中定义
publicViewinflate(@LayoutResintresource,@NullableViewGrouproot){
returninflate(resource,root,root!=null);
}
传入的参数一个是layout的id,一个是是否指定ParentView,而真正的实现我们还得往下看
publicViewinflate(@LayoutResintresource,@NullableViewGrouproot,booleanattachToRoot){
finalResourcesres=getContext().getResources();
if(DEBUG){
Log.d(TAG,"INFLATINGfromresource:\""+res.getResourceName(resource)+"\"("
+Integer.toHexString(resource)+")");
}
finalXmlResourceParserparser=res.getLayout(resource);
try{
returninflate(parser,root,attachToRoot);
}finally{
parser.close();
}
}
我们先从context中获取了Resources对象,然后通过res.getLayout(resource)方法获取一个xml文件解析器XmlResourceParser(关于在Android中的xml文件解析器这里就不详细讲了,免得扯得太远,不了解的同学可以在网上查找相关资料阅读),而这其实是把我们定义layout的xml文件给加载进来了。
然后,继续调用了另一个inflate方法
publicViewinflate(XmlPullParserparser,@NullableViewGrouproot,booleanattachToRoot){
synchronized(mConstructorArgs){
finalContextinflaterContext=mContext;
//快看,View的构造函数中的attrs就是这个!!!
finalAttributeSetattrs=Xml.asAttributeSet(parser);
//这个数组很重要,从名字就可以看出来,这是构造函数要用到的参数
mConstructorArgs[0]=inflaterContext;
Viewresult=root;
try{
//找到根节点,找到第一个START_TAG就跳出while循环,
//比如<TextView>是START_TAG,而</TextView>是END_TAG
inttype;
while((type=parser.next())!=XmlPullParser.START_TAG&&
type!=XmlPullParser.END_DOCUMENT){
//Empty
}
if(type!=XmlPullParser.START_TAG){
thrownewInflateException(parser.getPositionDescription()
+":Nostarttagfound!");
}
//获取根节点的名称
finalStringname=parser.getName();
//判断是否用了merge标签
if(TAG_MERGE.equals(name)){
if(root==null||!attachToRoot){
thrownewInflateException("<merge/>canbeusedonlywithavalid"
+"ViewGrouprootandattachToRoot=true");
}
//解析
rInflate(parser,root,inflaterContext,attrs,false);
}else{
//这里需要调用到PhoneLayoutInflater中的方法,获取到根节点对应的View
finalViewtemp=createViewFromTag(root,name,inflaterContext,attrs);
ViewGroup.LayoutParamsparams=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(Exceptione){
}finally{
//Don'tretainstaticreferenceoncontext.
mConstructorArgs[0]=lastContext;
mConstructorArgs[1]=null;
}
returnresult;
}
}
这个就稍微有点长了,我稍微去除了一些跟逻辑无关的代码,并且添加了注释,如果有耐心看的话应该是能看懂了。这里主要讲两个部分,首先是rInflateChildren这个方法,其实就是一层一层的把所有节点取出来,然后通过createViewFromTag方法将其转换成View对象。所以重点是在如何转换成View对象的。
3.createViewFromTag
我们一层层跟进代码,最后会到这里
ViewcreateViewFromTag(Viewparent,Stringname,Contextcontext,AttributeSetattrs,
booleanignoreThemeAttr){
......
......
try{
......
if(view==null){
finalObjectlastContext=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;
}
}
returnview;
}catch(InflateExceptione){
throwe;
......
}
}
为了方便理解,将无关的代码去掉了,我们看到其实就是调用的createView方法来从xml节点转换成View的。如果name中不包含'.'就调用onCreateView方法,否则直接调用createView方法。
在上面的PhoneLayoutInflater中就复写了onCreateView方法,而且不管是否重写,该方法最后都会调用createView。唯一的区别应该是系统的View的完整类名由onCreateView来提供,而如果是自定义控件在布局文件中本来就是用的完整类名。
4.createView方法
publicfinalViewcreateView(Stringname,Stringprefix,AttributeSetattrs)
throwsClassNotFoundException,InflateException{
//1.通过传入的类名,获取该类的构造器
Constructor<?extendsView>constructor=sConstructorMap.get(name);
Class<?extendsView>clazz=null;
try{
if(constructor==null){
clazz=mContext.getClassLoader().loadClass(
prefix!=null?(prefix+name):name).asSubclass(View.class);
if(mFilter!=null&&clazz!=null){
booleanallowed=mFilter.onLoadClass(clazz);
if(!allowed){
failNotAllowed(name,prefix,attrs);
}
}
constructor=clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
sConstructorMap.put(name,constructor);
}else{
if(mFilter!=null){
BooleanallowedState=mFilterMap.get(name);
if(allowedState==null){
clazz=mContext.getClassLoader().loadClass(
prefix!=null?(prefix+name):name).asSubclass(View.class);
booleanallowed=clazz!=null&&mFilter.onLoadClass(clazz);
mFilterMap.put(name,allowed);
if(!allowed){
failNotAllowed(name,prefix,attrs);
}
}elseif(allowedState.equals(Boolean.FALSE)){
failNotAllowed(name,prefix,attrs);
}
}
}
//2.通过获得的构造器,创建View实例
Object[]args=mConstructorArgs;
args[1]=attrs;
finalViewview=constructor.newInstance(args);
if(viewinstanceofViewStub){
finalViewStubviewStub=(ViewStub)view;
viewStub.setLayoutInflater(cloneInContext((Context)args[0]));
}
returnview;
}catch(NoSuchMethodExceptione){
......
}
}
这段代码主要做了两件事情
第一,根据ClassName将类加载到内存,然后获取指定的构造器constructor。构造器是通过传入参数类型和数量来指定,这里传入的是mConstructorSignature
staticfinalClass<?>[]mConstructorSignature=newClass[]{
Context.class,AttributeSet.class};
即传入参数是Context和AttributeSet,是不是猛然醒悟了!!!这就是为什么我们在自定义View的时候,必须要重写View(Contextcontext,AttributeSetattrs)则个构造方法,才能在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文件设定的属性值。需要在构造函数中将这些属性值取出来,赋给该实例的属性。
感谢阅读,希望能帮助到大家,谢谢大家对本站的支持!