前言
此篇文章記錄日常遇到的一個小坑:Handler的removeCallbacksAndMessages沒生效。
正文
需求:
需求:有1-5個超時任務(wù),如果某個任務(wù)在規(guī)定時間內(nèi)完成,需要取消對應(yīng)的超時任務(wù);
這個需求并不復(fù)雜,如果是比較簡單的延時任務(wù),可以使用Handler.postDelayed添加延時任務(wù),如果任務(wù)在預(yù)期內(nèi)完成,可以通過Handler.removeCallbacksAndMessages刪除掉對應(yīng)的任務(wù):
Handler handler = new Handler();
// 添加5個超時任務(wù),每一個任務(wù)的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后取消所有的延時任務(wù)
handler.postDelayed(() -> {
for (int i = 0; i < 5; i++) {
Log.e("lzp", "removeCallbacksAndMessages " + i);
handler.removeCallbacksAndMessages(generateToken(i));
}
}, 1000);
// 生成任務(wù)的Token
private String generateToken(int i) {
return “Task” + i;
}
如果上面的代碼正常運(yùn)行,按照我的期望這5個超時任務(wù)應(yīng)該會被取消掉,但是運(yùn)行的結(jié)果卻是:

分析
看來Handler.removeCallbacksAndMessages并沒有成功的取消對應(yīng)的任務(wù),只能去看源碼了:
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;
}
}
}
源碼也非常的簡單,很快就發(fā)現(xiàn)了問題可能發(fā)生的地方,這里判斷Token使用的“==”而不是equals,我使用的是字符串作為Token,確實(shí)可能這里判斷是不相等的,之后通過斷點(diǎn)確認(rèn)此處判斷的確是false。那么為什么我的字符串內(nèi)存地址不同呢,字符串常量池沒有復(fù)用嗎?為了解決這個疑問,只能去查看編譯后的字節(jié)碼,看看generateTag到底是如何工作的。
首先找到生成的apk文件,直接雙擊打開,此時可能看到多個dex文件,所以要找到類具體被打包到了那個dex文件中,然后找到對應(yīng)的方法右鍵點(diǎn)擊Show ByteCode:

.method private generateTag(I)Ljava/lang/String;
.registers 4
.param p1, "i" # I
.line 56
// 創(chuàng)建StringBuilder
new-instance v0, Ljava/lang/StringBuilder;
// 執(zhí)行StringBuilder構(gòu)造方法
invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V
// 創(chuàng)建常量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
// 拼接參數(shù)I
invoke-virtual {v0, p1}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder;
move-result-object v0
// 調(diào)用StringBuilder的toString方法
invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
// 把toString的結(jié)果賦值給v0
move-result-object v0
// 返回v0
return-object v0
.end method
字節(jié)碼的語法跟Java差不多,通過分析字節(jié)碼發(fā)現(xiàn),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調(diào)用native方法newStringFromChars,這個newStringFromChars方法實(shí)際上就是String內(nèi)部用來創(chuàng)建字符串的類,例如:
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 });
}
結(jié)論
所以通過以上的分析,得出結(jié)論:字符串使用“+”或StringBuilder進(jìn)行拼接,返回的是相同內(nèi)容的不同對象,而Handler.removeCallbacksAndMessages使用“==”判斷Token的內(nèi)存地址是否相等,所以導(dǎo)致移除任務(wù)失敗。