Android 17 重磅重构!服役 20 年的 MessageQueue 迎来无锁改造,卡顿大幅优化!

在 Android 17 中,以 SDK 37 或更高版本为目标平台的应用将收到 MessageQueue 的新实现,该实现是无锁的。新实现可提高性能并减少丢帧,但可能会破坏反映 MessageQueue 私有字段和方法的客户端。

0. Introduction

当我留意到 Android 17 将针对 MessageQueue 进行改动的时候,着实让我惊讶了番:这个 Android 系统核心的 MessageQueue 在 Handler 机制里运行了 20 多年,看起来一直运行稳定、没有明显缺陷。为何 Google 要大费周章改动它?怎么改动的?有何影响?

本篇文章将带你深入理解个中原因。

1. What and Why?

自 Android 操作系统首次发布以来,MessageQueue 一直依赖于单个锁(即 synchronized 代码块)来管理主线程的任务队列。

查看 MessageQueue 源码,你会发现,无论是调度 message、入队、删除 message、清空等方法,都首先需要该线程获取队列的锁才能继续下一步。

java 复制代码
public final class MessageQueue {
    ...
    @UnsupportedAppUsage
    Message mMessages;
    ...
    Message next() {
        ...
        for (;;) {
            ...
            synchronized (this) {
                // Try to retrieve the next message.  Return if found.
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                if (msg != null && msg.target == null) {
                    // Stalled by a barrier.  Find the next asynchronous message in the queue.
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                ...
            }
            ...
        }
    }
​
    void quit(boolean safe) {
        ...
        synchronized (this) {
            ...
            // We can assume mPtr != 0 because mQuitting was previously false.
            nativeWake(mPtr);
        }
    }
​
​
    boolean enqueueMessage(Message msg, long when) {
        ...
        synchronized (this) {
            if (msg.isInUse()) {
                throw new IllegalStateException(msg + " This message is already in use.");
            }
            ...
        }
        return true;
    }
    ...
​
    void removeMessages(Handler h, Runnable r, Object object) {
        ...
        synchronized (this) {
            Message p = mMessages;
            ...
        }
    }
    ...
}

这种设计易引发锁竞争;主线程可能会被后台线程阻塞,从而导致丢帧和界面卡顿。

我们来看一段切实的 case:用户在 Pixel 手机上使用 Camera app 拍照后、立即切换到 Launcher,这时候却遇到了卡顿。下面是 Perfetto 的屏幕截图,其中显示了导致丢帧的事件:

可以通过以下三点逐步拆解原因:

