今天碰到了在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,下面便是该流程用到的关键函数的主要代码:

  1     final Object[] mConstructorArgs = new Object[2];
  2
  3     static final Class<?>[] mConstructorSignature = new Class[] {
  4             Context.class, AttributeSet.class};
  5
  6     //...
  7
  8     /**
  9      * Creates a view from a tag name using the supplied attribute set.
 10      * <p>
 11      * <strong>Note:</strong> Default visibility so the BridgeInflater can
 12      * override it.
 13      *
 14      * @param parent the parent view, used to inflate layout params
 15      * @param name the name of the XML tag used to define the view
 16      * @param context the inflation context for the view, typically the
 17      *                {@code parent} or base layout inflater context
 18      * @param attrs the attribute set for the XML tag used to define the view
 19      * @param ignoreThemeAttr {@code true} to ignore the {@code android:theme}
 20      *                        attribute (if set) for the view being inflated,
 21      *                        {@code false} otherwise
 22      */
 23     View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
 24             boolean ignoreThemeAttr) {
 25         //**关键1**//
 26         if (name.equals("view")) {
 27             name = attrs.getAttributeValue(null, "class");
 28         }
 29
 30         // Apply a theme wrapper, if allowed and one is specified.
 31         if (!ignoreThemeAttr) {
 32             final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
 33             final int themeResId = ta.getResourceId(0, 0);
 34             if (themeResId != 0) {
 35                 context = new ContextThemeWrapper(context, themeResId);
 36             }
 37             ta.recycle();
 38         }
 39
 40         if (name.equals(TAG_1995)) {
 41             // Let's party like it's 1995!
 42             return new BlinkLayout(context, attrs);
 43         }
 44
 45         try {
 46             View view;
 47             if (mFactory2 != null) {
 48                 view = mFactory2.onCreateView(parent, name, context, attrs);
 49             } else if (mFactory != null) {
 50                 view = mFactory.onCreateView(name, context, attrs);
 51             } else {
 52                 view = null;
 53             }
 54
 55             if (view == null && mPrivateFactory != null) {
 56                 view = mPrivateFactory.onCreateView(parent, name, context, attrs);
 57             }
 58
 59             if (view == null) {
 60                 final Object lastContext = mConstructorArgs[0];
 61                 mConstructorArgs[0] = context;
 62                 try {
 63                     if (-1 == name.indexOf('.')) {
 64                         //**关键2**//
 65                         view = onCreateView(parent, name, attrs);
 66                     } else {
 67                         //**关键3**//
 68                         view = createView(name, null, attrs);
 69                     }
 70                 } finally {
 71                     mConstructorArgs[0] = lastContext;
 72                 }
 73             }
 74
 75             return view;
 76         }
 77         //后面都是catch,省略
 78     }
 79
 80     protected View onCreateView(View parent, String name, AttributeSet attrs)
 81             throws ClassNotFoundException {
 82         return onCreateView(name, attrs);
 83     }
 84
 85     protected View onCreateView(String name, AttributeSet attrs)
 86             throws ClassNotFoundException {
 87         return createView(name, "android.view.", attrs);
 88     }
 89
 90     /**
 91      * Low-level function for instantiating a view by name. This attempts to
 92      * instantiate a view class of the given <var>name</var> found in this
 93      * LayoutInflater's ClassLoader.
 94      *
 95      * @param name The full name of the class to be instantiated.
 96      * @param attrs The XML attributes supplied for this instance.
 97      *
 98      * @return View The newly instantiated view, or null.
 99      */
