3、合成触发机制:VSync信号、Choreographer与SurfaceFlinger的同步、合成请求的发起
好,咱们今天聊一个核心问题——SurfaceFlinger到底什么时候开始干活?
说白了,就是谁告诉它“该合成了”?这个触发机制,是整个图形系统的脉搏。我当年刚接触这部分源码时,总觉得VSync就是个简单的定时器,后来踩了坑才发现,事情远没那么简单。
3.1 VSync信号:整个系统的“心跳”
VSync,全称Vertical Synchronization(垂直同步)。它不是一个软件概念,而是硬件产生的。
你的屏幕刷新,是一行一行扫过去的。扫到最后一行,电子枪要回到左上角重新开始。这个“回扫”的瞬间,硬件会发出一个脉冲信号——这就是VSync。
嗯,这里要注意:VSync信号是硬件中断,不是软件轮询。Android系统通过/dev/display或/dev/graphics/fb0这类设备节点,注册一个监听器来捕获它。
核心要点:VSync的频率等于屏幕刷新率。60Hz的屏幕,每16.6ms来一次。120Hz的屏幕,每8.3ms来一次。
我在项目中遇到过一个问题:某款低端平板,屏幕刷新率标称60Hz,但实际VSync信号不稳定,偶尔会跳变到55Hz。结果就是UI掉帧、卡顿。后来查了硬件驱动,发现是时钟源精度不够。所以你看,VSync的稳定性直接影响用户体验。
3.2 Choreographer:应用的“节拍器”
应用层怎么知道什么时候该画下一帧?靠Choreographer。
Choreographer是Android Framework层的一个类,位置在android.view.Choreographer。它的作用很简单:等VSync来了,通知应用开始干活。
流程是这样的:
- 应用调用
Choreographer.postFrameCallback(),注册一个回调。 - Choreographer内部通过
FrameDisplayEventReceiver,向SurfaceFlinger请求VSync信号。 - VSync到来时,SurfaceFlinger把信号发回给Choreographer。
- Choreographer按优先级依次执行所有注册的回调。
你想想看,这就像乐队指挥。Choreographer就是那个指挥棒,它一挥,所有乐手(应用的各种UI线程)同时开始演奏。
避坑指南:我曾经调试过一个bug,某个第三方应用频繁掉帧。抓trace发现,它的Choreographer回调里做了大量文件IO操作。记住,Choreographer回调里绝对不能做耗时操作,否则会阻塞下一帧的绘制。我曾经见过有人在里面写数据库查询,那画面太美我不敢看。
3.3 SurfaceFlinger的同步:从VSync到合成
好,现在VSync来了,应用也开始画了。那SurfaceFlinger自己呢?它什么时候开始合成?
答案是:它也在等VSync。但这里有个关键区别——SurfaceFlinger用的是软件VSync,不是硬件VSync。
为什么?因为硬件VSync太“硬”了。如果SurfaceFlinger直接响应硬件VSync,那它必须在一个VSync周期内完成所有合成工作。万一某个Buffer还没准备好呢?那就得等下一个周期,造成掉帧。
所以Android设计了一套VSync偏移机制:
| VSync类型 | 用途 | 偏移量 |
|---|---|---|
| 硬件VSync | 屏幕刷新 | 0 |
| 应用VSync | 触发应用绘制 | 通常为0 |
| 合成VSync | 触发SurfaceFlinger合成 | 通常为几毫秒 |
合成VSync比应用VSync晚几毫秒。这样做的目的是:给应用留出绘制时间。应用在应用VSync时开始画,画完Buffer入队。SurfaceFlinger在合成VSync时开始合成,此时Buffer大概率已经准备好了。
我个人习惯把这个偏移量叫做“缓冲窗口”。太小了,应用来不及画完;太大了,会增加延迟。我记得在Android 8.0上,默认偏移是2ms。到了Android 12,有些设备改成了动态调整。
3.4 合成请求的发起:谁调用了doComposition?
好,现在到了最关键的环节——合成请求到底是怎么发起的?
SurfaceFlinger的主循环在SurfaceFlinger::onMessageReceived()里。当合成VSync到来时,会触发一个消息:MessageQueue::INVALIDATE。
看代码:
// frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp
void SurfaceFlinger::onMessageReceived(int32_t what) {
switch (what) {
case MessageQueue::INVALIDATE: {
// 处理无效化请求
handleMessageInvalidate();
break;
}
case MessageQueue::REFRESH: {
// 处理刷新请求
handleMessageRefresh();
break;
}
}
}
handleMessageInvalidate()会检查所有Layer的状态,看看有没有新的Buffer需要合成。如果有,就调用handleMessageRefresh(),最终走到doComposition()。
但这里有个细节:不是每次VSync都会触发合成。如果没有任何Layer更新,SurfaceFlinger会跳过这次合成,直接等待下一个VSync。这叫“空闲优化”,省电。
注意:我曾经遇到一个case,某个游戏在低帧率模式下,SurfaceFlinger每两帧才合成一次。结果就是画面看起来一卡一卡的。后来发现是游戏的Buffer提交频率和VSync没对齐。解决方案是让游戏使用setFrameRate()接口,明确告知SurfaceFlinger它的目标帧率。
合成请求的发起,总结下来就三步:
- VSync信号到达:硬件或软件产生VSync。
- 消息入队:SurfaceFlinger的MessageQueue收到INVALIDATE消息。
- 条件判断:检查是否有待合成的Buffer。有则合成,无则跳过。
嗯,这里还要提一个东西——主动请求合成。有时候应用等不及下一个VSync,比如用户滑动屏幕时,希望立即响应。这时候应用可以调用Surface.lockHardwareCanvas()或者View.invalidate(),这些API会触发一个“异步请求”,让SurfaceFlinger提前合成。
说白了,就是“别等了,现在就干”。但这样会打乱VSync的节奏,所以Android对主动请求做了限制——每个VSync周期内最多触发一次主动合成。
3.5 整个流程串起来
好,咱们把整个触发机制串起来看一遍:
- 硬件VSync:屏幕每16.6ms发出一次信号。
- 应用VSync:Choreographer收到信号,通知应用开始绘制。
- 应用绘制:UI线程执行onDraw,渲染到Buffer里。
- Buffer入队:应用通过
eglSwapBuffers把Buffer交给SurfaceFlinger。 - 合成VSync:几毫秒后,SurfaceFlinger收到信号,开始合成。
- doComposition:SurfaceFlinger把所有可见Layer合成到一块Buffer里。
- 送显:合成后的Buffer通过HWC或GPU提交给屏幕。
你想想看,这一整套流程,环环相扣。任何一个环节慢了,都会导致掉帧。我调试过最头疼的一个问题,就是Choreographer回调被GC阻塞了,导致应用VSync延迟了3ms,结果整个合成链都往后推,最终掉帧。
调试技巧:用adb shell dumpsys gfxinfo可以查看每一帧的耗时分布。重点关注Draw、Prepare、Process、Execute四个阶段。如果Process阶段耗时异常,那多半是SurfaceFlinger的合成出了问题。
好了,这一章的内容就到这里。下一章我们会深入doComposition()的内部,看看SurfaceFlinger到底是怎么把几十个Layer合到一起的。到时候我会分享一个我在三星设备上遇到的HWC兼容性bug,很有意思。