做项目的时候用Fragment展示界面,在onCreateView里加载XML布局文件。布局文件种有一个Button按钮控件设置了onClick属性,对应的方法实现则写在了Fragment里。随后运行程序发现按钮点击没有反应,最开始怀疑是定义的方法接口不正确,仔细检查之后发现实现没有问题。百思不得其解之下就去查看了一下系统在加载XML文件时是如何解析android:onClick事件的源码,终于明白了问题发生的原因。
既然是从XML布局文件种动态解析生成的视图树(ViewTree),自然第一个要查看的类就是LayoutInflater.inflate方法,该方法的源码如下:
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();
}
}
这个方法首先获取了资源对象,然后根据布局文件资源id获取布局文件对应的XML解析器。多说一句,这个方法调用有一个需要注意的地方,如果root也就是加载的布局到的父容器为空,那么布局文件最外面一层的宽高配置、Gravity等都不会被解析。这个问题在写ListView的Adapter时候经常会出现,让人觉得莫名奇妙。如果希望最外层的布局参数起效,需要保证root不为空。root不为空,第三个参数为false的情况下返回的是布局文件的根对象,如果为true的话返回的对象就是传入的root。inflate(parser…)这个方法内部会调用rInflate方法解析XML文件里的每个节点,r就是recursive递归的意思。
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
...
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
...
final String name = parser.getName();
if (TAG_REQUEST_FOCUS.equals(name)) {
parseRequestFocus(parser, parent);
} else if (TAG_TAG.equals(name)) {
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
// 这里是从根节点开始调用,如果根节点为include,也就是当前节点深度为0
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) { // merge标签执行到这里就表示merge被嵌套在其他的ViewGroup里
throw new InflateException("<merge /> must be the root element");
} else {
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);
}
}
可以看到这个方法里对Merge、include等标签都做了单独的判断。Button按钮属于View标签,对应执行的就是最后的else操作,所以要查看createViewFromTag这个方法的实现。
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
....
// 使用自定义的工厂类生成View对象,这里没有自定义的工厂类所以不用考虑这些逻辑
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
// 比如Button因为是系统控件所以走这里
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {// 比如com.example.MyView就会走这里,自定义控件生成
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
return view;
} catch (InflateException e) {
...// 异常处理逻辑
}
}
顺着onCreateView方法就能找到LayoutInflater最终生成Button控件是调用了Button的构造函数对象反射生成了一个按钮对象。代码逻辑如下:
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
Constructor<? extends View> constructor = sConstructorMap.get(name); // LayoutInflater实际上对这些控件的构造函数进行了缓存,这样就能提高解析生成对象的效率
Class<? extends View> clazz = null;
try { // 如果是头一次解析该对象,那么就要通过classloader根据对象的类名加载类对象
if (constructor == null) {
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
// 然后获取类对象里的构造函数对象,并放入缓存中
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
sConstructorMap.put(name, constructor);
} else {
...
}
Object[] args = mConstructorArgs;
args[1] = attrs;
// 到了这里就是通过构造函数生成View对象了
final View view = constructor.newInstance(args);
if (view instanceof ViewStub) { // 这里对ViewStub做特殊处理
final ViewStub viewStub = (ViewStub) view;
viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
}
return view;
} catch (NoSuchMethodException e) {
...
}
}
接下来查看Button的类继承关系,可以发现Button继承自TextView,而TextView继承自View。所以可以知道Button的构造函数执行之前会执行TextView的构造函数,TextView的构造函数执行之前又会执行View的构造函数。因为XML文件解析过程中控件的属性会被放到AttributeSet中,所以查看带有AttributeSet的构造函数就知道android:onClick的处理过程。在View的构造函数中找到了如下的代码:
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
this(context);
.... // 其他的初始化
case R.styleable.View_onClick:
...
final String handlerName = a.getString(attr);
if (handlerName != null) {
setOnClickListener(new DeclaredOnClickListener(this, handlerName));
}
break;
如果做过View自定义控件的属性就会知道R.styleable.View_onClick这个就是onClick所代表的属性常量。可以看到setOnClickListener设置了点击的回调接口对象,这个对象被包装成DeclaredOnClickListener类型。查看DeclaredOnClickListener的源码如下:
private static class DeclaredOnClickListener implements OnClickListener {
...
private Method mMethod;
@Override // 点击按钮时实际调用的回调函数
public void onClick(@NonNull View v) {
if (mMethod == null) {
mMethod = resolveMethod(mHostView.getContext(), mMethodName);
}
// 使用反射回调context里的android:onClick方法
try {
mMethod.invoke(mHostView.getContext(), v);
} catch (IllegalAccessException e) {
....
}
}
@NonNull // 从Context对象中获取注册的方法对象
private Method resolveMethod(@Nullable Context context, @NonNull String name) {
while (context != null) {
try {
if (!context.isRestricted()) {
return context.getClass().getMethod(mMethodName, View.class);
}
} catch (NoSuchMethodException e) {
}
}
}
}
上面的getContext就是Button所在的activity对象,所以如果注册方法在Activity里那么就能够回调成功。不过android:onClick注册在debug时能运行正常,切换成release版本时就又有问题了。这是因为release版本通常会混淆代码,方法的名称就改变了,所以最好的注册点击事件是在代码中设置。