Android 中多次设置 OnClickListener 只执行一次吗?

问题

对于 Android 初学者,可能对这个问题会比较疑惑: 对于一个 View,比如 Button,如果为其设置多次点击监听 OnClickListener 回调方法,同时还在布局中设置了 onClick 属性,并且也实现了点击回调方法,那么问题来了,哪些回调方法会执行呢?又是以怎样的顺序执行呢?请跟随脚步和我一探究竟…

实验现象

我们先来做个实验,观察一下实验现象。
首先在布局文件中声明一个 Button,并为其设置好点击属性:

1
2
3
4
5
6
<Button
android:id="@+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="click()"
android:text="BUTTON"/>

嗯,对,然后再在 Activity 中实现方法:

1
2
3
void click(View v) {
Log.i(TAG, "click: in layout file");
}

这样第一组测试样例放置好了,第二组和第三组很容易,先后在 onCreate() 中设置两次监听,都记得打上 Log 日志:

1
2
3
4
5
6
7
8
9
10
11
12
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.i(TAG, "click: in onCreate() first");
}
});
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.i(TAG, "click: in onCreate() second");
}
});

然后,见证奇迹了,运行观察实验结果:

1
I/MainActivity: click: in onCreate() second

你没有看错,只有一条结果,而且是第二次的结果,说明优先级 java 设置监听的优先级大于布局文件,而且最后一次设置的监听会覆盖前一次设置的监听。

结论
点击监听优先级: 之后设置的优先级大于之前设置的优先级,代码中设置的优先级大于布局中设置的优先级

寻找答案

结果很出乎意料,也许猜的都会执行,也许你猜的布局先执行,却没想到只有最后一次设置的执行,这其中的神秘之处究竟何在呢?让我们 Read The Fuck Source Code.

setOnClickListener

先从 setOnClickListener() 入手:

1
public void setOnClickListener(@Nullable OnClickListener l) {
    if (!isClickable()) {
        setClickable(true);
    }
    getListenerInfo().mOnClickListener = l;
}

先是将该 View 设置为可点击状态,所以即便一个 View 是不可点击的,你为其设置了监听,也会将其恢复成可点击状态。再是将 getListenerInfo() 返回的对象中的成员 mOnClickListener 直接复制为参数 l,还不明白?再看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ListenerInfo getListenerInfo() {
if (mListenerInfo != null) {
return mListenerInfo;
}
// 如果为空,重新创建用于保存所有监听器的容器类
mListenerInfo = new ListenerInfo();
return mListenerInfo;
}

// 容器类定义
static class ListenerInfo {
...
// 是数组 事件发生回调所有监听器
private CopyOnWriteArrayList<OnAttachStateChangeListener> mOnAttachStateChangeListeners;

// 不是数组 直接对其进行赋值
public OnClickListener mOnClickListener;

protected OnLongClickListener mOnLongClickListener;
...

哈,是不是恍然大悟,所以每一次设置监听都是对这个 mOnClickListener 成员进行替换赋值,所以说最后一次设置监听才是有效的。

布局文件

Java 中的监听确实明白了,那么布局文件中又是怎么回事呢?
通过 setOnClickListener() 可以对 mOnClickListener 进行修改,那么我们找一下这个函数在哪里有调用,果然找到了,
View 的构造函数中,有这样两行代码:

1
2
3
4
5
// 获取布局 android:onClick="" 属性值
final String handlerName = a.getString(attr);
if (handlerName != null) {
setOnClickListener(new DeclaredOnClickListener(this, handlerName));
}

也就是说,View 默认就设置了一个叫做 DeclaredOnClickListener 的监听器:

1
2
3
4
5
6
7
8
9
10
11
12
private static class DeclaredOnClickListener implements OnClickListener {

...
@Override
public void onClick(@NonNull View v) {
if (mMethod == null) {
mMethod = resolveMethod(mHostView.getContext(), mMethodName);
}
// 调用方法,并携带参数 v
mMethod.invoke(mHostView.getContext(), v);
...
}

默认的实现是通过 resolveMethod() 获取到方法,并调用之:

1
2
3
4
5
6
7
private Method resolveMethod(@Nullable Context context, @NonNull String name) {
...
if (!context.isRestricted()) {
// 使用参数 name 反射取得方法
return context.getClass().getMethod(mMethodName, View.class);
}
...

是不是很眼熟了,这不就是反射获取到方法吗?!一切都明白了吧,在 View 创建之初就设置了一个默认监听, 默认监听是调用的所在 Context 中的符合布局中定义的方法签名的方法。

结论

所以,总的来说是这样的,View 在构造之初就默认设置好了一个监听器,View 的构造也是在 onCreate() 方法执行前完成的,对于每一次设置监听都会覆盖上一次的监听,所以最后一次设置的监听才会是有效的。