详解Android XML中引用自定义内部类view的四个why
今天碰到了在XML中应用以内部类形式定义的自定义view,结果遇到了一些坑。虽然通过看了一些前辈写的文章解决了这个问题,但是我看到的几篇都没有完整说清楚why,于是决定做这个总结。
使用自定义内部类view的规则
本文主要是总结why,所以先把XML布局文件中引用内部类的自定义view的做法摆出来,有四点:
1.自定义的类必须是静态类;
2.使用view作为XML文件中的tag,注意,v是小写字母,小写字母v,小写字母v;
3.添加class属性,注意,没有带android:命名空间的,表明该自定义view的完整路径,且外部类与内部类之间用美元“$”连接,而不是“.”,注意,要美元“$”,不要“.”;
4.自定义的view至少应该含有带有Context,AttributeSet这两个参数的构造函数
布局加载流程主要代码
首先,XML布局文件的加载都是使用LayoutInflater来实现的,通过这篇文章的分析,我们知道实际使用的LayoutInflater类是其子类PhoneLayoutInflater,然后通过这篇文章的分析,我们知道view的真正实例化的关键入口函数是createViewFromTag这个函数,然后通过createView来真正实例化view,下面便是该流程用到的关键函数的主要代码:
finalObject[]mConstructorArgs=newObject[2];
staticfinalClass<?>[]mConstructorSignature=newClass[]{
Context.class,AttributeSet.class};
//...
/**
*Createsaviewfromatagnameusingthesuppliedattributeset.
*<p>
*<strong>Note:</strong>DefaultvisibilitysotheBridgeInflatercan
*overrideit.
*
*@paramparenttheparentview,usedtoinflatelayoutparams
*@paramnamethenameoftheXMLtagusedtodefinetheview
*@paramcontexttheinflationcontextfortheview,typicallythe
*{@codeparent}orbaselayoutinflatercontext
*@paramattrstheattributesetfortheXMLtagusedtodefinetheview
*@paramignoreThemeAttr{@codetrue}toignorethe{@codeandroid:theme}
*attribute(ifset)fortheviewbeinginflated,
*{@codefalse}otherwise
*/
ViewcreateViewFromTag(Viewparent,Stringname,Contextcontext,AttributeSetattrs,
booleanignoreThemeAttr){
//**关键1**//
if(name.equals("view")){
name=attrs.getAttributeValue(null,"class");
}
//Applyathemewrapper,ifallowedandoneisspecified.
if(!ignoreThemeAttr){
finalTypedArrayta=context.obtainStyledAttributes(attrs,ATTRS_THEME);
finalintthemeResId=ta.getResourceId(0,0);
if(themeResId!=0){
context=newContextThemeWrapper(context,themeResId);
}
ta.recycle();
}
if(name.equals(TAG_1995)){
//Let'spartylikeit's1995!
returnnewBlinkLayout(context,attrs);
}
try{
Viewview;
if(mFactory2!=null){
view=mFactory2.onCreateView(parent,name,context,attrs);
}elseif(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){
finalObjectlastContext=mConstructorArgs[0];
mConstructorArgs[0]=context;
try{
if(-1==name.indexOf('.')){
//**关键2**//
view=onCreateView(parent,name,attrs);
}else{
//**关键3**//
view=createView(name,null,attrs);
}
}finally{
mConstructorArgs[0]=lastContext;
}
}
returnview;
}
//后面都是catch,省略
}
protectedViewonCreateView(Viewparent,Stringname,AttributeSetattrs)
throwsClassNotFoundException{
returnonCreateView(name,attrs);
}
protectedViewonCreateView(Stringname,AttributeSetattrs)
throwsClassNotFoundException{
returncreateView(name,"android.view.",attrs);
}
/**
*Low-levelfunctionforinstantiatingaviewbyname.Thisattemptsto
*instantiateaviewclassofthegiven<var>name</var>foundinthis
*LayoutInflater'sClassLoader.
*
*@paramnameThefullnameoftheclasstobeinstantiated.
*@paramattrsTheXMLattributessuppliedforthisinstance.
*
*@returnViewThenewlyinstantiatedview,ornull.
*/
publicfinalViewcreateView(Stringname,Stringprefix,AttributeSetattrs)
throwsClassNotFoundException,InflateException{
Constructor<?extendsView>constructor=sConstructorMap.get(name);
Class<?extendsView>clazz=null;
try{
Trace.traceBegin(Trace.TRACE_TAG_VIEW,name);
if(constructor==null){
//**关键4**//
//Classnotfoundinthecache,seeifit'sreal,andtrytoaddit
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{
//Ifwehaveafilter,applyittocachedconstructor
if(mFilter!=null){
//Haveweseenthisnamebefore?
BooleanallowedState=mFilterMap.get(name);
if(allowedState==null){
//Newclass--rememberwhetheritisallowed
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);
}
}
}
Object[]args=mConstructorArgs;
args[1]=attrs;
//**关键5**//
finalViewview=constructor.newInstance(args);
if(viewinstanceofViewStub){
//UsethesamecontextwheninflatingViewStublater.
finalViewStubviewStub=(ViewStub)view;
viewStub.setLayoutInflater(cloneInContext((Context)args[0]));
}
returnview;
}
//后面都是catch以及finally处理,省略
}
PhoneLayoutInflater中用到的主要代码:
publicclassPhoneLayoutInflaterextendsLayoutInflater{
privatestaticfinalString[]sClassPrefixList={
"android.widget.",
"android.webkit.",
"android.app."
};
//......
/**OverrideonCreateViewtoinstantiatenamesthatcorrespondtothe
widgetsknowntotheWidgetfactory.Ifwedon'tfindamatch,
callthroughtooursuperclass.
*/
@OverrideprotectedViewonCreateView(Stringname,AttributeSetattrs)throwsClassNotFoundException{
//**关键6**//
for(Stringprefix:sClassPrefixList){
try{
Viewview=createView(name,prefix,attrs);
if(view!=null){
returnview;
}
}catch(ClassNotFoundExceptione){
//Inthiscasewewanttoletthebaseclasstakeacrack
//atit.
}
}
returnsuper.onCreateView(name,attrs);
}
//.........
}
WHY
对于任何一个XML中的元素,首先都是从“1”处开始(“1”即表示代码中标注“关键1”的位置,后同),下面便跟着代码的流程来一遍:
1.“1”处进行名字是否为“view”的判断,那么这里会有两种情况,我们先假设使用“view”
2.由于在“1”满足条件,所以name被赋值为class属性的值,比如“com.willhua.view.MyView”或者“com.willhua.view.MyClass$MyView”。其实这里也是要使用“view”来定义,而不是其他名字来定义的原因。
3.根据name的值,name中含有‘.'符号,于是代码走到“3”处,调用createView,且prefix参数为空
4.来到“4”处,prefix为空,于是loadClass函数的参数即为name,即在a.2中说的class属性的值。我们知道,传给loadClass的参数是想要加载的类的类名,而在Java中,内部类的类名表示都是在外部类类名的后面用符号“$”连接内部类类名而来,于是文章开头提到的第3点的答案也就是在这里了。补充一下,为什么是来到“4”,而不是对应的else块中呢?类第一次被加载的时候,构造器肯定是还不存在的,也就是if条件肯定是成立的。然后等到后面再次实例化的时候,就来到了else块中,而在else快中只是根据mFilter做一些是否可以加载该view的判断而已,并没有从本质上影响view的加载流程。
5.在“4”处还有一个很重要的地方,那就是constructor=class.getConstructor(mConstructorSignature)这句。首先,在代码开头已经给出了mConstructorSignature的定义:
staticfinalClass<?>[]mConstructorSignature=newClass[]{Context.class,AttributeSet.class};
在Oracle的文档上找到getConstructor函数的说明:
ReturnsaConstructorobjectthatreflectsthespecifiedpublicconstructoroftheclassrepresentedbythisClassobject.TheparameterTypesparameterisanarrayofClassobjectsthatidentifytheconstructor'sformalparametertypes,indeclaredorder.IfthisClassobjectrepresentsaninnerclassdeclaredinanon-staticcontext,theformalparametertypesincludetheexplicitenclosinginstanceasthefirstparameter.
TheconstructortoreflectisthepublicconstructoroftheclassrepresentedbythisClassobjectwhoseformalparametertypesmatchthosespecifiedbyparameterTypes.
于是,这里其实解答了我们两个问题:(1)getConstructor返回的构造函数是其参数与传getConstructor的参数完全匹配的那一个,如果没有就抛出NoSuchMethodException异常。于是我们就知道,必须要有一个与mConstructorSignature完全匹配,就需要Context和AttributeSet两个参数的构造函数;(2)要是该类表示的是非静态的内部类,那么应该把一个外部类实例作为第一个参数传入。而我们的代码中传入的mConstructorSignature是不含有外部类实例的,因此我们必须把我们自定义的内部类view声明为静态的才不会报错。有些同学的解释只是说到了非静态内部类需要由一个外部类实例来引用,但我想那万一系统在加载的时候它会自动构造一个外部类实例呢?于是这里给了否定的答案。
6.拿到了类信息clazz之后,代码来到了“5”,在这里注意下mConstructorArgs,在贴出的代码最前面有给出它的定义,为Object[2],并且,通过源码中发现,mConstructorArgs[0]被赋值为创建LayoutInflater时传入的context。于是我们就知道 在“5”这里,传给newInstance的参数为Context和AttributeSet。然后在Oracl的文档上关于newInstance的说明中也有一句:Iftheconstructor'sdeclaringclassisaninnerclassinanon-staticcontext,thefirstargumentto theconstructorneedstobetheenclosinginstance;我们这里也没有传入外部类实例,也就是说对于静态内部类也会报错。这里同样验证了自定义了的内部类view必须声明为静态类这个问题。
至此,我们已经在假设使用“view”,再次强调是小写‘v',作为元素名称的情况,推出了要在XML中应用自定义的内部类view必须满足的三点条件,即在class属性中使用“$”连接外部类和内部类,必须有一个接受Context和AttributeSet参数的构造函数,必须声明为静态的这三个要求。
而如果不使用“view”标签的形式来使用自定义内部类view,那么在写XML的时候我们发现,只能使用比如<com.willhua.MyClass.MyView/>的形式,而不能使用<com.willhua.MyClass$MyView/>的形式,这样AndroidStudio会报“Tagstartisnotclose”的错。显然,如果我们使用<com.willhua.MyClass.MyView/>的形式,那么在“关键4”处将会调用loadClass("com.willhua.MyClass.MyView"),这样在前面也已经分析过,是不符合Java中关于内部类的完整命名规则的,将会报错。有些人估计会粗心写成大写的V的形式,即<Viewclass="com.willhua.MyClass$MyView".../>的形式,这样将会在运行时报“wrongtype”错,因为这样本质上定义的是一个android.view.View,而你在代码中却以为是定义的com.willhua.MyClass$MyView。
在标识的“2”处,该出的调用流程为onCreateView(parent,name,attrs)——>onCreateView(name,attrs)——>createView(name,"android.view.",attrs),在前面提到过,我们真正使用的的LayoutInflater是PhoneLayoutInflater,而在PhoneLayoutInflater对这个onCreateView(name,attrs)函数是进行了重写的,在PhoneLayoutInflater的onCreateView函数中,即“6”处,该函数通过在name前面尝试分别使用三个前缀:"android.widget.","android.webkit.","android.app."来调用createView,若都没有找到则调用父类的onCreateView来尝试添加"android.view."前缀来加载该view。所以,这也是为什么我们可以比如直接使用<Button/>,<View/>的原因,因为这些常用都是包含在这四个包名里的。
总结
至此,开篇提到的四个要求我们都已经找到其原因了。其实,整个流程走下来,我们发现,要使定义的元素被正确的加载到相关的类,有三种途径:
1.只使用简单类名,即<ViewName/>的形式。对于“android.widget”,"android.webkit","android.app"以及"android.view"这四个包内的类,可以直接使用这样的形式,因为在代码中会自动给加上相应的包名来构成完整的路径,而对于的其他的类,则不行,因为ViewName加上这四个包名构成的完整类名都无法找到该类。
2.使用<"完整类名"/>的形式,比如<com.willhua.MyView/>或者<android.widget.Button/>的形式来使用,对于任何的非内部类view,这样都是可以的。但是对于内部类不行,因为这样的类名不符合Java中关于内部类完整类名的定义规则。如果<com.willhua.MyClass$MyView/>的形式能够通过编译话,肯定是能正确Inflate该MyView的,可惜在编写的时候就会提示错。
3.使用<viewclass="完整类名"/>的形式。这个是最通用的,所有的类都可以这么干,但是前提是“完整类名”写对,比如前面提到的内部类使用"$"符号连接。
为了搞清楚加载内部类view的问题,结果对整个加载流程都有了一定了解,感觉能说多若干个why了,还是收获挺大的。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。