  • behavior:Launcher 主线程被阻塞了 18ms,超过了通过 60Hz 刷新率所需的 16ms 上限,因此出现了界面卡顿与丢帧
  • analysis :Launcher 主线程被 MessageQueue 的锁阻塞,而名为 "BackgroundExecutor" 的线程在持有这个锁
  • root cause :Launcher 的 BackgroundExecutor 的优先级并不高,执行的是一项检查应用使用时限的非紧急任务。可是,Camera 进程的中优先级线程正在抢占 CPU 时间来处理来自摄像头的数据,这导致 BackgroundExecutor 线程暂时挂起无法完成本来要做的工作。

总结来讲,Camera 进程抢到了 CPU 资源让 Launcher 进程的后台线程得不到处理、无法及时释放锁,这间接导致了优先级更高的 Launcher 主线程被阻塞着、造成了卡顿。而这一切都和 MessageQueue 的锁机制脱不了干系!

2. What is DeliQueue and how does DeliQueue work?

基本思路是摒弃全局的传统锁来保护消息队列,取而代之的是设计了一种名为 DeliQueue 的新型数据结构,其采用 Varhandle + CAS 机制,实现了无锁架构。通过原子化的 wait-state(mWaitState)、序列号(sNextInsertSeq / sNextFrontInsertSeq)和特定的消息堆(Treiber stack)实现并发入队/出队逻辑。

代码地址:

新版本 MessageQueue 的构造函数在原本的基础上新增了 Thread、name、Tid 的记录,主要目的是为了 perfetto 调试方便。更重要的是加入了原子化的 mWaitState 进行 message 操作的核心依据。

ini 复制代码
public final class MessageQueue {
    ...
    MessageQueue(boolean quitAllowed) {
        mQuitAllowed = quitAllowed;
        mPtr = nativeInit();
        mLooperThread = Thread.currentThread();
        mThreadName = mLooperThread.getName();
        mTid = Process.myTid();
        long now = SystemClock.uptimeMillis();
        mWaitState = WaitState.composeDeadline(now + INDEFINITE_TIMEOUT_MS, false);
    }
    ...
}

以最重要的 next() 为例。

ini 复制代码
public final class MessageQueue {
    ...
​
    Message next() {
        final long ptr = mPtr;
        if (ptr == 0) {
            return null;
        }
​
        mNextPollTimeoutMillis = 0;
        int pendingIdleHandlerCount = -1; // -1 only during first iteration
        while (true) {
            if (mNextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }
​
            nativePollOnce(ptr, mNextPollTimeoutMillis);
​
            Message msg = nextMessage(false, false);
            if (msg != null) {
                msg.markInUse();
                decAndTraceMessageCount();
                return msg;
            }
​
            // Prevent any race between quit()/nativeWake(), dispose() and other users of mPtr
            if (mWorkerShouldQuit) {
                setMptrTeardownAndWaitForRefsToDrop();
                dispose();
                return null;
            }
​
            synchronized (mIdleHandlersLock) {
                if (pendingIdleHandlerCount < 0
                        && isIdle()) {
                    pendingIdleHandlerCount = mIdleHandlers.size();
                }
                if (pendingIdleHandlerCount <= 0) {
                    // No idle handlers to run.  Loop and wait some more.
                    continue;
                }
​
                if (mPendingIdleHandlers == null) {
                    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
                }
                mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
            }
​
            for (int i = 0; i < pendingIdleHandlerCount; i++) {
                final IdleHandler idler = mPendingIdleHandlers[i];
                mPendingIdleHandlers[i] = null; // release the reference to the handler
​
                boolean keep = false;
                try {
                    keep = idler.queueIdle();
                } catch (Throwable t) {
                    Log.wtf(TAG, "IdleHandler threw exception", t);
                }
​
                if (!keep) {
                    synchronized (mIdleHandlersLock) {
                        mIdleHandlers.remove(idler);
                    }
                }
            }
​
            pendingIdleHandlerCount = 0;
            mNextPollTimeoutMillis = 0;
        }
    }
​
    Message nextMessage(boolean peek, boolean returnEarliest) {
        while (true) {
            long oldWaitState = mWaitState;
            final long zeroCounter = WaitState.initCounter();
            while (!WaitState.isCounter(oldWaitState)) {
                if (sWaitState.compareAndSet(this, oldWaitState, zeroCounter)) {
                    oldWaitState = zeroCounter;
                    break;
                }
                oldWaitState = mWaitState;
            }
​
            mStack.heapSweep();
            mStack.drainFreelist();
​
            ...
            return null;
        }
    }
    ...
}

新版本的实现不再去持有当前 MessageQueue 的锁,而是直接通过 mWaitState 比较去操作存放 message 的堆。

我们都知道原始的 MessageQueue 代码使用单向链表 来实现有序队列。插入操作会按排序顺序遍历列表,进行线性搜索,在第一个超出插入点的元素处停止,并将新的 Message 链接到该元素之前。从头部移除只需解除头部关联即可。而 DeliQueue 使用的是最小堆,其中突变需要以对数复杂度在平衡的数据结构中重新排序一些元素(向上或向下过滤),其中任何比较都有相同的机会将遍历定向到左子节点或右子节点。

这个版本的 MessageQueue 改动巨大,在优化线程调度的同时还优化了消息数据结构,并在对同步屏障 sync barrier 特性的支持上也做了不少适配。

感兴趣的开发者可以查看源码,研究一番!

3. What's the impact?

对性能的正面影响

新引入的 DeliQueue 通过消除 MessageQueue 中的锁来提升系统和应用性能。据 Google 官方表示,

  • 由于改进了并发性(Treiber 堆栈)和插入速度(最小堆),多线程插入到繁忙队列的速度比旧版 MessageQueue 快了多达 5,000 倍

  • 从内部 Beta 版测试人员处获得的 Perfetto 轨迹中,发现 app 主线程在锁竞争中花费的时间减少了 15%

