前言
此篇文章记录日常遇到的一个小坑:Handler的removeCallbacksAndMessages没生效。
正文
需求:
需求:有1-5个超时任务,如果某个任务在规定时间内完成,需要取消对应的超时任务;
这个需求并不复杂,如果是比较简单的延时任务,可以使用Handler.postDelayed添加延时任务,如果任务在预期内完成,可以通过Handler.removeCallbacksAndMessages删除掉对应的任务:
Handler handler = new Handler();
// 添加5个超时任务,每一个任务的Tag通过generateToken()生成
for (int i = 0; i < 5; i++) {
Log.e("lzp", "start post delay task " + i);
int finalI = i;
handler.postDelayed(() -> Log.e("lzp", "timeout " + finalI), generateToken(i), 3000);
}
// 1s后取消所有的延时任务
handler.postDelayed(() -> {
for (int i = 0; i < 5; i++) {
Log.e("lzp", "removeCallbacksAndMessages " + i);
handler.removeCallbacksAndMessages(generateToken(i));
}
}, 1000);
// 生成任务的Token
private String generateToken(int i) {
return “Task” + i;
}
如果上面的代码正常运行,按照我的期望这5个超时任务应该会被取消掉,但是运行的结果却是:
分析
看来Handler.removeCallbacksAndMessages并没有成功的取消对应的任务,只能去看源码了:
public final void removeCallbacksAndMessages(Object token) {
mQueue.removeCallbacksAndMessages(this, token);
}
// MessageQueue.java
void removeCallbacksAndMessages(Handler h, Object object) {
if (h == null) {
return;
}
synchronized (this) {
Message p = mMessages;
// Remove all messages at front.
// 请注意这里使用的是 ==,而不是equals
while (p != null && p.target == h
&& (object == null || p.obj == object)) {
Message n = p.next;
mMessages = n;
p.recycleUnchecked();
p = n;
}
// Remove all messages after front.
while (p != null) {
Message n = p.next;
if (n != null) {
// 请注意这里使用的是 ==,而不是equals
if (n.target == h && (object == null || n.obj == object)) {
Message nn = n.next;
n.recycleUnchecked();
p.next = nn;
continue;
}
}
p = n;
}
}
}
源码也非常的简单,很快就发现了问题可能发生的地方,这里判断Token使用的“==”而不是equals,我使用的是字符串作为Token,确实可能这里判断是不相等的,之后通过断点确认此处判断的确是false。那么为什么我的字符串内存地址不同呢,字符串常量池没有复用吗?为了解决这个疑问,只能去查看编译后的字节码,看看generateTag到底是如何工作的。
首先找到生成的apk文件,直接双击打开,此时可能看到多个dex文件,所以要找到类具体被打包到了那个dex文件中,然后找到对应的方法右键点击Show ByteCode:
.method private generateTag(I)Ljava/lang/String;
.registers 4
.param p1, "i" # I
.line 56
// 创建StringBuilder
new-instance v0, Ljava/lang/StringBuilder;
// 执行StringBuilder构造方法
invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V
// 创建常量Task
const-string v1, "Task"
// StringBuilder拼接Task字符串
invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v0
// 拼接参数I
invoke-virtual {v0, p1}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder;
move-result-object v0
// 调用StringBuilder的toString方法
invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
// 把toString的结果赋值给v0
move-result-object v0
// 返回v0
return-object v0
.end method
字节码的语法跟Java差不多,通过分析字节码发现,JVM会把我们的字符串的“+”在编译的时候替换为StringBuilder的拼接操作,所以再去查看一下StringBuilder的toString方法:
@Override
public String toString() {
if (count == 0) {
return "";
}
return StringFactory.newStringFromChars(0, count, value);
}
@FastNative
static native String newStringFromChars(int offset, int charCount, char[] data);
StringBuilder的toString调用native方法newStringFromChars,这个newStringFromChars方法实际上就是String内部用来创建字符串的类,例如:
public static String valueOf(char c) {
// Android-changed: Replace constructor call with call to new StringFactory class.
// char data[] = {c};
// return new String(data, true);
return StringFactory.newStringFromChars(0, 1, new char[] { c });
}
结论
所以通过以上的分析,得出结论:字符串使用“+”或StringBuilder进行拼接,返回的是相同内容的不同对象,而Handler.removeCallbacksAndMessages使用“==”判断Token的内存地址是否相等,所以导致移除任务失败。