Android XML注册onClick事件详解

做项目的时候用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版本通常会混淆代码,方法的名称就改变了,所以最好的注册点击事件是在代码中设置。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值