  • 在相同的测试设备上,还显著改善了用户体验,例如:

    • 应用中错过的帧数减少了 4%。
    • 系统界面和启动器互动中的丢帧数减少了 7.7%。
    • 从应用启动到绘制第一帧的时间(第 95 百分位)缩短了 9.1%。

对现有逻辑的影响

避免通过反射读取内部字段

新的实现会破坏此前通过反射读取 MessageQueue 私有字段和方法的逻辑。

kotlin 复制代码
public final class MessageQueue {
    ...
    @UnsupportedAppUsage
    Message mMessages;
    ...
}

具体来讲,如果 app 或间接依赖会在运行时通过反射机制来读取 MessageQueue 的内部状态,则可能会受到此更改的影响。比如会有访问 MessageQueue.mMessages 等私有字段来检查待处理的消息的既有逻辑。

可是在新的无锁实现中,这些内部数据结构已完全更改。尽管 AOSP 为了保持二进制的兼容性,在Android 17 中保留了 mMessages 字段,但在新实现中,无论队列中是否有消息,此字段将始终为 null

PS. 笔者在最新的 AOSP 代码中并未看到 mMessages 的定义,不知道官方说的保留何意。。。

对测试库的影响

另外,如果 app 中使用了一些常用的测试库,则需要更新这些库,使其与新的 MessageQueue 实现兼容。

Espresso

开发者一般会使用 Espresso 用于界面测试。早期版本的 Espresso 会依赖反射技术去获悉主线程何时处于空闲状态,以正确断言界面的状态。而这与无锁 MessageQueue 并不兼容。所以需要更新到 Espresso 3.7.0 或更高版本。在这些版本中,将替代为 Android 16 引入的新 API TestLooperManager。这样便可以在不依赖 MessageQueue 内部实现细节的情况下更安全、解耦地与 Looper 交互。

Robolectric

相似的还有 Robolectric,这种单元测试框架依赖于旧版 Looper 模式,在 Android 17 下同样可能出现问题。开发者可以更新到 Robolectric 4.17 或更高版本。除此之外,如果 app 中给 @LooperMode() 指定的是 LEGACY,则需要指定为 PAUSED 避免时序错乱。如需了解详情,请参阅 Robolectric 的迁移指南

提前测试一下

可以提前在 Android 17 上测试该行为变更对既存 app 的影响,不需要更新 targetSDK,只需执行以下命令:

bash 复制代码
adb am compat enable USE_NEW_MESSAGEQUEUE <your-package-name>

只要 app 是 debug 模式,此命令会针对 app 预先启用无锁实现的 MessageQueue 机制。

当然,如果 app 以 Android 17 为目标平台,则 MessageQueue 将默认启用新行为。如果发生了 crash 或者 exception,可以利用如下两种办法恢复到旧的锁实现行为,以此来快速验证是否和该行为变更有关。

  1. UI 方式:在开发者选项中的"应用兼容性变更"中找到"USE_NEW_MESSAGEQUEUE",将开关关闭

  2. 命令方式:运行以下 ADB 命令

    bash 复制代码
    adb am compat disable USE_NEW_MESSAGEQUEUE <your-package-name>

4. References

相关推荐
星辰徐哥6 小时前
AI性能优化:数据预处理加速
人工智能·性能优化
yuhuofei20216 小时前
【Python入门】Python中字符串相关拓展
android·java·python
dalancon6 小时前
Android Input Spy Window
android
dalancon8 小时前
InputDispatcher派发事件,查找目标窗口
android
我命由我123458 小时前
Android Framework P3 - MediaServer 进程、认识 ServiceManager 进程
android·c语言·开发语言·c++·visualstudio·visual studio·android runtime
天才少年曾牛9 小时前
Android14 新增系统服务后,应用调用出现 “hidden api” 警告的原因与解决方案
android·frameworks
赏金术士9 小时前
Jetpack Compose 底部导航实战教程(完整版)
android·kotlin·compose
随遇丿而安9 小时前
第5周:XML 资源、样式和主题,真正解决的是“页面以后还改不改得动”
android
zh_xuan10 小时前
Android 获取系统内存页大小:sysconf(_SC_PAGESIZE) 与 JNI 实现
android·jni·ndk·内存页大小