4、合成线程模型:主线程、合成线程、事件线程的职责与协作、线程间通信

好,咱们今天来聊聊 SurfaceFlinger 里最核心的“三驾马车”——主线程、合成线程、事件线程。这三个线程怎么分工、怎么吵架、怎么和好,搞明白了,你对图形系统的理解就能上一个台阶。

我记得刚接触 SurfaceFlinger 源码时,第一反应是“这玩意儿怎么这么多线程”?后来踩了几个坑才明白,这种设计不是拍脑袋想出来的,而是被逼出来的。你想想看,如果所有事情都挤在一个线程里干,那画面卡顿、掉帧就是家常便饭。

4.1 三个线程的职责划分

先给个总览,我用一张表格来概括,这样更直观:

线程名称 主要职责 核心循环
主线程 (Main Thread) 处理客户端请求、事务管理、Layer 状态更新 MessageQueue 事件循环
合成线程 (Composition Thread) 执行图形合成、调用 HWC、处理 Present 时间 CompositionEngine 驱动
事件线程 (Event Thread) 监听 Vsync 信号、处理输入事件、唤醒其他线程 EventThread::threadMain()

4.2 主线程:事务的“大管家”

主线程,说白了就是 SurfaceFlinger 的“前台经理”。所有来自客户端的请求——比如 App 要创建一个 Surface、要更新一个 Buffer、要改变窗口大小——都得经过主线程。

它的核心工作流程是这样的:

  1. 从 MessageQueue 里取出待处理的消息
  2. 解析并应用事务(Transaction)
  3. 更新 Layer 的状态(比如可见性、位置、Z-order)
  4. 触发合成流程

我个人习惯把主线程看作一个“状态机”。它不直接参与图形合成,而是负责把“世界状态”整理好,然后告诉合成线程:“兄弟,该干活了。”

关键点:主线程处理完事务后,会通过 signalLayerUpdate() 通知合成线程。这个通知是异步的,主线程不会傻等合成结束。

4.3 合成线程:真正的“画师”

合成线程才是真正干苦力活的。它负责把各个 Layer 的 Buffer 合成到一起,然后输出到屏幕。

它的核心循环大致长这样:

// 伪代码,展示合成线程的核心逻辑
void CompositionThread::threadMain() {
    while (true) {
        // 1. 等待主线程的信号
        waitForSignal();

        // 2. 获取当前帧的所有 Layer
        auto layers = fetchLayers();

        // 3. 执行合成
        auto result = mCompositionEngine->compose(layers);

        // 4. 处理 Present 时间
        handlePresentTime(result);

        // 5. 通知主线程合成完成
        notifyCompositionDone();
    }
}

嗯,这里要注意一个细节:合成线程并不是每帧都从头开始合成。它会尽量复用上一帧的结果,只合成发生变化的部分。这个优化在 Android 里叫“部分合成”(Partial Composition)。

避坑指南:我曾经遇到过一个性能问题,就是合成线程被频繁唤醒,但实际需要合成的 Layer 很少。后来发现是某个 App 每帧都在提交空事务,导致主线程不断触发合成信号。解决方案是在主线程里加了一个“去抖”逻辑——如果两次事务间隔小于 1ms,就合并处理。

4.4 事件线程:节奏的“鼓手”

事件线程,我更喜欢叫它“节拍器”。它负责监听硬件 Vsync 信号,然后把这个节奏传递给整个系统。

它的工作流程很简单:

  1. 等待 Vsync 中断
  2. 记录时间戳
  3. 唤醒主线程和合成线程
  4. 处理输入事件(如果有)

你可能会问:“为什么需要单独一个线程来做这件事?”

原因很简单:Vsync 信号是硬件产生的,频率固定(通常是 60Hz 或 120Hz)。如果把这个监听任务放在主线程里,主线程一旦被事务处理阻塞,就会错过 Vsync 信号,导致画面撕裂或卡顿。

事件线程的设计,说白了就是“专人专事”——让最紧急的事情由最专门的线程来处理。

4.5 线程间通信:消息、信号与锁

三个线程之间怎么协作?总不能各干各的吧。这里涉及三种主要的通信机制:

4.5.1 MessageQueue + Looper

