4、CPU瓶颈:脚本逻辑过重、物理引擎开销、GC垃圾回收导致的卡顿
说到CPU瓶颈,很多人的第一反应是「渲染压力大」。其实不然。真正让动画掉帧的,往往是那些看不见的CPU开销。我见过太多项目,GPU利用率才40%,动画却卡得像幻灯片。查到最后,全是CPU在拖后腿。
CPU瓶颈主要有三个来源:脚本逻辑、物理引擎、GC垃圾回收。咱们一个一个拆开看。
4.1 脚本逻辑过重:每帧都在做无用功
脚本逻辑过重,说白了就是每帧的计算量太大了。你想想看,60fps意味着每帧只有16.6ms。如果脚本逻辑占了10ms,留给渲染和物理的时间就只剩6.6ms。一旦超了,掉帧是必然的。
我遇到过最典型的例子:一个RPG项目,角色移动时每帧都要遍历所有敌人做距离检测。场景里200个敌人,每帧做200次Vector3.Distance。结果呢?移动时帧率直接掉到30fps。
常见的脚本性能杀手有哪些?
- Update里做高频计算——每帧都执行,能放协程就别放Update
- 频繁的GetComponent/Find——这些API有反射开销,缓存引用才是正道
- 大量对象遍历——尤其是每帧遍历List或数组,O(n)复杂度累积起来很可怕
- 字符串拼接——每帧拼接字符串会产生大量临时对象,触发GC
核心原则:每帧只做必须做的事。能缓存就缓存,能延迟就延迟,能合并就合并。
我个人习惯在写脚本前先画一个「每帧执行路径图」。哪些逻辑必须每帧跑?哪些可以隔帧跑?哪些可以异步跑?画清楚了再动手,能省掉后期80%的性能优化工作。
4.2 物理引擎开销:碰撞检测不是免费的
物理引擎的开销,很多人低估了。尤其是移动端的物理引擎,远没有PC上那么高效。
物理引擎的瓶颈主要来自三个方面:
| 瓶颈类型 | 原因 | 典型表现 |
|---|---|---|
| 碰撞检测 | 每帧检测所有碰撞体对 | 物体越多,开销呈指数增长 |
| 刚体模拟 | 每帧求解物理方程 | 大量刚体同时运动时帧率骤降 |
| 触发器回调 | OnTriggerEnter/Stay频繁触发 | 回调里做复杂逻辑时卡顿 |
我曾经接手过一个跑酷项目,场景里铺了上百个BoxCollider做地面检测。每帧物理引擎要检测几千对碰撞体,CPU时间全耗在物理上了。后来我把地面改成一个MeshCollider,配合射线检测,物理开销直接降了70%。
避坑指南:我曾经在项目里用大量CharacterController做AI角色移动,结果每帧物理同步开销巨大。后来换成NavMeshAgent + 手动位置同步,帧率从25fps提升到55fps。
物理引擎优化的几个实用技巧:
- 减少碰撞体数量——能用BoxCollider就别用MeshCollider,能合并就别拆分
- 使用碰撞矩阵——只检测必要的层级,不相关的层级直接忽略
- 降低物理更新频率——FixedUpdate不一定要跑60fps,30fps在很多场景下够用
- 慎用连续碰撞检测——CCD开销很大,只在高速物体上启用
4.3 GC垃圾回收:看不见的帧率杀手
GC是CPU瓶颈里最隐蔽的一个。它不像脚本逻辑那样每帧都跑,而是突然来一次,一次卡好几帧。
为什么会这样?因为GC触发时,Unity会暂停所有线程,遍历托管堆,标记并回收不再使用的对象。这个过程是STW(Stop The World)的。如果堆里有大量临时对象,GC一次可能耗时几十甚至上百毫秒。
我见过最夸张的例子:一个卡牌游戏,每帧new了上千个临时字符串做UI拼接。游戏运行30秒后,GC一次耗时200ms,画面直接卡死。玩家反馈说「点卡牌像在PPT里操作」。
警告:GC不是「偶尔卡一下」的问题。在移动端,GC频率高会导致设备发热、降频,最终帧率持续走低。这不是优化问题,是体验问题。
GC优化的核心思路就一个:减少堆内存分配。具体怎么做?
- 对象池——子弹、特效、敌人等频繁创建销毁的对象,用对象池复用
- 避免字符串拼接——用StringBuilder或字符串格式化替代
- 使用结构体——struct是值类型,分配在栈上,不触发GC
- 缓存委托和Lambda——每帧new Lambda表达式会产生闭包对象
- 协程用yield return null——别用yield return new WaitForSeconds,会分配堆内存
我的经验:在项目初期就引入内存分析工具(如Unity Profiler的Memory模块),每两天跑一次内存快照。发现堆内存持续增长,立刻定位分配热点。等到项目后期再修GC问题,成本至少翻三倍。
4.4 三者的关联与排查顺序
脚本逻辑、物理引擎、GC这三者不是孤立的。它们经常互相影响:
- 脚本逻辑过重 → CPU占用高 → 物理引擎时间被挤占 → 物理模拟精度下降
- 物理引擎开销大 → 每帧耗时增加 → 帧率下降 → 脚本逻辑执行频率降低
- 脚本逻辑产生大量临时对象 → GC触发 → 卡顿 → 物理引擎同步延迟
排查时,我建议按这个顺序来:
- 先看Profiler的CPU模块——哪个模块耗时最多?脚本?物理?还是GC?
- 如果是脚本——定位具体函数,看是不是每帧都在做重复计算
- 如果是物理——检查碰撞体数量、刚体数量、物理更新频率
- 如果是GC——看堆内存分配曲线,定位分配热点的代码行
下面这张图是我自己总结的CPU瓶颈排查流程,你可以参考:
嗯,这里要注意一点:很多时候CPU瓶颈是复合的。脚本逻辑产生大量GC,GC卡顿导致物理同步延迟,物理延迟又让脚本逻辑多跑了几帧。你修了其中一个,另一个又冒出来。所以排查时要「修一个、测一次、再修下一个」。
我个人习惯在Profiler里加几个自定义标记(Profiler.BeginSample),把关键逻辑的耗时单独标出来。这样一眼就能看出哪个环节最耗时间,不用靠猜。
最后说一句:CPU优化没有银弹。每个项目的情况都不一样。但只要你掌握了排查方法,再复杂的瓶颈也能一步步拆解掉。