100     public final View createView(String name, String prefix, AttributeSet attrs)
101             throws ClassNotFoundException, InflateException {
102         Constructor<? extends View> constructor = sConstructorMap.get(name);
103         Class<? extends View> clazz = null;
104
105         try {
106             Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);
107
108             if (constructor == null) {
109                 //**关键4**//
110                 // Class not found in the cache, see if it's real, and try to add it
111                 clazz = mContext.getClassLoader().loadClass(
112                         prefix != null ? (prefix + name) : name).asSubclass(View.class);
113
114                 if (mFilter != null && clazz != null) {
115                     boolean allowed = mFilter.onLoadClass(clazz);
116                     if (!allowed) {
117                         failNotAllowed(name, prefix, attrs);
118                     }
119                 }
120                 constructor = clazz.getConstructor(mConstructorSignature);
121                 constructor.setAccessible(true);
122                 sConstructorMap.put(name, constructor);
123             } else {
124                 // If we have a filter, apply it to cached constructor
125                 if (mFilter != null) {
126                     // Have we seen this name before?
127                     Boolean allowedState = mFilterMap.get(name);
128                     if (allowedState == null) {
129                         // New class -- remember whether it is allowed
130                         clazz = mContext.getClassLoader().loadClass(
131                                 prefix != null ? (prefix + name) : name).asSubclass(View.class);
132
133                         boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
134                         mFilterMap.put(name, allowed);
135                         if (!allowed) {
136                             failNotAllowed(name, prefix, attrs);
137                         }
138                     } else if (allowedState.equals(Boolean.FALSE)) {
139                         failNotAllowed(name, prefix, attrs);
140                     }
141                 }
142             }
143
144             Object[] args = mConstructorArgs;
145             args[1] = attrs;
146             //**关键5**//
147             final View view = constructor.newInstance(args);
148             if (view instanceof ViewStub) {
149                 // Use the same context when inflating ViewStub later.
150                 final ViewStub viewStub = (ViewStub) view;
151                 viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
152             }
153             return view;
154
155         }
156         //后面都是catch以及finally处理,省略
157     }

  PhoneLayoutInflater中用到的主要代码:

 1 public class PhoneLayoutInflater extends LayoutInflater {
 2     private static final String[] sClassPrefixList = {
 3         "android.widget.",
 4         "android.webkit.",
 5         "android.app."
 6     };
 7     //......
 8
 9     /** Override onCreateView to instantiate names that correspond to the
10         widgets known to the Widget factory. If we don't find a match,
11         call through to our super class.
12     */
13     @Override protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
14         //**关键6**//
15         for (String prefix : sClassPrefixList) {
16             try {
17                 View view = createView(name, prefix, attrs);
18                 if (view != null) {
19                     return view;
20                 }
21             } catch (ClassNotFoundException e) {
22                 // In this case we want to let the base class take a crack
23                 // at it.
24             }
25         }
26
27         return super.onCreateView(name, attrs);
28     }
29
30     //.........
31 }

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的定义:
static final Class<?>[] mConstructorSignature = new Class[] {Context.class, AttributeSet.class};

在Oracle的文档上找到getConstructor函数的说明:

Returns a Constructor object that reflects the specified public constructor of the class represented by this Class object. The parameterTypes parameter is an array of Class objects that identify the constructor's formal parameter types, in declared order. If this Class object represents an inner class declared in a non-static context, the formal parameter types include the explicit enclosing instance as the first parameter.

The constructor to reflect is the public constructor of the class represented by this Class object whose formal parameter types match those specified by parameterTypes.