主线程和事件线程之间,通过 MessageQueue 传递消息。事件线程收到 Vsync 后,会向主线程的 MessageQueue 发送一个 INVALIDATE 消息,主线程收到后开始处理事务。

// 事件线程发送消息
void EventThread::onVsyncReceived(nsecs_t timestamp) {
    // 构造消息
    auto msg = Message(Message::INVALIDATE);
    msg.setTimestamp(timestamp);

    // 发送到主线程的消息队列
    mMainThreadMessageQueue->postMessage(msg);
}

4.5.2 Condition Variable + Mutex

主线程和合成线程之间,使用条件变量来同步。主线程准备好数据后,通过 mCompositionSignal 条件变量唤醒合成线程。

// 主线程唤醒合成线程
void SurfaceFlinger::signalLayerUpdate() {
    std::lock_guard<std::mutex> lock(mCompositionMutex);
    mCompositionPending = true;
    mCompositionCondition.notify_one();
}

// 合成线程等待信号
void CompositionThread::waitForSignal() {
    std::unique_lock<std::mutex> lock(mCompositionMutex);
    mCompositionCondition.wait(lock, [this] {
        return mCompositionPending;
    });
    mCompositionPending = false;
}

注意:条件变量的使用要特别小心。我曾经遇到过死锁问题,原因是主线程在持有某个锁的情况下调用了 signalLayerUpdate(),而合成线程在等待条件变量时也试图获取同一个锁。解决方案是确保信号发送时不要持有共享锁。

4.5.3 共享内存 + 原子操作

对于一些轻量级的标志位,比如“是否有新帧需要合成”,使用原子变量就足够了。这样可以避免锁的开销。

// 使用原子变量传递状态
std::atomic<bool> g_hasNewFrame{false};

// 主线程设置标志
void SurfaceFlinger::onTransactionApplied() {
    g_hasNewFrame.store(true, std::memory_order_release);
}

// 合成线程检查标志
void CompositionThread::checkForNewFrame() {
    if (g_hasNewFrame.load(std::memory_order_acquire)) {
        // 开始合成
        g_hasNewFrame.store(false, std::memory_order_release);
    }
}

4.6 协作流程:一帧的完整生命周期

说了这么多,咱们把三个线程串起来,看看一帧画面从产生到显示,到底经历了什么:

  1. 事件线程收到 Vsync 信号,记录时间戳
  2. 事件线程主线程发送 INVALIDATE 消息
  3. 主线程处理事务,更新 Layer 状态
  4. 主线程通过条件变量唤醒合成线程
  5. 合成线程执行合成,调用 HWC
  6. 合成线程处理 Present 时间,通知主线程完成
  7. 主线程更新帧统计信息,准备下一帧

这个流程看起来简单,但实际实现中充满了各种优化和边界处理。比如,如果合成线程还没完成上一帧,主线程会不会跳过这一帧?答案是会的——这就是“掉帧”的本质原因。

核心思想:三个线程的设计,本质上是把“状态管理”、“图形合成”、“事件监听”这三个不同频率、不同优先级的任务解耦。主线程处理低频的事务(几十到几百 Hz),合成线程处理中频的合成(60-120 Hz),事件线程处理高频的 Vsync(与屏幕刷新率同步)。

4.7 总结与思考

好了,关于线程模型,我最后说几点个人体会:

  • 不要小看线程间通信的开销——每次唤醒、每次加锁都有成本。我在优化一个 120Hz 屏幕的项目时,发现仅仅是把条件变量的通知从“每帧都发”改成“有变化才发”,就省下了 2ms 的 CPU 时间。
  • 理解线程模型是理解 SurfaceFlinger 的钥匙——很多疑难杂症,比如“为什么有时候滑动不跟手”、“为什么突然掉帧”,追根溯源都能落到线程协作的问题上。
  • 多看源码,少看二手资料——我建议你直接去看 SurfaceFlinger.cpp 里的 onMessageReceived()CompositionThread.cpp 里的 threadMain(),比任何文档都管用。

下一章,我们会深入合成线程的内部,看看它到底是怎么把一堆 Buffer 拼成一幅画面的。到时候你会发现,HWC 和 GPU 之间的博弈,比你想的要精彩得多。