对Android游戏画面抖动现象的研究

【USparkle专栏】如果你深怀绝技,爱"搞点研究",乐于分享也博采众长,我们期待你的加入,让智慧的火花碰撞交织,让知识的传递生生不息!


一、前言

近期笔者一直在研究一个专题:在Android平台下,游戏以30帧运行时,即便整体性能稳定,仍普遍存在画面抖动现象。即使是知名厂商的游戏也不例外。例如笔者测试《星穹轨道》时发现:

角色跑动时留意那盆绿植会比较明显

可能是出于功耗的考虑,一进来就默认推荐了30帧,玩起来也有持续性的画面抖动,不像是偶发的卡顿,我的手机性能是较好的,不至于连30帧都跑不流畅。

通过一段时间的研究,对这个问题的原理和解决方案有了一定的见解。

二、原因分析

1. 显示机制

Android的显示机制是一个典型的生产者-消费者模型。应用端负责生产画面,通过调用SwapBuffer将内容写入BufferQueue,系统端(SurfaceFlinger进程)则从BufferQueue中取出内容,执行后续的合成与显示流程。

SurfaceFlinger取BufferQueue的节奏是根据系统Vsync-sf信号调节的,一般跟屏幕刷新率走,比如60hz那么就是16.6ms为一个周期定期触发,当游戏为30帧时,平均是33.3ms生产一个Buffer,生产的速率远远慢于消耗的速率,当Vsync-sf信号来临但BufferQueue里面为空时,系统会重复显示上一帧的内容。

理想情况下,在60Hz屏幕下运行30帧,SurfaceFlinger应该每两个周期消耗一帧内容。然而通过分析发现,实际的消耗周期并不均匀:有时一个周期消耗,有时甚至三个周期消耗。导致每帧画面在屏幕上的停留时间不一致,从而在视觉上产生抖动感。

2. 量化工具

接下来使用Perfetto来看一下这个游戏,Perfetto是Android上系统级别的Trace工具,可以使用它来观察具体的BufferQueue、Vsync情况。

Perfetto有一些配置,针对画面抖动问题需要确认开启ATrace的gfx、view模块:

下图中标注了每一次Buffer被消耗时,距离上一次的周期数:

这里筛选出了Trace数据中相关的轨道,从BufferQueue里面可以看到大多数情况是2个周期消耗一次,偶尔会有1、3个周期出现。

Perfetto生成的Trace文件可以通过SQL进行统计。笔者统计了每次BufferQueue减少时的时间点,进一步计算了这些时间间隔的标准差。该指标可用于量化画面抖动的严重程度,为后续方案对比与优化提供了数据基础。

完整SQL内容在附录中提供

三、目前的解决方案

1. Swappy方案

Swappy是Google GameSDK的一部分,这里是它的官方介绍:
developer.android.com/games/sdk/f...

*上述网址需要使用VPN打开

它是大多数游戏引擎内置的方案,以Unity引擎为例直接设置一下生效就行。

此方案核心是通过两个EGL拓展来达成它的目的:

  1. 通过setPresentationTime,为Buffer设置一个时间戳,避免此Buffer被过快消耗掉。

回到之前我们的例子,如果这里设置了一个允许被消耗的时间戳,那么这里不会存在1个周期就消耗掉的情况:

  1. 利用EGL_KHR_fence_sync,可以追踪上一帧GPU处理完毕再将当前帧Swap进去。由于实际游戏运行过程中不会是平稳地每次33.3ms Swap一次,会有波动,此策略可以避免局部生产过快导致的BufferQueue堆积。

Hook了Swappy中的一些函数,将其标注到了Perfetto中以观察其运行过程:

当然,笔者在实际使用过程中,也是遇到了不少问题:

  • 掉帧,常见于开60帧的情况,因为要等上一帧GPU结束,不同手机追踪到的GPU时间差异大,有的要10几ms,再加上Swappy是以周期为单位进行等待的,很容易延后的时间太多,最终导致帧率下降严重,所以建议60帧时不启用Swappy机制。
  • 延迟明显,一方面是渲染线程进行Swap的平均时间是要延后很多的,另一个方面是主线程是先做逻辑Update,然后通过WaitForPendingPresent等待上一帧渲染线程结束后,再继续,从Update到最终呈现的链路多了一个流程,30帧时预估延迟会增加30~40ms左右,体感明显。
  • 不稳定,不同机型效果差异很大,观察了一下发现有些机型setPresentationTime调用后,实际生效的时间并不精确,还有些机型60hz的屏幕刷新率但程序返回的值是59、58这些奇怪的值,可能会导致功能失效、锁帧之类的问题。

