01 优化方法论:先测量,再优化
**核心原则:**不要凭感觉优化。所有优化必须来自真机 Profiler 数据,且改一处、复测一次。
性能优化是一个闭环过程,核心思路是:先建立基线,再定位瓶颈,然后针对性优化,最后回归验证。任何跳过测量直接动手改代码的做法,都可能导致无效优化甚至引入新的性能问题。
02 帧预算:明确性能目标
在开始优化之前,必须先明确目标帧率对应的帧预算。以 60 FPS 为例,单帧总预算约为 16.6 毫秒;以 30 FPS 为例,单帧总预算约为 33.3 毫秒。
帧预算需要进一步拆分为 CPU 和 GPU 两部分。CPU 侧主要关注逻辑更新、物理模拟、动画系统和渲染提交(Draw Call);GPU 侧主要关注顶点处理、片元着色、Overdraw 和带宽占用。建议为 CPU 和 GPU 分别设定预算上限,例如 CPU 8 毫秒、GPU 8 毫秒,剩余时间作为缓冲。
03 特效优化:从源头控制开销
粒子特效是移动端性能消耗的大户,优化时应从以下几个方面入手:
- 控制粒子数量:单个特效的粒子上限应结合屏幕占比设定,远处或小面积特效可适当降低发射率。
- 限制 Overdraw:半透明粒子叠加会造成严重的像素重复写入,应避免大面积、多层级的半透明特效叠加。
- 合理使用贴图:优先使用图集(Atlas)合并特效贴图,减少材质切换和 Draw Call。
- 关闭不必要的组件:不需要碰撞检测的粒子系统应关闭 Collision 模块,不需要灯光交互的应关闭灯光模块。
04 粒子模块:按需启用
Unity 粒子系统包含多个可选模块,每个模块都会带来额外的 CPU 开销。默认情况下,只应启用当前特效真正需要的模块。
常见的开销来源包括:Collision 模块(碰撞检测)、Sub Emitters 模块(子发射器)、Noise 模块(噪声扰动)、Trails 模块(拖尾渲染)。如果特效不需要这些能力,应保持模块关闭状态。此外,粒子系统的 Scaling Mode 建议设置为 Hierarchy 或 Shape,避免每帧进行不必要的变换计算。
05 模型管线:减面与 LOD
模型侧的性能优化主要围绕顶点数量和渲染负担展开:
- 减面:在建模阶段或导入设置中合理减面,去掉不可见或低贡献的顶点。
- LOD 分级:为模型配置多级 LOD(Level of Detail),根据相机距离切换不同精度的网格。
- 网格压缩:在导入设置中开启网格压缩(Mesh Compression),减少内存占用。
- 蒙皮优化:对于骨骼动画模型,合理设置骨骼数量,避免冗余骨骼参与计算。
06 合批:减少 Draw Call
Draw Call 是 CPU 侧渲染提交的主要开销。减少 Draw Call 的核心手段是合批(Batching),Unity 提供两种内置合批方式:
- 静态合批(Static Batching):适用于场景中不移动的物体,在构建时合并网格,运行时只需一次提交。
- 动态合批(Dynamic Batching):适用于小规模、移动的物体,Unity 在运行时自动合并满足条件的网格。
此外,使用图集(Atlas)合并贴图、减少材质种类,也能有效提升合批成功率。需要特别注意的是,动态合批对顶点数有限制,且合批本身也有 CPU 开销,应结合 Profiler 数据判断是否值得开启。
07 贴图内存:格式与压缩
贴图是内存占用的主要来源之一,优化方向包括:
- 压缩格式:移动端优先使用 ASTC(Android)和 PVRTC(iOS)等硬件压缩格式,避免使用未压缩的 RGBA32。
- 尺寸控制:贴图尺寸应匹配实际显示需求,避免使用过大的贴图。
- Mipmap 策略:对于 3D 场景中的贴图,开启 Mipmap 可以减少远处采样时的显存带宽压力;对于 UI 贴图,通常不需要 Mipmap。
- 图集合并:将多张小贴图合并为图集,减少内存碎片和加载次数。
08 UI 分组:动静分离
UI 性能优化的核心思路是动静分离:将静态 UI 和动态 UI 分开管理,避免动态元素破坏静态元素的合批。
Canvas 的重建(Rebuild)开销与 UI 元素的变更频率直接相关。建议将频繁变化的文本、图标等元素放在独立的 Canvas 中,静态背景和固定布局放在另一个 Canvas 中,从而减少不必要的网格重建。
09 UI 合批:层级与图集
UI 合批的成败取决于层级顺序和贴图使用方式:
- 层级顺序:相邻且使用同一图集的 UI 元素可以合批,中间插入其他图集的元素会打断合批。
- 图集管理:将 UI 图片统一放入图集,避免使用独立贴图。
- 避免 Raycast 滥用:不需要交互的 UI 元素应关闭 Raycast Target,减少命中检测开销。
- 减少 Outline / Shadow:描边和阴影组件会额外生成顶点,应谨慎使用。
10 GC 优化:减少堆分配
C# 脚本中的堆分配是 GC(垃圾回收)压力的主要来源。GC 优化应关注以下几点:
- 避免频繁字符串拼接:使用 StringBuilder 替代多次字符串相加。
- 缓存组件引用:避免在 Update 中反复调用 GetComponent。
- 使用对象池:对于频繁创建和销毁的对象(如子弹、飘字),使用对象池复用实例。
- 避免闭包和 LINQ:在热路径中避免使用匿名函数和 LINQ 查询,它们会产生额外的堆分配。
- 注意协程和事件:协程的 yield 和事件的 += / -= 也可能产生分配,应合理管理生命周期。
11 内存泄漏:定位与预防
内存泄漏会导致游戏运行时间越长越卡顿,最终可能被系统强制杀掉。常见的内存泄漏来源包括:
- 事件未解绑:对象销毁前未移除事件监听,导致对象无法被回收。
- 静态引用:静态字段持有对象引用,导致对象长期驻留内存。
- 协程未停止:场景切换时未停止协程,协程持有的引用无法释放。
- 资源未卸载:加载的 Asset 未通过 Resources.UnloadUnusedAssets 或 Addressables 释放。
定位内存泄漏的常用手段是使用 Unity Profiler 的 Memory 模块,观察堆内存的持续增长趋势,并结合对象快照对比分析。
12 渲染排序:减少状态切换
渲染排序直接影响 GPU 的状态切换开销。优化方向包括:
- 材质排序:将使用相同材质的物体尽量连续渲染,减少材质切换。
- 层级控制:通过 Render Queue 和 Sorting Layer 合理控制渲染顺序,避免不必要的深度测试失败。
- 减少 Shader 变体:Shader 变体过多会导致编译时间和运行时切换开销,应通过 Shader Variant Collection 控制。
13 包体优化:裁剪与压缩
包体大小影响下载转化率和用户留存,优化方向包括:
- 资源裁剪:移除未使用的资源,使用 Asset Bundle 按需加载。
- 贴图压缩:使用硬件压缩格式,避免重复导入同一贴图。
- 音频压缩:音频文件使用合适的压缩格式(如 Vorbis / AAC),控制采样率和时长。
- 代码裁剪:启用 IL2CPP 的代码裁剪(Strip Engine Code),移除未使用的引擎代码。
总清单:性能优化速查
以下是一份面向面试和日常开发的性能优化速查清单:
| 维度 | 核心手段 | 关键指标 |
|---|---|---|
| 帧预算 | CPU / GPU 分项预算 | 16.6 ms(60 FPS) |
| 特效 | 粒子数量、Overdraw、图集 | Draw Call、填充率 |
| 模型 | 减面、LOD、网格压缩 | 顶点数、三角面数 |
| 合批 | 静态合批、动态合批、图集 | Draw Call 数量 |
| 贴图 | ASTC / PVRTC、尺寸、Mipmap | 显存占用 |
| UI | 动静分离、图集、层级 | Canvas Rebuild 次数 |
| GC | 对象池、StringBuilder、缓存引用 | 堆分配量 |
| 内存 | 事件解绑、资源卸载 | 堆内存曲线 |
| 包体 | Asset Bundle、压缩、裁剪 | 安装包大小 |