Kotlin将只读lambda表达式作为监听器使用的坑

谷歌爸爸在今年的I/O大会上公布Kotlin成为官方支持的Android开发语言,于是我也学习了一个,并试着用Kotlin写了一个监听剪切板的应用。谁知上架商店之后没几天就发现出现了玄学的bug。

我的应用有一个开关,可以开启或关闭一个Service,这个Service在开启的时候会把剪切板的监听器添加到ClipboardManager,而在停止的时候则会移除该监听器。奇怪的事情是,添加是可以的,移除却失败了。

百思不得其解之下,我开始查看ClipboardManager的源码,其中添加和移除监听器的源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public void addPrimaryClipChangedListener(OnPrimaryClipChangedListener what) {
synchronized (mPrimaryClipChangedListeners) {
if (mPrimaryClipChangedListeners.size() == 0) {
try {
getService().addPrimaryClipChangedListener(
mPrimaryClipChangedServiceListener, mContext.getOpPackageName());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
mPrimaryClipChangedListeners.add(what);
}
}

public void removePrimaryClipChangedListener(OnPrimaryClipChangedListener what) {
synchronized (mPrimaryClipChangedListeners) {
mPrimaryClipChangedListeners.remove(what);
if (mPrimaryClipChangedListeners.size() == 0) {
try {
getService().removePrimaryClipChangedListener(
mPrimaryClipChangedServiceListener);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
}
}

而这个mPrimaryClipChangedListeners不过是个ArrayList而已。

如果排除了这是Android系统的bug,那么唯一的解释就是:移除监听器的时候传递的对象和一开始添加的监听器并不是同一个!(请自行脑补名侦探柯南BGM)

那么就让我们来看看这个Service被编译成什么样子了吧。下面是原来的Kotlin代码(省略了业务逻辑,只保留几个Log):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class CopyListenerService : Service() {
override fun onBind(p0: Intent?): IBinder? = null

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
clipboardManager.addPrimaryClipChangedListener(onPrimaryClipChangedListener)
return START_STICKY
}

override fun onDestroy() {
super.onDestroy()
clipboardManager.removePrimaryClipChangedListener(onPrimaryClipChangedListener)
}

val clipboardManager by lazy { getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager }
val onPrimaryClipChangedListener = {
Log.d(TAG, "Clip Item count: " + clipboardManager.primaryClip.itemCount)
Unit
}

companion object {
val TAG = "CopyListenerService"
}
}

可以看到,我的onPrimaryClipChangedListener只是一个纯洁的lambda表达式而已。

我们点击菜单Tools - Kotlin - Show Kotlin Bytecode,右边会多出一个编辑器,我们点击那个编辑器顶部的Decompile按钮,左边的编辑器就多了一个CopyListenerService.decompiled.java的文件。由于是反编译的,所以非常丑而且还有很多IDE飘红,不过这不影响我们找出元凶。下面是反编译结果,巨长:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
// CopyListenerServiceKt$sam$OnPrimaryClipChangedListener$15d0add3.java
package com.perqin.copyshare;

import android.content.ClipboardManager.OnPrimaryClipChangedListener;
import kotlin.Metadata;
import kotlin.jvm.functions.Function0;
import kotlin.jvm.internal.Intrinsics;

@Metadata(
mv = {1, 1, 6},
bv = {1, 0, 1},
k = 3
)
final class CopyListenerServiceKt$sam$OnPrimaryClipChangedListener$15d0add3 implements OnPrimaryClipChangedListener {
// $FF: synthetic field
private final Function0 function;

CopyListenerServiceKt$sam$OnPrimaryClipChangedListener$15d0add3(Function0 var1) {
this.function = var1;
}

// $FF: synthetic method
public final void onPrimaryClipChanged() {
Intrinsics.checkExpressionValueIsNotNull(this.function.invoke(), "invoke(...)");
}
}
// CopyListenerService.java
package com.perqin.copyshare;

import android.app.Service;
import android.content.ClipboardManager;
import android.content.Intent;
import android.content.ClipboardManager.OnPrimaryClipChangedListener;
import android.os.IBinder;
import android.util.Log;
import kotlin.Lazy;
import kotlin.LazyKt;
import kotlin.Metadata;
import kotlin.TypeCastException;
import kotlin.Unit;
import kotlin.jvm.functions.Function0;
import kotlin.jvm.internal.DefaultConstructorMarker;
import kotlin.jvm.internal.PropertyReference1Impl;
import kotlin.jvm.internal.Reflection;
import kotlin.reflect.KProperty;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

@Metadata(
mv = {1, 1, 6},
bv = {1, 0, 1},
k = 1,
d1 = {"\u00006\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\b\u0002\n\u0002\u0018\u0002\n\u0002\b\u0005\n\u0002\u0018\u0002\n\u0002\u0010\u0002\n\u0002\b\u0003\n\u0002\u0018\u0002\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0002\n\u0002\u0010\b\n\u0002\b\u0005\u0018\u0000 \u00182\u00020\u0001:\u0001\u0018B\u0005¢\u0006\u0002\u0010\u0002J\u0014\u0010\u000e\u001a\u0004\u0018\u00010\u000f2\b\u0010\u0010\u001a\u0004\u0018\u00010\u0011H\u0016J\b\u0010\u0012\u001a\u00020\u000bH\u0016J\"\u0010\u0013\u001a\u00020\u00142\b\u0010\u0015\u001a\u0004\u0018\u00010\u00112\u0006\u0010\u0016\u001a\u00020\u00142\u0006\u0010\u0017\u001a\u00020\u0014H\u0016R\u001b\u0010\u0003\u001a\u00020\u00048FX\u0086\u0084\u0002¢\u0006\f\n\u0004\b\u0007\u0010\b\u001a\u0004\b\u0005\u0010\u0006R\u0017\u0010\t\u001a\b\u0012\u0004\u0012\u00020\u000b0\n¢\u0006\b\n\u0000\u001a\u0004\b\f\u0010\r¨\u0006\u0019"},
d2 = {"Lcom/perqin/copyshare/CopyListenerService;", "Landroid/app/Service;", "()V", "clipboardManager", "Landroid/content/ClipboardManager;", "getClipboardManager", "()Landroid/content/ClipboardManager;", "clipboardManager$delegate", "Lkotlin/Lazy;", "onPrimaryClipChangedListener", "Lkotlin/Function0;", "", "getOnPrimaryClipChangedListener", "()Lkotlin/jvm/functions/Function0;", "onBind", "Landroid/os/IBinder;", "p0", "Landroid/content/Intent;", "onDestroy", "onStartCommand", "", "intent", "flags", "startId", "Companion", "production sources for module app"}
)
public final class CopyListenerService extends Service {
@NotNull
private final Lazy clipboardManager$delegate = LazyKt.lazy((Function0)(new Function0() {
// $FF: synthetic method
// $FF: bridge method
public Object invoke() {
return this.invoke();
}

@NotNull
public final ClipboardManager invoke() {
Object var10000 = CopyListenerService.this.getSystemService("clipboard");
if(var10000 == null) {
throw new TypeCastException("null cannot be cast to non-null type android.content.ClipboardManager");
} else {
return (ClipboardManager)var10000;
}
}
}));
@NotNull
private final Function0 onPrimaryClipChangedListener = (Function0)(new Function0() {
// $FF: synthetic method
// $FF: bridge method
public Object invoke() {
this.invoke();
return Unit.INSTANCE;
}

public final void invoke() {
Log.d(CopyListenerService.Companion.getTAG(), "Clip Item count: " + CopyListenerService.this.getClipboardManager().getPrimaryClip().getItemCount());
}
});
@NotNull
private static final String TAG = "CopyListenerService";
// $FF: synthetic field
static final KProperty[] $$delegatedProperties = new KProperty[]{(KProperty)Reflection.property1(new PropertyReference1Impl(Reflection.getOrCreateKotlinClass(CopyListenerService.class), "clipboardManager", "getClipboardManager()Landroid/content/ClipboardManager;"))};
public static final CopyListenerService.Companion Companion = new CopyListenerService.Companion((DefaultConstructorMarker)null);

@Nullable
public IBinder onBind(@Nullable Intent p0) {
return null;
}

public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
ClipboardManager var10000 = this.getClipboardManager();
CopyListenerServiceKt$sam$OnPrimaryClipChangedListener$15d0add3 var10001 = new CopyListenerServiceKt$sam$OnPrimaryClipChangedListener$15d0add3;
Function0 var10003 = this.onPrimaryClipChangedListener;
if(this.onPrimaryClipChangedListener == null) {
Object var10002 = null;
} else {
var10001.<init>(var10003);
}

var10000.addPrimaryClipChangedListener((OnPrimaryClipChangedListener)var10001);
return 1;
}

public void onDestroy() {
super.onDestroy();
ClipboardManager var10000 = this.getClipboardManager();
CopyListenerServiceKt$sam$OnPrimaryClipChangedListener$15d0add3 var10001 = new CopyListenerServiceKt$sam$OnPrimaryClipChangedListener$15d0add3;
Function0 var10003 = this.onPrimaryClipChangedListener;
if(this.onPrimaryClipChangedListener == null) {
Object var10002 = null;
} else {
var10001.<init>(var10003);
}

var10000.removePrimaryClipChangedListener((OnPrimaryClipChangedListener)var10001);
}

@NotNull
public final ClipboardManager getClipboardManager() {
Lazy var1 = this.clipboardManager$delegate;
KProperty var3 = $$delegatedProperties[0];
return (ClipboardManager)var1.getValue();
}

@NotNull
public final Function0 getOnPrimaryClipChangedListener() {
return this.onPrimaryClipChangedListener;
}

@Metadata(
mv = {1, 1, 6},
bv = {1, 0, 1},
k = 1,
d1 = {"\u0000\u0014\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u000e\n\u0002\b\u0003\b\u0086\u0003\u0018\u00002\u00020\u0001B\u0007\b\u0002¢\u0006\u0002\u0010\u0002R\u0014\u0010\u0003\u001a\u00020\u0004X\u0086D¢\u0006\b\n\u0000\u001a\u0004\b\u0005\u0010\u0006¨\u0006\u0007"},
d2 = {"Lcom/perqin/copyshare/CopyListenerService$Companion;", "", "()V", "TAG", "", "getTAG", "()Ljava/lang/String;", "production sources for module app"}
)
public static final class Companion {
@NotNull
public final String getTAG() {
return CopyListenerService.TAG;
}

private Companion() {
}

// $FF: synthetic method
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}

接下来,我们来简单分析试试。

首先,我们的lambda监听器被转换成什么代码了呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
@NotNull
private final Function0 onPrimaryClipChangedListener = (Function0)(new Function0() {
// $FF: synthetic method
// $FF: bridge method
public Object invoke() {
this.invoke();
return Unit.INSTANCE;
}

public final void invoke() {
Log.d(CopyListenerService.Companion.getTAG(), "Clip Item count: " + CopyListenerService.this.getClipboardManager().getPrimaryClip().getItemCount());
}
});

我们发现,我们在监听器里的实现被转换成了一个Function0对象,具体的实现代码被放在了invoke方法里。

接下来,我们来看看我们添加监听器的代码被转换成的样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
ClipboardManager var10000 = this.getClipboardManager();
CopyListenerServiceKt$sam$OnPrimaryClipChangedListener$15d0add3 var10001 = new CopyListenerServiceKt$sam$OnPrimaryClipChangedListener$15d0add3;
Function0 var10003 = this.onPrimaryClipChangedListener;
if(this.onPrimaryClipChangedListener == null) {
Object var10002 = null;
} else {
var10001.<init>(var10003);
}

var10000.addPrimaryClipChangedListener((OnPrimaryClipChangedListener)var10001);
return 1;
}

可以看到,我们添加监听器的时候传递的对象是var10001,而它是一个CopyListenerServiceKt$sam$OnPrimaryClipChangedListener$15d0add3类的实例。这个类又是个什么鬼?我们接着看:

1
2
3
4
5
6
7
8
9
10
11
12
13
final class CopyListenerServiceKt$sam$OnPrimaryClipChangedListener$15d0add3 implements OnPrimaryClipChangedListener {
// $FF: synthetic field
private final Function0 function;

CopyListenerServiceKt$sam$OnPrimaryClipChangedListener$15d0add3(Function0 var1) {
this.function = var1;
}

// $FF: synthetic method
public final void onPrimaryClipChanged() {
Intrinsics.checkExpressionValueIsNotNull(this.function.invoke(), "invoke(...)");
}
}

原来如此,这个名字巨长的类正是实现了OnPrimaryClipChangedListener的类,它持有一个Function0的引用,而它的onPrimaryClipChanged实现其实不过是调用这个对象的invoke方法。

现在再回去看看添加监听器的代码,我们就可以整理出这样的思路:

  • 首先把lambda里的实现语句封装到一个Function0
  • 然后让这个Service拥有这个Function0类的实例,这也就对应Kotlin代码里的val变量定义
  • 需要添加监听器的时候,构造一个实现了监听器接口的CopyListenerServiceKt$sam$OnPrimaryClipChangedListener$15d0add3对象
  • 然后把我们的Function0对象丢进去
  • 最后把这个构造出来的对象作为真正的监听器丢进去

等等,好像有哪里不对……构造一个对象?!?让我们看看这行代码:

1
CopyListenerServiceKt$sam$OnPrimaryClipChangedListener$15d0add3 var10001 = new CopyListenerServiceKt$sam$OnPrimaryClipChangedListener$15d0add3;

这是个局部变量啊!!!丢进去之后就拿不到它了啊!那我还怎么remove它呢??

来看看移除监听器是怎么写的:

1
2
3
4
5
6
7
8
9
10
11
12
13
public void onDestroy() {
super.onDestroy();
ClipboardManager var10000 = this.getClipboardManager();
CopyListenerServiceKt$sam$OnPrimaryClipChangedListener$15d0add3 var10001 = new CopyListenerServiceKt$sam$OnPrimaryClipChangedListener$15d0add3;
Function0 var10003 = this.onPrimaryClipChangedListener;
if(this.onPrimaryClipChangedListener == null) {
Object var10002 = null;
} else {
var10001.<init>(var10003);
}

var10000.removePrimaryClipChangedListener((OnPrimaryClipChangedListener)var10001);
}

咳咳,这么骚的吗?

所以,我们终于得出了结论:虽然我们的lambda表达式对应的是不变的Function0对象,但是每次从它得到的监听器却不是同一个监听器!

有了这个结论,也就有了一个解决方法:我们不要让onPrimaryClipChangedListener成为lambda表达式,而是一个正正经经的监听器匿名内部类对象,我们改改代码:

1
2
3
4
val onPrimaryClipChangedListener = ClipboardManager.OnPrimaryClipChangedListener {
Log.d(TAG, "Clip Item count: " + clipboardManager.primaryClip.itemCount)
Unit
}

额额,好像只是加了两个单词……接下来看看反编译的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@NotNull
private final OnPrimaryClipChangedListener onPrimaryClipChangedListener = (OnPrimaryClipChangedListener)(new OnPrimaryClipChangedListener() {
public final void onPrimaryClipChanged() {
Log.d(CopyListenerService.Companion.getTAG(), "Clip Item count: " + CopyListenerService.this.getClipboardManager().getPrimaryClip().getItemCount());
}
});

public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
this.getClipboardManager().addPrimaryClipChangedListener(this.onPrimaryClipChangedListener);
return 1;
}

public void onDestroy() {
super.onDestroy();
this.getClipboardManager().removePrimaryClipChangedListener(this.onPrimaryClipChangedListener);
}

这次,this.onPrimaryClipChangedListener终于不再是一个Function0对象,而是一个监听器了!

然而,过了那么久都没有人在应用商店反馈这个bug,我该高兴还是不高兴呢……


Kotlin将只读lambda表达式作为监听器使用的坑
https://perqin.github.io/2017/06/27/kotlin-issue-on-adding-lambda-as-listener/
作者
Perqin
发布于
2017年6月27日
许可协议