Handler 消息回收与协程异步执行的时序陷阱

Handler 消息回收与协程异步执行的时序陷阱

众所周知当你调用 Message.obtain() 时,Message 会优先检查缓存池中有没有实例,如果有就会直接拿出现有的 Message 对象,如果缓存池中没有才会去 new Message()

java 复制代码
public static Message obtain() {
    synchronized (sPoolSync) {
        if (sPool != null) {
            Message m = sPool;
            sPool = m.next;
            m.next = null;
            m.flags = 0; // clear in-use flag
            sPoolSize--;
            return m;
        }
    }
    return new Message();
}

正常情况下,Handler 在调用 handleMessage 方法之后便会立即清除 msg 中的数据并将它丢入缓存池。具体回收流程查看源码如下:

java 复制代码
public static void loop() {
    final Looper me = myLooper();
    if (me == null) {
        throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
    }
    if (me.mInLoop) {
        Slog.w(TAG, "Loop again would have the queued messages be executed"
                + " before this one completed.");
    }

    me.mInLoop = true;

    // Make sure the identity of this thread is that of the local process,
    // and keep track of what that identity token actually is.
    Binder.clearCallingIdentity();
    final long ident = Binder.clearCallingIdentity();

    // Allow overriding a threshold with a system prop. e.g.
    // adb shell 'setprop log.looper.1000.main.slow 1 && stop && start'
    final int thresholdOverride = getThresholdOverride();

    me.mSlowDeliveryDetected = false;

    for (;;) {
        if (!loopOnce(me, ident, thresholdOverride)) {
            return;
        }
    }
}

loop() 会不断调用 loopOnce(),以下为了方便定位核心逻辑我删除了很多代码并保持了剩余代码的原结构:

java 复制代码
private static boolean loopOnce(final Looper me,
        final long ident, final int thresholdOverride) {
    Message msg = me.mQueue.next(); // might block
    if (msg == null) {
        // No message indicates that the message queue is quitting.
        return false;
    }
    // 一系列代码(性能监控,观察者模式回调...)
    try {
        msg.target.dispatchMessage(msg);
        if (observer != null) {
            // 关键代码在此处
            observer.messageDispatched(token, msg);
        }
        dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
    } catch (Exception exception) {
        if (observer != null) {
            observer.dispatchingThrewException(token, msg, exception);
        }
        throw exception;
    } finally {
        ThreadLocalWorkSource.restore(origWorkSource);
        if (traceTag != 0) {
            Trace.traceEnd(traceTag);
        }
    }

    PerfettoTrace.end(PerfettoTrace.MQ_CATEGORY).emit();
    msg.recycleUnchecked();

    return true;
}

可以看到 msg.target.dispatchMessage(msg); 这个分发事件的核心入口,无论成功或抛异常,最终都会执行到 msg.recycleUnchecked()

我们接着在 Message 中找到 target,可以发现它是一个 Handler 类型的对象:

java 复制代码
@UnsupportedAppUsage
/*package*/ Handler target;

再从 Handler 找到 dispatchMessage() 方法:

java 复制代码
public void dispatchMessage(@NonNull Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}

在这里我们终于看到了 handleMessage(msg) 也就是我们重写的那个方法。

我们接着查看 recycleUnchecked()

java 复制代码
@UnsupportedAppUsage
void recycleUnchecked() {
    // Mark the message as in use while it remains in the recycled object pool.
    // Clear out all other details.
    flags = FLAG_IN_USE;
    what = 0;
    arg1 = 0;
    arg2 = 0;
    obj = null;
    replyTo = null;
    sendingUid = UID_NONE;
    workSourceUid = UID_NONE;
    when = 0;
    target = null;
    callback = null;
    data = null;

    synchronized (sPoolSync) {
        if (sPoolSize < MAX_POOL_SIZE) {
            next = sPool;
            sPool = this;
            sPoolSize++;
        }
    }
}

其实包括 msg.recycle() 最终也是调用的 recycleUnchecked()

java 复制代码
public void recycle() {
    if (isInUse()) {
        if (gCheckRecycle) {
            throw new IllegalStateException("This message cannot be recycled because it "
                    + "is still in use.");
        }
        return;
    }
    recycleUnchecked();
}

