这篇按性能问题来查:卡顿看哪、GC 哪里来、对象池怎么写、UI 和渲染先关心什么。
快速索引
| 现象 |
先查 |
| 偶发卡一下 |
GC、Instantiate、加载、Shader 编译 |
| 每帧都慢 |
Update、UI Rebuild、Rendering、Physics |
| 越玩越卡 |
对象没释放、资源没卸载、列表增长、事件残留 |
| Editor 流畅真机卡 |
分辨率、后处理、阴影、贴图、平台性能 |
| 打开 UI 卡 |
Layout Rebuild、列表创建、图片加载 |
| 子弹/特效多时卡 |
对象池、粒子数量、碰撞检测 |
| 内存高 |
贴图、音频、网格、Addressables 释放 |
0. Profiler 看哪里
1 2 3 4 5 6
| CPU Usage # 脚本、物理、动画、UI 耗时 GC Alloc # 每帧托管内存分配 Rendering # Draw Call、SetPass、渲染耗时 Memory # 贴图、网格、音频、托管堆 UI # Canvas Rebuild、Layout Rebuild Physics # 物理模拟、碰撞、射线检测
|
建议流程:
1 2 3 4 5
| 目标平台复现 不开 Deep Profile 看整体 定位模块后短时间开 Deep Profile 只改一个点 优化前后对比数据
|
1. GC 常见来源
1 2 3 4 5 6 7 8
| Update 里 new List / new Dictionary Update 里 LINQ 字符串频繁拼接 foreach 某些集合枚举 装箱 boxing 闭包 lambda Debug.Log 高频输出 ToString 高频调用
|
减少 GC:
1 2 3 4 5 6
| private readonly List<Enemy> results = new();
private void Update() { results.Clear(); }
|
字符串减少刷新:
1 2 3 4 5
| if (lastHp != currentHp) { hpText.text = currentHp.ToString(); lastHp = currentHp; }
|
2. 对象池
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public sealed class SimplePool : MonoBehaviour { [SerializeField] private GameObject prefab; private readonly Queue<GameObject> pool = new();
public GameObject Get() { GameObject obj = pool.Count > 0 ? pool.Dequeue() : Instantiate(prefab);
obj.SetActive(true); return obj; }
public void Release(GameObject obj) { obj.SetActive(false); pool.Enqueue(obj); } }
|
适合池化:
1 2 3 4 5 6
| 子弹 特效 伤害数字 UI Item 怪物 音效源
|
回收时检查:
1 2 3 4 5 6
| 停止协程 取消事件 清理状态 关闭粒子 关闭碰撞 重置父节点
|
3. Update 优化
少用:
1 2 3 4 5
| private void Update() { FindObjectOfType<Player>(); GetComponent<Rigidbody>(); }
|
降频:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| private float timer;
private void Update() { timer += Time.deltaTime;
if (timer < 0.2f) { return; }
timer = 0f; ScanEnemies(); }
|
事件驱动:
1
| player.HpChanged += RefreshHp;
|
4. UI 性能
1 2 3 4 5
| 动态 UI 单独 Canvas # 减少整块重建 列表 Item 复用 # 避免频繁创建销毁 装饰图关闭 Raycast Target # 减少射线检测 减少 Layout Group 嵌套 # 降低布局计算 文本变化才赋值 # 避免反复 dirty
|
查 Profiler:
1 2 3 4
| Canvas.SendWillRenderCanvases Layout.Rebuild GraphicRaycaster.Raycast TMP.GenerateText
|
5. 渲染优化
1 2 3 4 5
| Draw Call / Batches # 批次数 SetPass Calls # 材质切换 Overdraw # 透明重叠 Shadow # 阴影开销 Post Processing # 后处理开销
|
材质实例化坑:
1
| renderer.material.color = Color.red;
|
批量改共享材质:
1
| renderer.sharedMaterial.color = Color.red;
|
单对象改参数:
1 2 3 4 5 6 7 8 9 10 11 12 13
| private MaterialPropertyBlock block;
private void Awake() { block = new MaterialPropertyBlock(); }
private void SetColor(Color color) { renderer.GetPropertyBlock(block); block.SetColor("_BaseColor", color); renderer.SetPropertyBlock(block); }
|
6. 物理优化
1 2 3 4 5
| Layer Collision Matrix # 关闭不需要的层碰撞 Fixed Timestep # 物理步长别过高 Raycast 加 LayerMask # 缩小检测范围 NonAlloc API # 减少分配 Collider 数量 # 控制碰撞体复杂度
|
Raycast:
1 2 3 4
| if (Physics.Raycast(origin, direction, out RaycastHit hit, 20f, enemyMask)) { Debug.Log(hit.collider.name); }
|
NonAlloc:
1 2 3
| private readonly RaycastHit[] hits = new RaycastHit[16];
int count = Physics.RaycastNonAlloc(ray, hits, 20f, enemyMask);
|
7. 资源导入
贴图:
1 2 3 4 5
| Max Size 是否过大 Compression 是否按平台设置 Read/Write 是否关闭 Mip Map 是否需要 Alpha 是否真的需要
|
模型:
1 2 3 4
| Read/Write 是否关闭 Rig / Animation 是否需要 Mesh Compression 是否可接受 不需要的节点和动画是否删除
|
音频:
1 2 3
| 短音效:Decompress On Load 长音乐:Streaming 压缩格式按平台设置
|
8. 移动端检查
1
| Application.targetFrameRate = 60;
|
1 2 3 4 5 6 7
| 真机测试,不只看 Editor 发热后是否降频 分辨率 / Render Scale 是否过高 后处理是否过重 阴影距离是否过大 透明 UI 是否过多 包体和内存是否超预算
|
9. 常见坑速查
1 2 3 4 5 6 7
| 没看 Profiler 凭感觉优化 # 容易改错方向 Update 每帧分配 # GC 抖动 大量 Instantiate / Destroy # 峰值卡顿 UI 列表全销毁重建 # 打开界面卡 访问 renderer.material # 材质实例增加 Addressables 不 Release # 内存增长 只测 Editor 不测真机 # 结果不准
|
系列导航