在 APP 开发过程中,给一个 View 设置监听点击事件是常见的需求。一般两种实现方式:

1、设置 listener

view.setOnClickListener(onClickListener);

2、在 XML 文件中设置对应的 view 点击时候的回调方法,当然需要在 Activity 中编写用于回调的方法

// Activity
public void onClickView(){
	// do something
}

在 XML 设置 View 的 android:onClick 属性

<View
	android:layout_width="match_parent"
	android:layout_height="match_parent"
	android:onClick="onClickView" />

有的时候从 XML 布局里直接设定点击事件会比较方便(特别是在写 DEMO 项目的时候),这种做法平时用的人并不多,从使用方式上大致能猜出来,View 应该是在运行的时候,使用反射的方式从 Activity 找到 “onClickView” 方法并调用,因为这种做法并没有用到任何接口。

接下来,我们可以从源码中分析出 View 是怎么触发回调方法的。

我们知道,View 有多个构造方法,平时在 XML 布局渲染出来的 View 实例最后都是要调用四参的构造方法。

public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        this(context);

        final TypedArray a = context.obtainStyledAttributes(
                attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);
                
        for (int i = 0; i < N; i++) {
            int attr = a.getIndex(i);
            switch (attr) {
                ……
                // 处理 onClick 属性
                case R.styleable.View_onClick:
                    if (context.isRestricted()) {
                        throw new IllegalStateException("The android:onClick attribute cannot "
                                + "be used within a restricted context");
                    }

                    final String handlerName = a.getString(attr);
                    if (handlerName != null) {
                        // 给当前 View 实例设置一个 DeclaredOnClickListener 监听器
                        setOnClickListener(new DeclaredOnClickListener(this, handlerName));
                    }
                    break;
                }
        }
}

处理 onClick 属性的时候,先判断 View 的 Context 是否 isRestricted,如果是就抛出一个 IllegalStateException 异常。看看 isRestricted 方法。

a restricted context may disable specific features. For instance, a View associated with a restricted context would ignore particular XML attributes.

按照官方的说法,isRestricted 是用于判断当前的 Context 实例是否处于被限制的状态,处于被限制状态的 Context,会忽略某些特点的功能,比如 XML 的某些属性,很明显,我们在研究的 android:onClick 属性也会被忽略。

/**
     * Indicates whether this Context is restricted.
     *
     * @return {@code true} if this Context is restricted, {@code false} otherwise.
     *
     * @see #CONTEXT_RESTRICTED
     */
    public boolean isRestricted() {
        return false;
    }

接着,final String handlerName = a.getString(attr); 其实就是拿到了 android:onClick="onClickView" 中的 “onClickView” 这个字符串,然后使用了当前 View 的实例和 “onClickView” 创建了一个 DeclaredOnClickListener 实例,并设置为当前 View 的点击监听器。

/**
     * An implementation of OnClickListener that attempts to lazily load a
     * named click handling method from a parent or ancestor context.
     */
private static class DeclaredOnClickListener implements OnClickListener {
        private final View mHostView;
        private final String mMethodName;

        private Method mMethod;

        public DeclaredOnClickListener(@NonNull View hostView, @NonNull String methodName) {
            mHostView = hostView;
            mMethodName = methodName;
        }

        @Override
        public void onClick(@NonNull View v) {
            if (mMethod == null) {
                mMethod = resolveMethod(mHostView.getContext(), mMethodName);
            }

            try {
                mMethod.invoke(mHostView.getContext(), v);
            } catch (IllegalAccessException e) {
                throw new IllegalStateException(
                        "Could not execute non-public method for android:onClick", e);
            } catch (InvocationTargetException e) {
                throw new IllegalStateException(
                        "Could not execute method for android:onClick", e);
            }
        }

        @NonNull
        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) {
                    // Failed to find method, keep searching up the hierarchy.
                }

                if (context instanceof ContextWrapper) {
                    context = ((ContextWrapper) context).getBaseContext();
                } else {
                    // Can't search up the hierarchy, null out and fail.
                    context = null;
                }
            }

            final int id = mHostView.getId();
            final String idText = id == NO_ID ? "" : " with id '"
                    + mHostView.getContext().getResources().getResourceEntryName(id) + "'";
            throw new IllegalStateException("Could not find method " + mMethodName
                    + "(View) in a parent or ancestor Context for android:onClick "
                    + "attribute defined on view " + mHostView.getClass() + idText);
        }
}

到这里就清楚了,当点击 View 的时候,DeclaredOnClickListener 实例的 “onClick” 方法会被调用,接着会调用 “resolveMethod” 方法,使用反射的方式从 View 的 Context 中找一个叫 “onClickView” 方法。要注意的是,“onClickView” 方法必须是 public 类型的,不然反射调用时会抛出 IllegalAccessException 异常。

同时从源码也能看出,使用 android:onClick 设置点击事件的方式是从 Context 里面查找回调方法的,所以如果对于在 Fragment 的 XML 里创建的 View,是无法通过这种方式绑定 Fragment 中的回调方法的,因为 Fragment 自身并不是一个 Context,这里的 View 的 Context 其实是 FragmentActivity。

此外,从 DeclaredOnClickListener 类的注释也能看出 android:onClick 的功能,主要是起到懒加载的作用,只有到点击 View 的时候,才会知道哪个方法是用于点击回调的。

最后,特别需要补充说明的是,使用 android:onClick 给 View 设置点击事件,就意味着要在 Activity 里添加一个非接口的 public 方法。现在 Android 的开发趋势是 “不要把业务逻辑写在 Activity 类里面”,这样做有利于项目的维护,防止 Activity 爆炸,所以尽量不要在 Activity 里出现非接口、非生命周期的 public 方法。因此,随意使用 android:onClick 可能会“污染” Activity。