可以清楚地看到调用 recycleUnchecked() 的一系列操作。至此整个链路全部完毕:

  1. 取出消息 : Message msg = me.mQueue.next();
  2. 开始处理 : msg.target.dispatchMessage(msg);
  3. 进入你的 handleMessage(msg)Runnable.run()
  4. 你的代码执行完毕handleMessage 返回。
  5. dispatchMessage 方法执行完毕并返回
  6. 处理后工作: 记录耗时、打印日志、恢复线程状态等。
  7. 回收消息 : msg.recycleUnchecked();
  8. 宣告完成 : return true; // 告诉 Looper "本次任务完成"

总结就是一旦 handleMessage 执行完毕,msg 便会被清洗回收加入缓存池中。

kotlin 复制代码
class HandlerTest() {

    var handlerA: Handler? = null
    var handlerB: Handler? = null

    fun start() {
        Thread {
            Looper.prepare()
            handlerA = object : Handler(Looper.myLooper()!!) {

                override fun handleMessage(msg: Message) {

                    val count = msg.data.getInt("count")
                    println("HandlerTest A:$count")

                    CoroutineScope(Dispatchers.IO).launch {
                        println("HandlerTest A Coroutine: ${msg.data.getInt("count")}")
                        delay(1000)
                        handlerB?.sendMessage(
                            Message.obtain().apply {
                                data = Bundle().apply {
                                    putInt("count", count + 1)
                                }
                            }
                        )
                    }
                }
            }
            Looper.loop()
        }.start()

        Thread {
            Looper.prepare()
            handlerB = object : Handler(Looper.myLooper()!!) {

                override fun handleMessage(msg: Message) {

                    val count = msg.data.getInt("count")
                    println("HandlerTest B:$count")

                    CoroutineScope(Dispatchers.IO).launch {
                        delay(1000)
                        handlerA?.sendMessage(
                            Message.obtain().apply {
                                data = Bundle().apply {
                                    putInt("count", count + 1)
                                }
                            }
                        )
                    }
                }
            }
            Looper.loop()
        }.start()

        Thread.sleep(500)
        handlerA?.sendMessage(
            Message.obtain().apply {
                data = Bundle().apply {
                    putInt("count", 0)
                }
            }
        )
    }
}

执行代码可以看到以下日志:

text 复制代码
I  HandlerTest B:1
I  HandlerTest A:2
I  HandlerTest A Coroutine: 0
I  HandlerTest B:3
I  HandlerTest A:4
I  HandlerTest A Coroutine: 0
I  HandlerTest B:5
I  HandlerTest A:6
I  HandlerTest A Coroutine: 0

可以看到在协程内调用 msg.data.getInt() 返回的是 0。这是因为 launch 将任务提交给协程调度器后立即返回,handleMessage 随即执行完毕。loopOnce() 紧接着调用 recycleUnchecked()msg 的所有字段清空。等到协程真正被调度到后台线程开始执行时,msg 已经被回收。

后续在协程内访问 msg.data,实际上调用的是 msg.getData(),这是 Kotlin 的语法糖。实际执行以下代码:

java 复制代码
public Bundle getData() {
    if (data == null) {
        data = new Bundle();
    }

    return data;
}

所以实际访问的是一个新的空 Bundle,接着调用 getInt() 返回默认值 0:

java 复制代码
public int getInt(String key) {
    unparcel();
    return getInt(key, 0);
}

最后,如果在该 Message 被回收后、协程执行前,它恰好被另一次 Message.obtain() 复用并写入了新数据,协程将读到完全不属于当前业务的脏数据。希望以上内容对大家理解 Handler 消息机制有所帮助。

相关推荐
恋猫de小郭2 小时前
KMP / CMP 鸿蒙版本 Beta 发布,他有什么特别之处?
android·前端·flutter
三少爷的鞋3 小时前
Android 协程并发控制:别动线程池,控制好并发语义就够了
android
weiggle19 小时前
第七篇:状态提升与单向数据流——架构设计的核心
android
xingpanvip19 小时前
星盘接口开发文档:本命盘接口指南
android·开发语言·css·php·lua
goldenrolan19 小时前
A公司物料替代测试系统 v1.7:从需求到 exe/apk 的 AI 辅助全链路实践
android·自动化测试·软件测试·python·ai
AC赳赳老秦20 小时前
用 OpenClaw 搭建服务器故障应急响应系统,自动处理 80% 常见运维故障
android·运维·服务器·python·rxjava·deepseek·openclaw
骇客之技术21 小时前
AutoLua:在安卓上写 Lua 脚本
android·junit·lua
kiros_wang1 天前
Android 常见面试题
android
货拉拉技术1 天前
Hook植入日志协助定位问题方案
android