问题1:为什么CustomView在xml中使用时,不可以像系统View一样类名引用,而必须是以完整包名引用,否则会出现ClassNotFoundException
。
AndroidRuntime: Caused by: android.view.InflateException: Binary XML file line #9 in com.example.myapplication:layout/activity_main: Error inflating class CustomView
AndroidRuntime: Caused by: java.lang.ClassNotFoundException: android.view.CustomView
at java.lang.Class.classForName(Native Method)
at java.lang.Class.forName(Class.java:454)
at android.view.LayoutInflater.createView(LayoutInflater.java:825)
at android.view.LayoutInflater.createView(LayoutInflater.java:786)
...
问题2: 为什么CustomView在xml中使用时,不可缺少constructor(Context, AttributeSet)
的构造函数,否则会出现NoSuchMethodException
。
AndroidRuntime: Caused by: android.view.InflateException: Binary XML file line #9 in com.example.myapplication:layout/activity_main: Error inflating class com.example.myapplication.CustomView
AndroidRuntime: Caused by: java.lang.NoSuchMethodException: com.example.myapplication.CustomView.<init> [class android.content.Context, interface android.util.AttributeSet]
at java.lang.Class.getConstructor0(Class.java:2332)
at java.lang.Class.getConstructor(Class.java:1728)
at android.view.LayoutInflater.createView(LayoutInflater.java:834)
...
一、LayoutInflater解析xml的过程
一切还要从LayoutInflater
解析布局说起,当Activity中通过setContentView
传入xml时,最终会进入AppCompatDelegateImpl
中,就是在这里,LayoutInflater
正式登场了。
@Override
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mAppCompatWindowCallback.getWrapped().onContentChanged();
}
LayoutInflater
登场后,做了两场表演,一是构造自己的实例,二是通过inflate()
创建View树。
1)LayoutInflater.from(mContext)
构建实例。
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;
}
而不是通过LayoutInflater
自身的构造函数创建实例,
protected LayoutInflater(LayoutInflater original, Context newContext) {
mContext = newContext;
mFactory = original.mFactory;
mFactory2 = original.mFactory2;
mPrivateFactory = original.mPrivateFactory;
setFilter(original.mFilter);
initPrecompiledViews();
}
并且没有在外部设置过mFactory
,mFactory2
和mPrivateFactory
。
public void setFactory2(Factory2 factory) {
if (mFactorySet) {
throw new IllegalStateException("A factory has already been set on this LayoutInflater");
}
if (factory == null) {
throw new NullPointerException("Given factory can not be null");
}
mFactorySet = true;
if (mFactory == null) {
mFactory = mFactory2 = factory;
} else {
mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
}
}
这样可以得出一个结论,LayouterInflater实例中的mFactory
,mFactory2
和mPrivateFactory
三个变量至始至终为null,这个结论先放这,后面会用到。
2)View树的解析工作通过inflater.inflate()
发起,分为两个大的步骤,一是通过createViewFromTag()
方法创建View,二是通过rInflateChildren()
方法创建ViewGroup的子View。
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
...
View result = root;
try {
...
// 第一步,根据节点标签创建View对象
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
...
// 第二步,创建View对象的子View
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 (Exception e) {
...
}
return result;
}
}
- 第一步:通过
createViewFromTag()
创建View对象。
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,boolean ignoreThemeAttr) {
...
try {
View view = tryCreateView(parent, name, context, attrs);
if (view == null) {
...if (-1 == name.indexOf('.')) {
view = onCreateView(context, parent, name, attrs);
} else {
view = createView(context, name, null, attrs);
}
...
}
return view;
} catch (Exception e) {
...
}
}
这个方法里会先执行tryCreateView()
方法,这里就会依赖mFactory
,mFactory2
及mPrivateFactory
,根据前文的结论,这三个变量都为null,所以这个方法最终返回的View是null。
public final View tryCreateView(@Nullable View parent, @NonNull String name,
@NonNull Context context,
@NonNull AttributeSet attrs) {
if (name.equals(TAG_1995)) {
// Let's party like it's 1995!
return new BlinkLayout(context, attrs);
}
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);
}
return view;
}
因此creatViewFromTag()
方法里也没有真正创建View,而是进入到onCreateView()
或者createView()
中进行创建,不论哪个,最终都会执行到下面的方法中。
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<? extends View> constructor = sConstructorMap.get(name);
if (constructor != null && !verifyClassLoader(constructor)) {
constructor = null;
sConstructorMap.remove(name);
}
Class<? extends View> clazz = null;
try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);
if (constructor == null) {
// 通过标签名反射获取View Class对象
clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
mContext.getClassLoader()).asSubclass(View.class);
...
// 获取View的构造器
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
sConstructorMap.put(name, constructor);
}
...
try {
// 通过构造器反射创建View对象
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) {
...
}
}
这个方法就是真正创建View的实现,根据传入的标签名,反射创建出View对象【tip: 创建过程采用了缓存机制实现,缓存对象是要创建View的构造器,只有当View没有被创建过时,才会执行反射创建,并将View的构造器存到缓存表中,当下一次再需要创建相同的View对象时,就直接从缓存表中取出对应的构造器,创建对象,避免重复反射】。
- 第二步:通过
rInflateChildren()
方法创建ViewGroup的子View。相关源码如下,
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
final int depth = parser.getDepth();
int type;
boolean pendingRequestFocus = false;
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
if (type != XmlPullParser.START_TAG) {
continue;
}
final String name = parser.getName();
if (TAG_REQUEST_FOCUS.equals(name)) {
pendingRequestFocus = true;
consumeChildElements(parser);
} else if (TAG_TAG.equals(name)) {
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
throw new InflateException("<merge /> must be the root element");
} else {
//和创建父View一样的过程
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
//递归调用
rInflateChildren(parser, view, attrs, true);
viewGroup.addView(view, params);
}
}
if (pendingRequestFocus) {
parent.restoreDefaultFocus();
}
if (finishInflate) {
parent.onFinishInflate();
}
}
会对创建出来的View进行遍历,如果存在子View,那么就通过递归调用,一层一层地将所有子View通过createViewFromTag()
创建出来,创建的子View会被添加到对应的ViewGroup中,就是这样把一个完整的xml布局对应的View树给创建了出来。
二、为什么自定义View在xml中使用时,必须引用完整包名,而系统View只需要引入类名?
为了说明问题,这里自定义了一个CustomView
,并设置了三个默认构造函数,
public class CustomView extends View {
public CustomView(Context context) {
super(context);
init(context);
}
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public CustomView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
private void init(Context context) {
//...
}
}
将CustomView和系统TextView放到同一个布局中,其中TextView只以类名引入,而CustomView以完整包名引入。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<TextView
android:layout_width="match_parent"
android:layout_height="44dp"
android:background="#000000" />
<com.example.myapplication.CustomView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ff0000" />
</LinearLayout>
这时运行发现页面正常显示无异常。
[问题来了] 现在将布局中的CustomView也改成类名引入,再运行,就遇到了文章开头的ClassNotFoundException
。
<CustomView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ff0000" />
再回头看下createViewFromTag()
方法源码,
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr) {
...
try {
View view = tryCreateView(parent, name, context, attrs);
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {//name字串中没有.
view = onCreateView(context, parent, name, attrs);
} else {//name字串中含有.
view = createView(context, name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
return view;
} catch (Exception e) {
...
}
}
也就是看view标签name中是否含有“.”字符,如果有就执行createView()
方法,没有则执行onCreateView()
方法。
i) name中不含“.”,onCreateView()
执行时,调用了createView(name, attrs)
,该方法里又调用了createView(context, name, prefix, attrs)
,传入prefix=“android.view”
。
protected View onCreateView(String name, AttributeSet attrs)
throws ClassNotFoundException {
return createView(name, "android.view.", attrs);
}
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
Context context = (Context) mConstructorArgs[0];
if (context == null) {
context = mContext;
}
return createView(context, name, prefix, attrs);
}
ii) name中含“.”,直接调用了createView(context, name, prefix, attrs)
方法,传入的prefix=null
。
[答案在此] 因此不管name中是否含有“.”,最终都会执行createView(context, name, prefix, attrs)
,只是传入的prefix不同而已。方法内部与prefix产生关联的代码是这句,
clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
mContext.getClassLoader()).asSubclass(View.class);
这句话是根据View标签name反射创建View对象,反射创建时,需要完整的类名,否则会找不到类。
由于系统View都是在android.view
包下的,createView()
时name中不含“.”,因而会添加android.view
的prefix,得到View的完整名称。因此在xml布局放置系统View标签时,不写完整包名也能正常解析。然而对于自定义View,如果xml不写完整的包名,自然也是会被加上android.view
的prefix,而自定义View根本不在android.view
包下,反射时自然就找不到类了。
三、为什么自定义View在xml中使用时,不可缺少constructor(Context, AttributeSet)?
[问题来了] 在xml布局中使用自定义View时,有时会出现NoSuchMethodException
异常。由于解析过程实际是根据标签名反射创建View的过程,可以推断是反射过程抛了异常。于是查看LayouterInflater#onCreateView()
反射创建的源码,
public final View createView(@NonNull Context viewContext, @NonNull String name,
@Nullable String prefix, @Nullable AttributeSet attrs){
...
clazz = Class.forName(prefix != null ? (prefix + name) : name, false, mContext.getClassLoader()).asSubclass(View.class); constructor = clazz.getConstructor(mConstructorSignature); constructor.setAccessible(true);
sConstructorMap.put(name, constructor);
...
final View view = constructor.newInstance(args);
return view;
}
其中,mConstructorSignature
为,
static final Class<?>[] mConstructorSignature = new Class[] { Context.class, AttributeSet.class};
[答案在此] 也就是反射调用类中参数表为(Context, AttributeSet)
的构造函数,创建出对象。显然易见,导致反射报错的原因,是自定义View中缺少了参数表为(Context, AttributeSet)
的构造函数。因此,自定义View时,如果要在xml布局中使用,一定要加上参数表为(Context, AttributeSet)
的构造函数。