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、要改变窗口大小——都得经过主线程。
它的核心工作流程是这样的:
- 从 MessageQueue 里取出待处理的消息
- 解析并应用事务(Transaction)
- 更新 Layer 的状态(比如可见性、位置、Z-order)
- 触发合成流程
我个人习惯把主线程看作一个“状态机”。它不直接参与图形合成,而是负责把“世界状态”整理好,然后告诉合成线程:“兄弟,该干活了。”
关键点:主线程处理完事务后,会通过 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 信号,然后把这个节奏传递给整个系统。
它的工作流程很简单:
- 等待 Vsync 中断
- 记录时间戳
- 唤醒主线程和合成线程
- 处理输入事件(如果有)
你可能会问:“为什么需要单独一个线程来做这件事?”
原因很简单: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 协作流程:一帧的完整生命周期
说了这么多,咱们把三个线程串起来,看看一帧画面从产生到显示,到底经历了什么:
- 事件线程收到 Vsync 信号,记录时间戳
- 事件线程向主线程发送 INVALIDATE 消息
- 主线程处理事务,更新 Layer 状态
- 主线程通过条件变量唤醒合成线程
- 合成线程执行合成,调用 HWC
- 合成线程处理 Present 时间,通知主线程完成
- 主线程更新帧统计信息,准备下一帧
这个流程看起来简单,但实际实现中充满了各种优化和边界处理。比如,如果合成线程还没完成上一帧,主线程会不会跳过这一帧?答案是会的——这就是“掉帧”的本质原因。
核心思想:三个线程的设计,本质上是把“状态管理”、“图形合成”、“事件监听”这三个不同频率、不同优先级的任务解耦。主线程处理低频的事务(几十到几百 Hz),合成线程处理中频的合成(60-120 Hz),事件线程处理高频的 Vsync(与屏幕刷新率同步)。
4.7 总结与思考
好了,关于线程模型,我最后说几点个人体会:
- 不要小看线程间通信的开销——每次唤醒、每次加锁都有成本。我在优化一个 120Hz 屏幕的项目时,发现仅仅是把条件变量的通知从“每帧都发”改成“有变化才发”,就省下了 2ms 的 CPU 时间。
- 理解线程模型是理解 SurfaceFlinger 的钥匙——很多疑难杂症,比如“为什么有时候滑动不跟手”、“为什么突然掉帧”,追根溯源都能落到线程协作的问题上。
- 多看源码,少看二手资料——我建议你直接去看
SurfaceFlinger.cpp里的onMessageReceived()和CompositionThread.cpp里的threadMain(),比任何文档都管用。
下一章,我们会深入合成线程的内部,看看它到底是怎么把一堆 Buffer 拼成一幅画面的。到时候你会发现,HWC 和 GPU 之间的博弈,比你想的要精彩得多。