尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Unity卡顿度量与帧率优化:从帧时间到性能基线

Unity卡顿度量与帧率优化:从帧时间到性能基线 做 Unity 项目这些年我听到最多的技术诉求大概就是“卡”。游戏卡、数字孪生卡、UI 卡、场景切换卡甚至连编辑器里挪个窗口都有人问我为什么卡。但每次我追问一句“你说的卡是帧率掉了还是操作响应慢还是加载时卡住不动”大部分人都会愣一下然后说就是……卡啊。其实“卡”这个字背后包含了太多完全不同的技术问题。在做任何优化之前先得把卡顿这件事度量清楚否则你连敌人是谁都不知道优化就成了一场玄学。这篇《Unity 卡顿·帧率保卫战》系列的第一篇我不打算聊任何具体的优化技巧而是先把最底层的工作做扎实帧率、帧时间、卡顿数据采集与剖析思路。适合刚接手 Unity 项目性能优化的同学也适合被“优化一下卡顿”这种需求反复轰炸却没有一套度量方法的技术负责人。1. 先把“卡”定义清楚帧率、帧时间与卡顿的真实关系1.1 平均帧率 60为什么还是觉得卡很多人汇报性能时只给一个平均帧率“我项目稳定 60 帧。”但用户体验并不是由平均帧率决定的。举一个我很常见到的例子在一次 10 秒测试里大部分时间每帧消耗都不到 20ms但场景切换或某个敌人出现的瞬间产生了 250ms 的帧尖刺算下来平均帧率还是有 50 多。如果只看平均值问题会被彻底掩盖玩家却会在那瞬间真实地“卡”一下。帧率FPS本质是每秒渲染多少帧它是帧时间的倒数。问题在于FPS 在玩家端是一个实时变化的瞬时值而我们汇报性能时往往算的是平均值平均值最容易抹平尖刺。A 项目帧时间恒定为 16.7msB 项目多数时候 16.7ms、但每 3 秒出现一次 100ms 尖刺大概率 B 项目用户更明显地感觉到卡。也就是说帧率平均值高并不等于流畅帧率平均值低也不代表到处都卡。这也是为什么我在做性能评审时要求团队必须提供帧时间曲线截图而不是只给一个平均 FPS。没有曲线就没有讨论卡顿的基础。1.2 帧时间与帧预算16.67ms 这道坎帧时间就是渲染一帧所花的时间。目标 60 帧每秒单帧预算 1000 / 60 ≈ 16.67ms目标 30 帧每秒预算是 33.33ms。注意这是理论峰值实际还要留给系统、驱动、垂直同步等余量所以业界常说 60 帧的目标预算按 15ms 留才安全。不同平台的帧预算很不一样。移动端很多休闲项目仍以 30 帧为目标动作游戏和高帧率手游做到 60 甚至 90 帧到了 VR 一体机设备上PICO 和 Quest 这类平台常见 72Hz、90Hz、120Hz单帧预算分别是 13.89ms、11.11ms、8.33ms。之前有个 PICO 项目需求是 90Hz 不掉帧也就意味着每个逻辑帧加渲染帧超过 11.1ms 就算一次可见掉帧优化空间比 60 帧项目紧张得多。目标帧率单帧预算实际建议预留常见场景30 FPS33.33ms30ms普通移动端 UI 应用、低配手机游戏60 FPS16.67ms15ms主流手游、桌面工具90 FPS11.11ms10msVR 一体机PICO/Quest 等120 FPS8.33ms7.5ms高刷移动端、部分桌面应用这里的预留意思是当帧时间超过预算的那一瞬间就是用户感知到的掉帧或卡顿。提高帧率的本质不是让 FPS 数字变大而是让帧时间稳定在预算线以下。1.3 P50、P95、P99用百分位数量化卡顿FPS 平均值不靠谱那用什么业界通用做法是看帧时间百分位数。把所有采集到的帧时间从小到大排序第 50 百分位叫 P50代表普通水平第 95、第 99 百分位代表最差的那 5% 和 1% 帧。例如某项目测出 P50 为 16msP95 为 32msP99 为 250ms。含义是一半帧稳定在 16ms 左右表现不错但有 1% 的帧耗时 250ms用户感知就是“顿了一下”。如果不量 P99光看 P50 会觉得项目很健康。另一个常用指标是 Jank卡顿帧频率业界常把单帧耗时超过特定阈值如 50ms的帧密度作为衡量标准比如“每 10 分钟出现 2 次 100ms 的尖刺”这比“平均 60 帧”更能反映主观流畅度。所以当我们说“把卡顿度量清楚”第一步就是把项目里帧时间统计的 P50、P95、P99 打出来最好画成曲线而不是只填一个平均 FPS 到日报里。2. 卡顿发生在哪条链路CPU、GPU 与线程拆分2.1 主线程、渲染线程和工作线程在忙什么Unity 应用运行时不是一个线程从头跑到尾。主线程负责 Update、协程、物理部分、UI 逻辑、动画回调渲染线程负责把主线程提交的渲染指令转换为 GPU 指令另外还有工作线程处理 Job System、部分物理和资源加载等。卡顿度量时首先要问是哪条线程爆了Profiler 的 CPU Usage 模块会把各线程的耗时列出来。主线程耗时高多半是逻辑、物理、GC 分配、UI 重建等渲染线程耗时高则要看 DrawCall 提交、Canvas 重建、Shader 状态切换GPU 耗时高再看填充率、后处理、实时阴影等。很多新手只看主线程的耗时忽略了渲染线程这样定位不到渲染侧的卡顿。正确做法是把每一帧里主线程、渲染线程、GPU 的耗时都记录下来才能判断瓶颈。没有线程维度你说“卡”别人只能猜。2.2 CPU 瓶颈还是 GPU 瓶颈先做一次粗判有个很简单很粗暴的判断方式在真机或编辑器中把分辨率调低一档或者把后处理关闭如果帧率大幅提升很大概率是 GPU 压力过大如果帧率几乎没变说明瓶颈在 CPU 侧因为 CPU 兜底的计算没有变。更低级的方案是直接看 Profiler 里的 GPU 模块但像青涩项目里没接 Frame Timing 的情况先做分辨率测试是最快的。更准确的判断要看计时工具。Unity 编辑器里用 Profiler 的 CPU Usage 模块看主线程耗时再配合 Frame Timing 或 GPU Usage 模块看 GPU Busy 时间。移动端还可以用厂商的工具例如高通和联发科各自的性能分析器或者直接在 Unity Profiler 里看 Rendering Profiler。GPU 卡顿在帧时间曲线上通常表现为一整段高耗时而不是单帧尖刺CPU 侧的资源加载、GC 则更多表现为单帧尖刺。这个特征可以作为定位的第一印象。2.3 热点方向阴影、UI、后处理与网络同步热词里有很多都指向具体场景。比如 Unity 阴影问题实时阴影尤其是方向光阴影每帧都要额外渲染一张深度图再按阴影距离裁剪阴影距离调太远或分辨率太高GPU 负载直接上涨帧时间曲线会呈现典型的持续偏高。UI 界面卡顿更是 Unity 项目里的老大难。一个常见原因是 Canvas 频繁重建只要 Canvas 里任何 UI 元素的尺寸、位置、文本内容发生变化整个 Canvas 往往要重新生成网格。如果界面操作时帧时间突然从 16ms 跳到 50ms第一反应就是看 Canvas 的 build 耗时。网络帧同步卡顿则是另一种“卡”帧同步逻辑里某帧在多玩家同步时等待了数据返回帧时间被拉高表现类似卡顿但根源在网络延迟和同步策略而不是渲染。所以度量的第一步不是拉开代码改而是先判断这类卡顿是渲染、逻辑还是网络导致的。3. 建立第一份性能基线Profiler 实战操作3.1 测什么场景才有参考价值性能基线的核心是“在相同条件下反复测量”。我见过太多项目拿编辑器里的空场景说“跑得多快”拿真机复杂场景说“太卡”两边不是一个量级结论自然没有意义。建基线建议选三类场景一是项目的主流程比如从启动到主城二是复杂度最高的场景通常是战斗、多人同屏、复杂 UI三是目标设备覆盖的中低配机。每个场景固定跑 2-3 分钟覆盖常规操作避开测试刚开始的 Shader 编译和资源加载窗口。记录下帧时间中位数、P95、P99以及 GC 分配量、DrawCall 数、三角形面数等关键指标。还有个小建议基线场景里的人工操作路径最好录制成固定流程例如每 10 秒开一次背包、每 30 秒发动一次技能。手点虽然有随机性但只要大致固定就已经比随机乱点强很多。3.2 编辑器 Profiler 的正确打开方式Unity 编辑器里 Window Analysis Profiler 是老牌入口。打开后默认是 CPU Usage 模块用鼠标框选帧时间曲线里的尖刺区域就能看到每一帧内部各模块的耗时。但编辑器 Profiler 有几个天然不准的地方编辑器渲染走的是桌面显卡驱动和真机 GPU 差别很大Shader 编译方式不同AssetBundle 加载路径可能不完整。所以编辑器数据只能做相对分析不能直接代表真机。另一个要注意的是 Deep Profile 选项。它会把每个函数的调用都插桩信息很全但性能开销极大实测可能让帧时间翻几倍。用它定位调用栈可以但绝对不要开着它去采集性能基线否则你会拿一把放大镜当体温计用。3.3 真机 ProfilingAndroid/iOS/PICO 的接法真机采集的意义在于拿到真实硬件上的帧时间。以 Android 为例手机开启开发者选项和 USB 调试连接电脑。Unity Build Settings 里勾选 Development Build、Autoconnect Profiler。构建安装包并运行在真机上。打开 Unity Profiler等待自动连接设备。跑测试流程 2-3 分钟保存 Profiler 数据。iOS 通过 Xcode 或 Unity 的 Player Connection 连接。PICO/Quest 这类一体机也支持无线连接但我建议优先用 USB 或有线网络无线 Profiling 本身会占用通信带宽对帧时间数据有一定干扰。要注意的是Development Build Profiler 跑出来的数据会略高于正式发布包因为 Profiler 本身有开销。这套方式适合定位问题、对比相对变化如果要验证最终线上性能必须发布包再单独采集一次 FPS 和帧时间日志。3.4 记录并保存“卡顿档案”度量结果要存档不然没法做前后对比。Profiler 窗口右上角可以把当前数据保存为 Profiler 文件文件名建议带上项目名称、平台、场景、日期。同样建议把帧时间统计的 P50、P95、P99 写进自动化脚本或表格后续每次优化前后都跑同一套流程对比同一组指标。我在实操里还有一个习惯每一轮优化只改一个变量录一次基线严禁同时改 5 个点然后看整体效果因为那样根本不知道是哪一步产生了收益。这也是很多团队优化做了半天最后无法验证结论的原因。4. 把卡顿变成可量化指标帧时间采样与交叉验证4.1 用 ProfilerRecorder 自己采集帧时间从 Unity 2019.3 开始官方提供了 ProfilerRecorder API可以在运行时以低开销采集 Profiler 计数器的采样值。这比在 Update 里自己用 Time.deltaTime 求均值更规范也不需要额外安装工具。using UnityEngine; using Unity.Profiling; public sealed class FrameStats : MonoBehaviour { private ProfilerRecorder frameTimeRecorder; private ProfilerRecorder mainThreadRecorder; private void OnEnable() { frameTimeRecorder ProfilerRecorder.StartNew(ProfilerCategory.Internal, Frame Time); mainThreadRecorder ProfilerRecorder.StartNew(ProfilerCategory.Internal, Main Thread); } private void Update() { if (frameTimeRecorder.Valid frameTimeRecorder.Count 0) { float frameMs frameTimeRecorder.LastValue * 1e-6f; Debug.Log($[Stats] FrameTime{frameMs:F2}ms); } } private void OnDisable() { frameTimeRecorder.Dispose(); mainThreadRecorder.Dispose(); } }这段代码里ProfilerRecorder.LastValue 的单位是纳秒所以乘 1e-6 转成毫秒。只要把它挂到一个长期存在的 GameObject 上就能持续输出帧时间。生产环境里更推荐把采样值每 60 帧汇总一次算平均、P95、P99然后走日志或埋点上报而不是每帧打一条 Debug.Log否则 Debug.Log 本身就会成为性能瓶颈。4.2 从帧时间尖刺反推可疑模块拿到帧时间曲线后优化思路就从“感觉卡”变成了“这一帧 80ms去查这一帧里到底发生了什么”。查的方法是把帧时间曲线上的尖刺帧在 Profiler 里逐帧展开看主线程、渲染线程、GPU 各占多少。常见的尖刺来源包括GC 分配触发的垃圾回收Update 里 Instantiate 生成对象并同步触发资源加载Shader 首个变体编译AssetBundle 首次加载Canvas 重建后端低频逻辑集中执行等。逐个把这些模块的耗时跟帧时间尖刺对齐就能确定优先优化的对象。举个实际例子在 UI 卡顿的场景里看到 Profiler 中 Canvas.SendWillRenderCanvases 单帧占了 35ms再结合 Frame Debugger 看 Canvas 包含几个界面问题就很清楚了。这时候你知道卡顿是 UI 网格重建导致的而不是去优化技能特效或阴影距离。4.3 Frame Debugger Memory Profiler 交叉验证Profiler 告诉你时间花在哪儿Frame Debugger 告诉你渲染状态长什么样。针对某一帧打开 Frame Debugger可以看到完整的 DrawCall 列表任意选中一个 DrawCall还能查看画面网格、材质、贴图及各项渲染状态。如果怀疑是渲染卡顿比起瞎猜直接在这里排查 DrawCall 数和重复网格。Memory Profiler 则是用来查资源层面的问题。很多卡顿不是计算量太大而是内存抖动一个三秒循环里反复 Instantiate 又重新加载图集或 Prefab带来大量的 GC 和资源 IO。用 Memory Profiler 抓快照对比两个相近瞬间托管堆的分配能快速找出哪些对象在反复“出生-死亡”。交叉验证的意思是说一个帧时间尖刺背后往往是多个线索帧时间定位用了多少 msFrame Debugger 看是不是 DrawCall 突然增多Memory Profiler 看是不是 GC 或资源分配问题三个工具互相对齐定位才可靠不会因为看见一个可疑点就冲上去改。5. 性能度量防坑指南与排查实录5.1 编辑器数据不等于真机数据编辑器里跑得飞快不代表真机没问题。原因包括但不限于编辑器使用主机显卡驱动真机 GPU 的浮点性能、带宽差异极大编辑器不会真实模拟移动端的内存带宽和功耗压力某些优化比如动态骨骼、LOD 切换在编辑器里表现不明显到了真机才看得出差别。所以我的结论一直很明确做性能决策以真机为准编辑器负责定位调用栈。用编辑器 Profiler 找逻辑调用栈是可以的但要确认最终效果必须回到目标设备上重新采集基线。5.2 那些让数据“失真”的设置一个是垂直同步VSync。开着垂直同步帧时间会被强制对齐到屏幕刷新率的整数倍如果本来就达不到 60 帧实际表现可能直接掉到 30 帧曲线会变得非常诡异。采集数据前先明确是否开 VSync尽量所有测试保持一致。另一个是手机发热降频。连续跑二十分钟手机上电池温度上来后CPU/GPU 会主动降频帧率曲线持续下滑。这不是代码变差了是散热变差了。所以做真机对比时同一台设备、同一环境温度、每次跑相同时长甚至每轮测试之间让设备静置降温都是必要流程。还有一个容易被忽略的点开发版和发布版的逻辑条件可能不同比如开发版里开着日志、热更新调试、Profiler 记录都会额外增加耗时。度量基线时必须注明是什么版本测得避免拿开发版数据和发布版数据直接比较。5.3 常见卡顿现象、优先怀疑对象与排查动作速查表最后分享一张我会贴在工位上的速查表。适合启动项目性能排查时按图索骥但请记住它是“优先怀疑”不是“最终结论”。常见现象优先怀疑对象最先看的度量点UI 操作时明显卡顿Canvas 重建、UI 网格更新、OverdrawProfiler 中 Canvas.SendWillRenderCanvases、UI 模块耗时同屏物体多时突然掉帧阴影距离/实时阴影、粒子特效、动态光源GPU 耗时、Rendering 模块、Frame Debugger 的 DrawCall场景切换卡顿、加载卡顿Shader 编译、AssetBundle IO、GC 释放尖刺帧的主线程耗时、异步加载进度、Memory Profiler网络同步引起“卡帧”帧同步等待、RTT 抖动、包体过大网络延迟曲线、逻辑帧耗时确认不是渲染层问题WebGL 持久化写入时卡顿浏览器 IndexedDB 写入、同步刷新逻辑主线程帧时间、异步写入回调区分是否为 IO 阻塞低端机发热后越来越卡降频、功耗控制、没有帧率上限连续运行帧率曲线、设备温度、是否开启自适应帧率策略提示所有“优先怀疑对象”都需要在实际项目中验证。性能问题的反直觉之处在于有时候看起来是 UI 的卡顿真正元凶是那一帧触发了 GC 分配的粒子系统看起来是 GPU 压力大实际可能是主线程把渲染指令提交得太慢。度量工具交叉使用结论才经得起推敲。写到这里第一部分的“度量清楚”就说得差不多了。我个人在项目里坚持的流程是先测基线再改代码改完再测同一套指标只改一个变量。踩过太多次“感觉顺畅了”的坑后来发现 P95、P99 一点没动只是自己心理上觉得好了。所以现在只信帧时间曲线和 P99 数字。《Unity 卡顿·帧率保卫战》第一篇先讲度量。下一篇我们开始进入真正的战斗帧时间尖刺出现后如何逐层缩小范围定位到具体的函数、资源或渲染指令。在那之前建议先把手头项目的帧时间基线建起来把“卡”这个模糊的感受变成一组可以讨论、可以对比、可以追踪的数字。
返回列表