总的来说Swappy对画面抖动是有一定效果的,上述问题有些也好处理,只是此方案虽然开启简单,但决策前仍然要权衡、多机型实测评估。

2. 渲染线程同步方案

这是我在《王者荣耀》上发现的,他们使用了一种不同的方案来解决这个问题。

王者开30帧时的表现

此方案是通过让应用端画面生产节奏保持绝对的均匀,来间接让SurfaceFlinger消耗的节奏均匀,需要改一下引擎实现:

  • 渲染线程每次Swap调用前留一个时间戳,如果下一次Swap前的间隔时间小于33ms,则Sleep补全时间后再Swap。另外主线程WaitForTargetFPS的逻辑要去掉,依赖渲染线程控制输出节奏。
  • 还有个小细节,我通过逆向分析发现王者主线程等待上一个渲染线程结束的Sync点调整到了逻辑Update前,这样虽然降低了主线程、渲染线程的并行程度,但延迟上有优势,因为从逻辑Update到Swap中间没有WaitForPendingPresent。

这个办法简单易行,笔者实测是有效的,当然延迟还是不可避免会增加,30帧时预估延迟会增加16ms左右,仍然比Swappy少很多,算是一个平衡的方案,值得参考。

四、后续探索

很惭愧,目前对于这一问题,笔者尚未找到一个完全理想的解决方案。虽然"渲染线程同步"方案在实际测试中确实能够改善画面抖动,但仍存在不可忽视的局限:一方面,这一方案本质上是通过延长时间来达到输出节奏的均匀,导致整体延迟有所增加,另一方面,对于抖动的缓解效果其实也是有限的。

同时,Swappy库作为Google官方提供的开源方案,内部实现中有许多值得借鉴的点,它能推算出Vsync-sf信号点时间。所以笔者计划在现有"渲染线程同步"方案上进行进一步优化,结合推算出的Vsync-sf信号点时间信息来减少渲染线程Sleep的量,降低延迟,提升精准度。希望通过不断的迭代,能逐步逼近一个更加完美、兼顾效果与延迟的方案。

五、附录:Perfetto中量化统计画面抖动的SQL

vbnet 复制代码
WITH ValueChange AS (
    SELECT
        c.ts,
        t.name,
        c.value,
        LAG(c.value) OVER (PARTITION BY c.track_id ORDER BY c.ts) AS previous_value,
        process.name
    FROM
        counter AS c
    JOIN process_counter_track AS t ON c.track_id = t.id
    JOIN process ON t.upid = process.upid
    WHERE
        t.name LIKE "%SurfaceView%" and process.name LIKE "%SurfaceFlinger%"
),
ValueChange2 AS (
    SELECT
        ts,
        name,
        value
    FROM
        ValueChange
    WHERE
        value < previous_value
),
intervals AS (
    SELECT
        ts - LAG(ts) OVER (ORDER BY ts) AS interval_ns
    FROM ValueChange2
),
intervals_ms AS (
    SELECT
        interval_ns / 1000000.0 AS interval_ms  -- 将纳秒转换为毫秒
    FROM intervals
    WHERE interval_ns IS NOT NULL and interval_ns < 100000000
),
stats AS (
    SELECT
        COUNT(interval_ms) AS count_interval,
        AVG(interval_ms) AS mean_interval,
        AVG(interval_ms * interval_ms) AS mean_square_interval
    FROM intervals_ms
)
SELECT
    mean_interval AS avg_consume_interval_ms,
    SQRT(mean_square_interval - mean_interval * mean_interval) AS stddev_consume_interval_ms
FROM stats;

这是侑虎科技第1894篇文章,感谢作者其乐陶陶供稿。欢迎转发分享,未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。

作者主页:www.zhihu.com/people/jun-...

再次感谢其乐陶陶的分享,如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。

相关推荐
Kapaseker35 分钟前
Compose 进阶—巧用 GraphicsLayer
android·kotlin
黄林晴1 小时前
Android17 为什么重写 MessageQueue
android
冰_河11 小时前
QPS从300到3100:我靠一行代码让接口性能暴涨10倍,系统性能原地起飞!!
java·后端·性能优化
阿巴斯甜1 天前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker1 天前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq95271 天前
Andorid Google 登录接入文档
android
黄林晴1 天前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab2 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿2 天前
Android MediaPlayer 笔记
android
Jony_2 天前
Android 启动优化方案
android