每个Unity开发者都经历过这种落差:开发机上丝滑流畅,真机一跑帧率暴跌、手机发烫、电量肉眼可见地往下掉。Ocean View Games在Domi Online项目里处理过10万棵GPU实例化树木的渲染,创始人曾在Jagex负责RuneScape手游优化——这套内部清单就是他们用血泪换来的。
优化不是最后打补丁,而是贯穿开发全程的纪律。但即便小心翼翼,问题还是会冒出来:Draw Call堆积、纹理膨胀、GC(垃圾回收) spikes在Profiler里准时打卡。这份清单按优先级排序,从分析工具开始,因为盲目优化是灾难。
第0步:先分析,再动手
这是铁律。永远不要凭直觉优化。必须在真机上分析,定位真实瓶颈,再针对性解决。
核心工具包:Unity Profiler(CPU/GPU/内存/渲染模块,USB或WiFi连接真机,Editor数据不可信)、Frame Debugger(逐帧查看渲染顺序,揪出冗余Draw Call和Overdraw)、Memory Profiler(Package Manager单独安装,追踪纹理/网格/音频内存)、Xcode Instruments(iOS Metal System Trace看GPU底层)、Android GPU Inspector/Snapdragon Profiler(高通芯片专项)。
分析前回答三个问题:CPU瓶颈还是GPU瓶颈?Profiler时间轴一目了然——CPU帧时超过GPU,问题在脚本/物理/动画,而非渲染。Spikes在哪?稳定20fps和周期性200ms卡顿是两种病,后者通常是GC或一次性高开销操作。内存 footprint 多少?移动端有硬上限,超了就静默崩溃,连报错日志都没有。
关键原则:永远在最低配目标设备上分析。iPhone 15 Pro跑60fps、三年前的Android闪退——这不叫优化,叫没测试。
渲染与Draw Call
渲染是移动端最吃资源的环节。Unity默认设置对PC友好,对手机是灾难。策略分三层:减少Draw Call、降低Overdraw、控制着色器复杂度。
Draw Call合并:Static Batching对静态场景有效,Dynamic Batching适合小网格但CPU开销大,GPU Instancing是大规模重复物体的最优解(Domi Online的10万棵树靠这个)。SRP Batcher在URP/HDRP下能大幅降低CPU准备时间,但要求Shader兼容。
Overdraw控制:移动端填充率有限,半透明叠加是杀手。用Frame Debugger检查,确保UI和粒子系统不堆叠。Opaque物体从前到后渲染,Unity会自动Early-Z剔除;如果顺序乱了,在URP里调整Render Queue。
Shader精简:复杂光照计算、逐像素效果在移动端是奢侈品。用Simple Lit代替Lit,移动端专用Shader变体,关闭不必要的Keyword。在Xcode Instruments里看GPU时间分布,Fragment Shader占比过高就是像素杀手的信号。
纹理与内存
纹理是内存大户。原则:用得上才加载,用完立刻释放。
导入设置:移动端强制ASTC(iOS/Android通用,4x4到12x12质量可选)、ETC2(Android fallback)、PVRTC(旧iOS)。禁用Read/Write Enabled(内存翻倍),非UI纹理关闭Mip Maps(省30%内存),UI纹理开启(避免远处锯齿)。Max Size按实际屏幕占比设置,1024x1024的图标在手机上可能是浪费。
图集策略:Sprite Atlas减少Draw Call,但尺寸失控会爆内存。按场景/功能拆分,运行时动态加载。Memory Profiler里盯紧Texture2D的Native内存,和Profiler里的Total Reserved对比,差距大说明有泄漏。
资源流送:Addressables或AssetBundle按需加载,场景切换时UnloadUnusedAssets——但注意这会触发GC,避开性能敏感期。
代码与GC
托管内存的GC spikes是30fps游戏的帧率杀手。C#的便利是有代价的。
零分配原则:Update里不new对象,用对象池(对象池本身用Queue/Stack预分配)。LINQ和闭包在循环里禁用,它们背后偷偷装箱。字符串用StringBuilder拼接,或者直接用插值(C# 10起分配优化了,但仍要Profiler验证)。
结构体陷阱:非只读结构体传参会复制,大结构体改class。但class有GC压力,权衡看Profiler的GC Alloc列。
异步加载:AssetBundle.LoadAssetAsync、SceneManager.LoadSceneAsync不阻塞主线程,但回调里别做重活。AsyncOperation.completed += 是C#事件,记得取消订阅防泄漏。
Job System:Burst编译的并行Job把CPU密集型工作从主线程剥离,适合大规模计算(寻路、物理模拟)。但Job数据布局要连续,指针和托管对象进不了Job。
物理与动画
Physics.Simulate默认在FixedUpdate跑,移动端降到30fps或更低。减少Rigidbody数量,静态碰撞器用Mesh Collider是性能自杀——凸包分解或干脆用Box/Sphere近似。Layer Collision Matrix关掉不交互的层,Broad Phase剔除效率翻倍。
动画:Mecanim的Blend Tree在运行时计算,简单动画用Simple Animation组件或DOTween。骨骼数量直接关联GPU Skinning开销,LOD级别里砍低级模型的骨骼数。Animator的Culling Mode设成Cull Completely,屏幕外不更新。
音频与杂项
AudioClip的Load Type:短音效Decompress On Load(CPU换内存),长音乐Streaming(内存换IO),中等Compressed In Memory。Sample Rate强制22050或更低,移动端听不出44100的区别。
最后检查项:Quality Settings按平台覆盖,移动端关掉抗锯齿或只用FXAA;V Sync Count设成Every Second V Blank(30fps目标)或Don't Sync(自己控制);Target Frame Rate显式设置,Unity默认300fps烧电。
发布前在最低配设备上跑完整流程:冷启动、连续玩30分钟、后台切回、接打电话后恢复。记录温度、电量、帧率曲线。Profiler里没问题的代码,真机热节流后会暴露。
这份清单不是一次性任务,是迭代循环。每加一个功能,重新Profile。性能预算和美术预算一样,超支就得砍。
热门跟贴