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来了,通知应用开始干活

流程是这样的:

  1. 应用调用Choreographer.postFrameCallback(),注册一个回调。
  2. Choreographer内部通过FrameDisplayEventReceiver,向SurfaceFlinger请求VSync信号。
  3. VSync到来时,SurfaceFlinger把信号发回给Choreographer。
  4. 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它的目标帧率。

合成请求的发起,总结下来就三步:

  1. VSync信号到达:硬件或软件产生VSync。
  2. 消息入队:SurfaceFlinger的MessageQueue收到INVALIDATE消息。
  3. 条件判断:检查是否有待合成的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可以查看每一帧的耗时分布。重点关注DrawPrepareProcessExecute四个阶段。如果Process阶段耗时异常,那多半是SurfaceFlinger的合成出了问题。

好了,这一章的内容就到这里。下一章我们会深入doComposition()的内部,看看SurfaceFlinger到底是怎么把几十个Layer合到一起的。到时候我会分享一个我在三星设备上遇到的HWC兼容性bug,很有意思。