关于 unity-performance
面向 Unity 开发者的性能分析与优化参考技能,介绍 Profiler、内存分析和 Frame Debugger 等工具。内容涵盖 CPU、GPU、内存、渲染及脚本优化,并提供性能分析实践。
主要功能
- 01Profiler 与性能分析工具概览
- 02CPU、GPU 和内存指标解读
- 03GC 分配与资源内存优化
- 04脚本、物理与渲染优化
- 05Frame Debugger 绘制调用检查
- 06设备性能分析实践
适用场景
- 01在目标设备上定位帧时间瓶颈
- 02追踪游戏过程中的 GC 分配
- 03检查批处理中断与过度绘制
- 04优化纹理、网格和音频资源占用
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ nice-wolf-studio/ unity-claude-skills --skill unity-performance安装后在 Claude Code 中输入/unity-performance确认已加载。
附带文件references/memory-optimization.mdreferences/profiler-guide.md
详细介绍
Unity 性能分析与优化
基于 Unity 6.3 LTS(6000.3)官方文档
核心工具概览
Unity 提供了多种内置工具和基于软件包的工具,用于性能分析与优化:
| 工具 | 访问方式 | 用途 |
|---|---|---|
| Unity Profiler | Window > Analysis > Profiler | 按帧查看 CPU、GPU、内存和渲染指标 |
| Memory Profiler | 软件包:com.unity.memoryprofiler | 深度内存快照、对比和泄漏检测 |
| Frame Debugger | Window > Analysis > Frame Debugger | 检查渲染帧中的每一次绘制调用 |
| Profile Analyzer | 软件包:com.unity.performance.profile-analyzer | 对多个 Profiler 帧进行统计分析 |
Unity Profiler
Profiler 会捕获逐帧性能数据。它可以在 Editor 中运行,也可以连接到目标设备上的 Development Build。
CPU 模块
CPU 模块是查找性能瓶颈的主要工具。
视图:
- Timeline view -- 在时间轴上并排显示所有线程。最适合查看线程争用、作业系统利用率以及帧时间的分配情况。
- Hierarchy view -- 将采样数据汇总成调用树。显示 Total %、Self %、Calls、GC Alloc 和 Time 列。最适合查找开销最大的函数。
关键线程:
- 主线程 -- 游戏逻辑、脚本、动画、UI 布局
- 渲染线程 -- 向 GPU 提交绘制调用(SRP 命令缓冲区执行)
- Job Worker 线程 -- C# Jobs、Burst 编译的工作负载、物理模拟
Profiler 标记:
- 内置标记(例如,
PlayerLoop、Update.ScriptRunBehaviourUpdate、Physics.Simulate) - 使用
Unity.Profiling.ProfilerMarker自定义标记(参见 references/profiler-guide.md)
深度性能分析:
- 自动检测每一次 C# 方法调用
- 警告:会产生 10-100 倍的额外开销,分析结果无法反映实际性能
- 请改用
ProfilerMarker进行有针对性的检测
GC 分配的调用堆栈:
- 在 Profiler 工具栏中启用此功能,即可查看托管分配的来源
- 会带来适度的额外开销,但对于追踪 GC 峰值非常有用
GPU 模块
- 按渲染通道显示 GPU 帧时间
- 识别着色器复杂度和填充率问题
- 有助于诊断过度绘制和开销较大的片元着色器
- 注意:并非所有平台都支持 GPU 性能分析(请查阅平台文档)
- 在移动设备上,请使用平台专用的 GPU 性能分析工具(Xcode GPU Debugger、RenderDoc、Arm Mobile Studio)
内存模块
显示逐帧内存摘要:
- 托管内存 -- C# 堆(Mono/IL2CPP)、GC 分配
- 原生内存 -- 纹理、网格、音频剪辑、渲染目标、原生容器
- 预留总量与已用总量 -- Unity 从 OS 预留的内存量与实际使用的内存量
- 帧内 GC 分配量 -- 关键指标;游戏运行期间该值非零表示存在分配压力
其他模块
| 模块 | 关键指标 |
|---|---|
| 渲染 | 批次、SetPass 调用次数、三角形、顶点、阴影投射体 |
| 物理 | 活动/休眠刚体、接触点、宽相位碰撞对 |
| 音频 | 播放中的音源、DSP 负载、内存使用量 |
| UI | Canvas 重建、布局重建、批次 |
| 视频 | 视频播放器解码时间 |
性能分析最佳实践
- 在目标设备上进行性能分析 —— 编辑器开销会显著影响结果。务必在实际硬件上验证。
- 使用开发版构建 —— 在 Build Settings 中启用
Development Build和Autoconnect Profiler。构建启动后,Profiler 会自动连接。 - 使用 ProfilerMarker —— 使用自定义标记对关键代码路径进行插桩,无需承担 Deep Profile 的开销即可精确测量。
- 分析有代表性的场景 —— 测试最坏情况的场景(大量敌人、粒子效果、复杂 UI)。
- 比较前后数据 —— 使用 Profile Analyzer 对采集结果进行统计比较。
- 设定帧预算 —— 明确目标:
- 60 FPS = 每帧 16.6 ms
- 120 FPS = 每帧 8.3 ms
- 30 FPS = 每帧 33.3 ms
- 分析 CPU 时禁用 VSync —— VSync 会掩盖 CPU 余量。
内存优化
应避免的 GC 分配模式
每次托管内存分配都会增加 GC 压力。在高频执行路径(Update、FixedUpdate、LateUpdate)中,目标是零分配。
| 模式 | 问题 | 修复方法 |
|---|---|---|
字符串拼接(+) | 每次都会创建新字符串 | 使用 StringBuilder 或 string.Create |
| LINQ 查询 | 产生迭代器分配,并可能导致闭包装箱 | 使用手动 for/foreach 循环 |
| 捕获局部变量的 Lambda 闭包 | 编译器会生成类分配 | 缓存委托或使用静态 Lambda |
装箱(int 转 object) | 在托管堆上分配内存 | 使用泛型 API,避免使用 object 参数 |
对非泛型 IEnumerable 使用 foreach | 枚举器装箱 | 使用强类型集合或 for 循环 |
params 数组 | 每次调用都会分配数组 | 提供重载或使用 Span<T> |
对值类型调用 ToString() | 会分配字符串 | 缓存结果或避免在高频路径中调用 |
资源内存
纹理(通常是最大的内存消耗项):
- 在移动平台上使用 ASTC 压缩,在桌面平台上使用 BC7/DXT
- 启用 mipmap 流式加载(
Texture.streamingMipmaps)以限制 VRAM 使用量 - 在画质允许的情况下,降低最大纹理尺寸
- 使用纹理图集以减少绘制调用和内存碎片
网格:
- 在导入设置中启用网格压缩
- 使用 LOD Groups 在远处降低顶点数量
- 在导入设置中移除未使用的顶点通道(切线、颜色)
音频:
Decompress on Load—— 播放速度快、内存占用高(仅适用于较短的音效)Compressed in Memory—— 内存占用较低、CPU 开销适中(适用于中等长度的片段)Streaming—— 内存占用极低、持续进行 I/O(适用于音乐、环境音)- 根据长度和使用频率为每个片段设置
Load Type
卸载:
Resources.UnloadUnusedAssets()—— 释放没有引用的资源(速度较慢,适合在加载界面使用)- Addressables:使用
Addressables.Release()减少引用计数,并在计数归零时卸载
对象池
重复使用频繁创建和销毁的对象,以避免 GC 压力和实例化开销。
using UnityEngine.Pool;
public class BulletSpawner : MonoBehaviour
{
[SerializeField] private Bullet bulletPrefab;
private ObjectPool<Bullet> _pool;
private void Awake()
{
_pool = new ObjectPool<Bullet>(
createFunc: () => Instantiate(bulletPrefab),
actionOnGet: b => b.gameObject.SetActive(true),
actionOnRelease: b => b.gameObject.SetActive(false),
actionOnDestroy: b => Destroy(b.gameObject),
defaultCapacity: 20,
maxSize: 100
);
}
public Bullet GetBullet() => _pool.Get();
public void ReturnBullet(Bullet b) => _pool.Release(b);
}
另请参阅:unity-scripting,了解更多对象池模式。
CPU 优化
脚本
- 缓存
GetComponent结果 —— 在Awake/Start中调用,并将结果存储在字段中 - 使用
CompareTag("Tag")—— 避免从gameObject.tag产生字符串分配 - 避免在高频路径中使用
Find方法 ——Find、FindObjectOfType、FindObjectsByType开销较大;应缓存引用 - 使用 C# Jobs + Burst —— 适用于数据密集型处理(空间查询、AI、寻路)。参见 unity-ecs-dots。
- 使用
Awaitable执行异步操作 —— 非阻塞式场景加载、资源加载和 Web 请求
// Cache component references
private Rigidbody _rb;
private void Awake() => _rb = GetComponent<Rigidbody>();
// Use CompareTag instead of string comparison
if (other.CompareTag("Enemy")) { /* ... */ }
物理
- 简化碰撞体 —— 优先使用基本碰撞体(Box、Sphere、Capsule),而非 MeshCollider
- 基于图层的碰撞矩阵 —— 在 Project Settings > Physics 中禁用不必要的碰撞组合
- FixedUpdate 时间步长 —— 默认值为 0.02s(50Hz)。对于性能要求较低的游戏,可增大到 0.03-0.04s。
- Rigidbody 休眠 —— 确保对象进入休眠;避免对静止对象持续调用
AddForce。 - 无分配查询 —— 使用
Physics.RaycastNonAlloc、Physics.OverlapSphereNonAlloc
渲染
- SRP Batcher —— 在 URP/HDRP 中默认启用。可降低兼容着色器的 SetPass 调用开销。
- 静态批处理 —— 将不移动的对象标记为
Batching Static,以便在构建时合并网格。 - GPU Instancing —— 为大量相同对象(树木、草、碎片)启用材质上的此功能。
- 动态批处理 —— 在运行时批处理小型网格(在 SRP 中收益有限;优先使用 GPU instancing)。
- 遮挡剔除 —— 烘焙遮挡数据,以跳过对墙后对象的渲染。
- LOD Groups —— 降低远处对象的几何复杂度。
- 着色器变体 —— 尽量减少关键字组合;在 Player Settings 中剥离未使用的变体。
帧调试器
帧调试器(Window > Analysis > Frame Debugger)可让你逐个检查一帧中的每次绘制调用。
工作流:
- 打开帧调试器并点击 Enable
- Game 视图会冻结在当前帧
- 在左侧面板中逐步检查绘制调用
- 检查:着色器、通道、关键字、渲染目标、网格、材质属性
- 找出冗余绘制、批处理断点和过度绘制
常见用途:
- 验证 SRP Batcher / 批处理的效果
- 找出批次中断的原因(不同的材质、着色器关键字、渲染状态变化)
- 检查阴影通道及其开销
- 调试透明对象排序问题
- 验证渲染目标切换(每次切换都会产生开销)
- 检查全屏后处理通道的数量
IL2CPP 和托管代码性能
Unity 6 默认在大多数平台上使用 IL2CPP。需要注意的性能要点:
- IL2CPP 提前编译 会将 C# 转换为 C++,相比 Mono JIT 可实现更好的优化
- 代码剥离 会移除未使用的代码,从而减小构建体积并改善启动速度
- 泛型共享 —— IL2CPP 会为引用类型泛型共享代码,但会为值类型泛型生成单独的代码。过多的值类型泛型实例化会增大二进制文件体积。
- 虚方法调用 —— IL2CPP 使用 vtable 分派;
sealed类/方法可实现去虚拟化和内联 - 结构体布局 —— 对传递给原生代码的数据使用
[StructLayout(LayoutKind.Sequential)]。保持结构体较小(小于 16 bytes),以便高效传递。
// sealed enables devirtualization -- IL2CPP can inline the call
public sealed class FastEnemy : EnemyBase
{
public override void Think()
{
// This can be inlined by IL2CPP when called through a typed reference
}
}
质量设置和自适应性能
运行时质量切换
// Switch quality level at runtime (e.g., from settings menu)
QualitySettings.SetQualityLevel(qualityIndex, applyExpensiveChanges: true);
// Adjust individual settings
QualitySettings.shadows = ShadowQuality.Disable;
QualitySettings.vSyncCount = 0;
Application.targetFrameRate = 60;
自适应性能(移动端)
Adaptive Performance 包(com.unity.adaptiveperformance)会根据设备热状态和性能指标动态调整质量:
- 设备过热时自动降低分辨率、LOD 偏差和阴影质量
- 提供
IAdaptivePerformance接口,用于自定义缩放逻辑 - 支持 Samsung(GameSDK)和通用提供程序
常见性能模式
异步场景加载
using UnityEngine.SceneManagement;
public async Awaitable LoadSceneAsync(string sceneName)
{
AsyncOperation op = SceneManager.LoadSceneAsync(sceneName);
op.allowSceneActivation = false;
while (op.progress < 0.9f)
{
// Update loading bar: op.progress / 0.9f
await Awaitable.NextFrameAsync();
}
op.allowSceneActivation = true;
}
将工作分散到多个帧
// Process a large list over multiple frames
private IEnumerator ProcessChunked<T>(List<T> items, int chunkSize, System.Action<T> process)
{
for (int i = 0; i < items.Count; i++)
{
process(items[i]);
if ((i + 1) % chunkSize == 0)
yield return null; // Resume next frame
}
}
关注帧预算
using System.Diagnostics;
private readonly Stopwatch _sw = new();
private const float BUDGET_MS = 4f; // Portion of frame budget
private void Update()
{
_sw.Restart();
while (_workQueue.Count > 0 && _sw.Elapsed.TotalMilliseconds < BUDGET_MS)
{
ProcessNextItem(_workQueue.Dequeue());
}
}
使用 Additive 场景进行关卡流式加载
using UnityEngine.SceneManagement;
// Load adjacent area additively without blocking
public async Awaitable StreamArea(string sceneName)
{
AsyncOperation op = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);
while (!op.isDone)
await Awaitable.NextFrameAsync();
}
// Unload area when player moves away
public async Awaitable UnloadArea(string sceneName)
{
AsyncOperation op = SceneManager.UnloadSceneAsync(sceneName);
while (!op.isDone)
await Awaitable.NextFrameAsync();
// Reclaim memory from unloaded assets
Resources.UnloadUnusedAssets();
}
自定义 ProfilerMarker 插桩
using Unity.Profiling;
public class AISystem : MonoBehaviour
{
private static readonly ProfilerMarker s_AIUpdate = new("AISystem.Update");
private static readonly ProfilerMarker s_Pathfind = new("AISystem.Pathfind");
private void Update()
{
using (s_AIUpdate.Auto())
{
foreach (var agent in _agents)
{
using (s_Pathfind.Auto())
{
agent.RecalculatePath();
}
}
}
}
}
常见瓶颈与解决方法
| 症状 | 可能原因 | 解决方法 |
|---|---|---|
| FPS 低,CPU 主线程占用高 | Update 中逻辑过多 | 使用 Profiler 分析;优化、改用 Jobs,或分散到多帧处理 |
| GC 峰值(帧卡顿) | 热路径中发生分配 | 使用对象池、缓存;避免在 Update 中使用 LINQ/字符串拼接 |
| 绘制调用 / SetPass 调用过多 | 未进行批处理或材质过多 | 启用 SRP Batcher、使用 GPU 实例化、使用图集纹理 |
| 内存持续增长 | 资源泄漏、Addressables 未释放 | 调用 Resources.UnloadUnusedAssets(),检查 Addressable 引用计数 |
| 物理卡顿 | 激活的碰撞体或接触点过多 | 简化碰撞体、使用碰撞矩阵图层、增大时间步长 |
| 着色器编译卡顿 | 按需编译着色器变体 | 使用着色器预热(ShaderVariantCollection.WarmUp()),减少变体数量 |
| 加载时间过长 | 同步加载资源 | 使用 Addressables.LoadAssetAsync 或 Resources.LoadAsync |
| 渲染线程受限 | 绘制调用或 GPU 命令过多 | 减少批次、简化着色器、降低分辨率 |
反模式
| 做法 | 有害原因 | 解决方法 |
|---|---|---|
在生产环境中使用 Debug.Log | 字符串格式化 + 托管分配 + I/O | 使用 [Conditional("UNITY_EDITOR")],或通过脚本定义剔除 |
在 Update 中使用 Camera.main | 每次都会在内部调用 FindGameObjectWithTag | 在 Awake 或 Start 中缓存 |
每帧调用 Instantiate/Destroy | 每次调用都会产生 GC 分配 + 原生开销 | 使用 ObjectPool<T> |
在 Update 中调用 GetComponent<T>() | 每帧都会产生查找开销 | 在 Awake/Start 中缓存 |
使用 OnGUI 构建游戏 UI | IMGUI 每帧重绘,GC 开销大 | 使用 UI Toolkit 或 uGUI(Canvas) |
| 使用 Deep Profile 进行性能测试 | 10-100x 的开销会使结果失效 | 使用 ProfilerMarker 进行针对性检测 |
在 FixedUpdate 中分配内存 | 默认以物理频率(50Hz)运行,会放大 GC 影响 | 预先分配、使用对象池、使用 NativeArray |
使用 SendMessage / BroadcastMessage | 基于反射,且会产生分配 | 使用直接方法调用、事件或 UnityEvent |
关键 API 速查
| API | 命名空间 | 用途 |
|---|---|---|
ProfilerMarker | Unity.Profiling | 零开销的自定义 Profiler 检测 |
ProfilerRecorder | Unity.Profiling | 在运行时读取 Profiler 计数器值 |
Profiler.BeginSample / EndSample | UnityEngine.Profiling | 旧版自定义 Profiler 样本(在非开发版构建中会被剔除) |
ObjectPool<T> | UnityEngine.Pool | 通用对象池 |
ListPool<T> / HashSetPool<T> | UnityEngine.Pool | 临时集合对象池 |
NativeArray<T> | Unity.Collections | 用于 Jobs/Burst 的非托管、无 GC 数组 |
NativeList<T> | Unity.Collections | 非托管、可调整大小的列表 |
Stopwatch | System.Diagnostics | 高精度计时 |
ShaderVariantCollection | UnityEngine.Rendering | 通过着色器预热避免编译卡顿 |
QualitySettings | UnityEngine | 在运行时切换质量等级 |
相关技能
- 有关脚本编写模式和组件生命周期,请参阅 unity-scripting
- 有关渲染管线和着色器优化,请参阅 unity-graphics
- 有关移动端/主机平台优化,请参阅 unity-platforms
- 有关 ECS、Jobs 和 Burst(面向数据的性能优化),请参阅 unity-ecs-dots
- 有关物理配置和优化,请参阅 unity-physics
- 有关 UI 性能(Canvas 重建、批处理),请参阅 unity-ui
附加资源
- 详细了解 Profiler API 的用法和工作流,请参阅 references/profiler-guide.md
- 深入了解内存管理策略,请参阅 references/memory-optimization.md
评论
支持 Markdown