一、引子
在xml文件中,如果想使用TextView、WebView、Button、RelativeLayout等系统View,直接写类名即可,但如果是自定义的View,则需要写带有包名的全类名,否则运行时会报错。这是为什么呢?
二、查找原因
我们知道Android是通过LayoutInflater.inflate()方法解析xml文件创建View的,从这个方法找下答案:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
调用了LayoutInflater类的inflate(XmlPullParser, ViewGroup, Boolean)方法
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
View result = root;
try {
// Look for the root node.
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
final String name = parser.getName();
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 {
// Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, inflaterContext, attrs); // ①
ViewGroup.LayoutParams params = null;
if (root != null) {
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
// Inflate all children under temp against its context.
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;
}
}
}
// 省略异常处理代码
finally {
// Don't retain static reference on context.
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;
}
return result;
}
}
对于xml文件中最外层标签名不为merge的布局而言,主要做两件事;
①创建xml最外层标签名的View;
②解析最外层标签内部的View。
2.1 createViewFromTag()
private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) {
return createViewFromTag(parent, name, context, attrs, false);
}
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
// 省略其他代码
try {
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);// A
} else {
view = createView(name, null, attrs); // B
}
} finally {
mConstructorArgs[0] = lastContext;
}
return view;
}
可以看到如果标签名字不带".",也就是标签就是类名(SimpleName),不包含包名,则执行onCreateView(parent, name, attrs),否则执行createView(name, null, attrs)。
2.1.1 createView(name, null, attrs)
先看下createView(name, null, attrs)。
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
Constructor<? extends View> constructor = sConstructorMap.get(name);
if (constructor != null && !verifyClassLoader(constructor)) {
constructor = null;
sConstructorMap.remove(name);
}
Class<? extends View> clazz = null;
try {
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
sConstructorMap.put(name, constructor);
}
Object lastContext = mConstructorArgs[0];
if (mConstructorArgs[0] == null) {
// Fill in the context if not already within inflation.
mConstructorArgs[0] = mContext;
}
Object[] args = mConstructorArgs;
args[1] = attrs;
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]));
}
mConstructorArgs[0] = lastContext;
return view;
}
主要是通过完全限定类名(包含包名)加载Class,然后通过反射调用其构造函数生成View。
2.1.2 onCreateView(parent, name, attrs)
protected View onCreateView(String name, AttributeSet attrs)
throws ClassNotFoundException {
return createView(name, "android.view.", attrs);
}
onCreateView()方法非常简单,调用的是createView()方法,只是prefix不为null。因此,对于LayoutInflater类而言,如果不含包名的类名作为标签,则加上"android.view."来构造全限定类名。
2.2 问题解决了吗?
问题似乎是解决了,但TextView、ImageView、ListView、GridView等都是在android.widget包下,如果直接加上"android.view",加载class时会报ClassNotFoundException的!Android是如何解决的呢?
2.2.1 LayoutInflater.from()
看下LayoutInflater的创建方法。
public static LayoutInflater from(Context context) {
LayoutInflater LayoutInflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
return LayoutInflater;
}
直接看下ContextImpl.getSystemService()方法
@Override
public Object getSystemService(String name) {
return SystemServiceRegistry.getSystemService(this, name);
}
跟下去
public static Object getSystemService(ContextImpl ctx, String name) {
ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
return fetcher != null ? fetcher.getService(ctx) : null;
}
SYSTEM_SERVICE_FETCHERS是HashMap类型变量,
private static final HashMap<String, ServiceFetcher<?>> SYSTEM_SERVICE_FETCHERS =
new HashMap<String, ServiceFetcher<?>>();
数据在SystemServiceRegistry类的静态语句块中填充
registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
new CachedServiceFetcher<LayoutInflater>() {
@Override
public LayoutInflater createService(ContextImpl ctx) {
return new PhoneLayoutInflater(ctx.getOuterContext());
}});
registerService()方法
private static <T> void registerService(String serviceName, Class<T> serviceClass,
ServiceFetcher<T> serviceFetcher) {
SYSTEM_SERVICE_NAMES.put(serviceClass, serviceName);
SYSTEM_SERVICE_FETCHERS.put(serviceName, serviceFetcher);
}
因此
public static LayoutInflater from(Context context) {
ServiceFetcher<?> fetcher = new CachedServiceFetcher<LayoutInflater>() {
@Override
public LayoutInflater createService(ContextImpl ctx) {
return new PhoneLayoutInflater(ctx.getOuterContext());
}};
return fetcher != null ? fetcher.getService(ctx) : null;
}
看下CachedServiceFetcher类
static abstract class CachedServiceFetcher<T> implements ServiceFetcher<T> {
CachedServiceFetcher() {
}
@Override
public final T getService(ContextImpl ctx) {
// 省略同步和异常处理代码
T service = null;
@ServiceInitializationState int newState = ContextImpl.STATE_NOT_FOUND;
// This thread is the first one to get here. Instantiate the service
// *without* the cache lock held.
service = createService(ctx);
return service;
}
}
}
public abstract T createService(ContextImpl ctx) throws ServiceNotFoundException;
}
因此LayoutInflater.from()方法返回的是PhoneLayoutInflater类。
2.2.2 PhoneLayoutInflater
public class PhoneLayoutInflater extends LayoutInflater {
private static final String[] sClassPrefixList = {
"android.widget.",
"android.webkit.",
"android.app."
};
/** Override onCreateView to instantiate names that correspond to the
widgets known to the Widget factory. If we don't find a match,
call through to our super class.
*/
@Override protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
for (String prefix : sClassPrefixList) {
try {
View view = createView(name, prefix, attrs);
if (view != null) {
return view;
}
} catch (ClassNotFoundException e) {
// In this case we want to let the base class take a crack
// at it.
}
}
return super.onCreateView(name, attrs);
}
}
PhoneLayoutInflater非常简单,复写了onCreateView()方法,依次尝试"android.widget.", “android.webkit.”, "android.app."三个前缀,如果在这三个包名下有找到指定的类,则直接创建并返回,否则执行父类(LayoutInflater)的onCreateView()方法。
三、总结
结论很简单,xml布局中系统View标签不需要带包名的原因是,LayoutInflater及其子类PhoneLayoutInflater会在创建View对象时替开发者加上!