于是,这里其实解答了我们两个问题:(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的说明中也有一句:If the constructor's declaring class is an inner class in a non-static context, the first argument to                   the constructor needs to be the enclosing instance; 我们这里也没有传入外部类实例,也就是说对于静态内部类也会报错。这里同样验证了自定义了的内部类view必须声明为静态类这个问题。

  至此,我们已经在假设使用“view”,再次强调是小写‘v’,作为元素名称的情况,推出了要在XML中应用自定义的内部类view必须满足的三点条件,即在class属性中使用“$”连接外部类和内部类,必须有一个接受Context和AttributeSet参数的构造函数,必须声明为静态的这三个要求。

  而如果不使用“view”标签的形式来使用自定义内部类view,那么在写XML的时候我们发现,只能使用比如<com.willhua.MyClass.MyView />的形式,而不能使用<com.willhua.MyClass$MyView />的形式,这样AndroidStudio会报“Tag start is not close”的错。显然,如果我们使用<com.willhua.MyClass.MyView />的形式,那么在“关键4”处将会调用loadClass("com.willhua.MyClass.MyView"),这样在前面也已经分析过,是不符合Java中关于内部类的完整命名规则的,将会报错。有些人估计会粗心写成大写的V的形式,即<View class="com.willhua.MyClass$MyView" ... />的形式,这样将会在运行时报“wrong type”错,因为这样本质上定义的是一个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. 使用<view class="完整类名" />的形式。这个是最通用的,所有的类都可以这么干,但是前提是“完整类名”写对,比如前面提到的内部类使用"$"符号连接。

  为了搞清楚加载内部类view的问题,结果对整个加载流程都有了一定了解,感觉能说多若干个why了,还是收获挺大的。

参考:

http://blog.csdn.net/gorgle/article/details/51428515

http://blog.csdn.net/abcdef314159/article/details/50921160

http://blog.csdn.net/abcdef314159/article/details/50925124

周末愉快~~~

转载于:https://www.cnblogs.com/willhua/p/6130155.html

Android XML中引用自定义内部类view的四个why相关推荐

  1. Android中自定义View的研究 -- 在XML中引用自定义View

    如果在一直使用SetContentView(new HellwView(this)觉得总是少了一点东西,少了什么了,失去了Android中使用XML定义组件的便携性,这种感觉让人很不爽,呵呵,在这节里 ...

  2. android 在xml文件中引用自定义View

    在xml中引用自定义view 方法一: [java] view plaincopy <com.test.copytext.CopyText android:layout_width=" ...

  3. android 自定义view xml ,Android实现在xml文件中引用自定义View的方法分析

    本文实例讲述了Android实现在xml文件中引用自定义View的方法.分享给大家供大家参考,具体如下: 在xml中引用自定义view 方法一: android:layout_width=" ...

  4. android自定义控件是一个 内部类 如何在xml中引用,android 自定义view属性

    android 自定义view属性 一个完美的自定义控件也可以添加xml来配置属性和风格.要实现这一点,可按照下列步骤来做: 1) 添加自定义属性到xml文件中 2) 在xml的中,指定属性的值 3) ...

  5. Android中使用自定义的view实现圆形图片的效果

    今天给大家讲的是怎么在xml文件找中通过引用自定义的view实现ImageView的圆形图片效果.首先在你的项目中新建一个类,我给它命名为:CircleImageView:然后在res目录下的valu ...

  6. android studio 自定义字体,Android Studio中的自定义字体

    如何在android studio中创建自定义字体? 我试图使用自定义字体,我读过,我想将字体放在资产/字体. 我已经搜索了很长时间,但很难找到帮助. 我哪里错了?我真的不知道该怎么做. 我写下了所有 ...

  7. 墨迹天气php,Android_仿墨迹天气在Android App中实现自定义zip皮肤更换,在这里谈一下墨迹天气的换肤 - phpStudy...

    仿墨迹天气在Android App中实现自定义zip皮肤更换 在这里谈一下墨迹天气的换肤实现方式,不过首先声明我只是通过反编译以及参考了一些网上其他资料的方式推测出的换肤原理, 在这里只供参考. 若大 ...

  8. Mybatis xml中引用枚举值

    xml中引用枚举值 ${@com.demo.Sex@MAN.value} ${@枚举类全类名@枚举实例.属性名} 例: package com.demo.Sex;import lombok.AllAr ...

  9. Android studio 中引用jar的其实是Maven?(一)

    由于Studio比eclipse多了一步对工程构建的步骤,即为build.gradle这个文件运行,因此其引入第三方开发jar包与lib工程对比Eclipse已完成不同,引入第三方jar与lib工程显 ...

最新文章

  1. 在普通PC上安装XENSERVER 6.2
  2. android 重绘如何能不闪一下屏幕_浏览器渲染机制——重绘重排
  3. Swift class和struct的解归档
  4. mysql yum安装与配置文件_MySQL 8.0 yum安装和配置
  5. Webrtc之2台电脑视频聊天
  6. 组合数学1.1——棋盘的完美覆盖
  7. Lambda演算与科里化(Currying)
  8. 怎么将表中的空格都转变为0???
  9. 惯性矩如何计算机械转动惯量,[转载]ug中的惯性矩与转动惯量
  10. 机房收费系统可行性研究报告
  11. 使用导入 Excel 的方式批量修改文件夹名称
  12. 怎样压缩图片大小到20k?教你一键压缩图片大小
  13. python2048游戏代码_python 实现 2048 游戏 (二)
  14. xampp运行不成功或者安装过程中提示找不到文件“-n”,没有安装vcredist_x86的解决方法
  15. hyperlink的学习
  16. 供应链金融融资的业务模式
  17. Hazelcast IMDG学习 Map java demo
  18. Navicat怎样导入Excel表格和txt文本的数据
  19. 被垃圾分类逼疯?这个深度学习技术帮你做到垃圾自动分类
  20. 消息被服务器拒绝访问,服务器上登录的Firebase访问被拒绝消息在哪里?

热门文章

  1. linux var run目录,PXE系列文章(16) - Linux /run 和 /var/run 目录介绍
  2. Android Handler的内存泄露场景分析
  3. 大数据分析机器学习(一)之线性模型-年龄和心率关系
  4. js实现kmp算法_字符串匹配算法KMP算法
  5. 我的内核学习笔记13:x86平台linux系统重启流程跟踪
  6. coreboot学习8:ramstage阶段之资源分配流程
  7. jar导出与制作成exe在没jdk电脑下运行(图文教程+工具)
  8. 【flink】flink作业超额启动多个taskManager k8s
  9. 【kafka】kafka topic某些分区 副本落后leader太多
  10. Maven : Maven和jenkins报错 ClassNotFoundException : org.slf4j.Logger