十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Unity移动端帧率保卫战:从硬件差异到性能优化的完整指南

Unity移动端帧率保卫战:从硬件差异到性能优化的完整指南 作为一个常年蹲在Profiler前面掉头发的Unity开发者我每次在编辑器里跑得飞起的Demo一打包到手机上就变成幻灯片这种“同码不同命”的落差感太熟悉了。很多刚转移动端的朋友都会问同一个问题明明逻辑一模一样、资源一模一样为什么移动端和PC的帧率能差出好几倍甚至有些项目在PC上能跑到200帧到了手机上连30帧都稳不住。这篇《Unity 卡顿·帧率保卫战》第二篇我就用实战视角把这件事拆透移动端和PC跑同样代码差距到底从哪里来以及我们在Unity里从哪些维度去追、去修。这套内容适合正在做移动端Unity项目、被帧率困扰的客户端开发者也适合准备从PC转向移动端的同学。我先说结论性能差距九成以上不怪代码逻辑而是硬件执行模型和资源管线的底层差异。你写的那行Shader、那个粒子特效、那套UI结构在PC上是小事在移动端可能就是吞帧大户。搞清楚这个底层逻辑后面所有优化手段才有方向感。1. 同为渲染一帧PC和移动端的硬件工作方式完全不同1.1 指令执行模型PC的“暴力堆料”与移动端的“精打细算”先看CPU侧。桌面端的CPU无论Intel还是AMD单核性能都极其夸张主频动不动4到5GHz还有巨大的L2/L3缓存和极高的内存带宽功耗可以不设上限地往上飙。移动端CPU是ARM架构主打能效比典型的大核主频也就2到3GHz还要考虑散热和电池不能长时间满血跑。这意味着什么你在PC上写一个每帧执行的复杂循环假设它消耗2ms在PC CPU上可能只占一个核的10%完全不疼不痒。但同样的代码放到手机的大核上跑可能就是3ms甚至4ms如果手机还有多任务在后台抢资源这个数字还要翻。更麻烦的是移动端CPU是大小核架构比如骁龙的Cortex-X超大核配A系列中核Unity的主线程调度到哪个核上直接影响你的帧耗时这就不完全是代码层面能控制的了。GPU侧的差异更是根本性的。桌面GPUNVIDIA、AMD用的是Immediate Mode RenderingIMR简单说就是“画一笔算一笔写一笔”——每个绘制指令产生的结果直接写回显存然后再画下一个。这种模式优点是灵活缺点是显存带宽消耗大。移动端GPU则基本全是Tile-Based RenderingTBR或PowerVR的TBDR它把屏幕分成一个个小块Tile通常是16x16或32x32像素每个Tile先在芯片内部的快速缓存里完成所有绘制计算最后才把结果一次性写回显存。这个架构差异直接决定了移动端最忌讳的事情之一——Overdraw重复绘制。在PC上你叠加10层全屏半透明粒子GPU也就是多算几次混合带宽管够问题不大。在移动端每个Tile内的片元计算是并行的但片上缓存和带宽非常有限大量Overdraw会瞬间把Tile的存储和带宽打穿帧率直接崩塌。这也是为什么移动端项目会特别强调“控制半透明层级”和“减少全屏特效”。1.2 带宽与内存移动端的隐形天花板很多开发者只盯着GPU算力忽略了带宽这个更隐蔽的限制。桌面显卡有专门的GDDR6显存带宽动辄几百GB/s甚至更高还配了超大容量纹理随便采。移动端是统一内存架构UMACPU和GPU共享同一块LPDDR内存带宽通常只有20到50GB/s还要同时喂给CPU、GPU、显示控制器、ISP图像信号处理器等一堆单元。带宽不够的直接后果就是纹理采样和帧缓冲读写成为巨大瓶颈。比如你用了未经压缩的RGBA32纹理每采一次样就要读32bit数据在移动端密密麻麻的片元计算下带宽会被迅速耗尽。这就是为什么移动端强制要求用ASTC或ETC2这类压缩纹理格式——ASTC不仅能压缩到原来的四分之一甚至八分之一还同样支持Alpha通道是平衡画质和带宽的最佳选择。我见过团队把一张4K RGBA未压缩贴图直接扔进手机项目一帧光采样这张图就把GPU时间吃掉了大半换成ASTC 8x8之后GPU耗时直接砍掉60%以上。内存方面还有个更现实的坑移动端的内存和显存是同一份剩余内存不足时系统会果断杀掉后台应用甚至触发低内存回收。PC上你开个3GB的Unity项目毫无感觉手机上一个500MB的内存峰值在低端机上就可能直接闪退。所以移动端不止要管帧率还要管内存水位。1.3 温控与功耗PC跑分式性能与移动端持续性能的差别这是很多人忽略的一点。PC性能测试基本是一次性的瞬时峰值散热好可以长时间满载跑。手机不行——SoC一热系统强制降频大核锁频、GPU降频性能直接腰斩。我实测过一台骁龙8系机型刚开机时能稳定跑满60帧的某场景连续玩10分钟后帧率降到40帧再往后锁到30帧这就是温控墙在起作用。所以移动端优化的目标从来不是“瞬间的高帧率”而是“持续稳定的帧率”。这就要求我们对CPU和GPU的负载要留足余量而不是压着极限跑。行业内常说“50%负载法则”——最好让单帧的CPU和GPU耗时都控制在目标帧耗时的一半以内这样遇到温控降频、后台任务抢占时帧率才不至于立刻崩塌。举个具体的数目标60帧单帧预算16.6ms最好把CPU主线程压到8ms以内GPU压到8ms以内两边并行后实际单帧能在10ms左右这才算健康。2. 同样的Unity工程移动端最容易踩的几个性能大坑2.1 Draw Call与合批CPU侧的第一堵墙拿到一个PC上流畅的Demo先别急着看画面打开Profiler看两个数Draw Call数量和SetPass Call数量。PC上几百上千的Draw Call对CPU来说只是毛毛雨但移动端CPU性能弱每多一个Draw Call就意味着CPU要多一次状态切换和提交开销。Unity的渲染线程要把渲染命令打包提交给GPU这个提交过程在移动端非常昂贵。以我的经验移动端中低端机的安全Draw Call线在200到300高端机可以到400到500超过这条线帧率就开始明显下滑。优化手段按优先级排列能用SRP Batcher就用SRP BatcherURP管线默认支持前提是Shader兼容它能把大量使用同一种Shader变体的小物体合批到一起然后是GPU Instancing适合大量相同网格的物体比如草、树木、石头最后才是Static Batching和动态合批这两个在移动端限制多、收益不稳定别把它们当主力。顺便说一个容易被忽略的点UI的Draw Call。UGUI的Canvas重建和批处理在移动端是重灾区尤其是频繁变动的文本、带阴影的文字、动态列表。一个拥有大量动态元素的UI界面轻松吃掉几百个Draw Call和大量的CPU时间这在PC上看不出来在手机上卡顿感极其明显。建议移动端用TextMeshPro时开启其自带的合批优化动态UI元素尽量分层隔离在独立的Canvas中避免任何UI变动都触发整个Canvas的重建。2.2 Overdraw与填充率GPU侧的隐形杀手填充率Fill Rate决定GPU每秒能处理多少像素。移动端GPU虽然在小Tile内计算效率不错但总填充率远低于桌面GPU。而Overdraw也就是同一个像素被重复绘制的次数直接放大填充率的压力。怎么量化Unity提供了Overdraw视图Scene窗口右上角的下拉菜单里切到Overdraw模式画面越亮代表该区域绘制次数越多。移动端项目我一般要求平均Overdraw控制在2到3倍以内全屏特效和半透明粒子密集的区域可以局部超过但不能全屏持续高Overdraw。典型案例包括大范围粒子特效每个粒子都是半透明Quad、多层全屏雾效、带大量半透明材质的UI弹窗叠加、以及传统渲染里给所有物体都加上的额外Pass的描边效果。这些在PC上可能是视觉亮点在移动端直接把GPU按在地上摩擦。替代方案有很多粒子改用Mesh实例化减少数量、用Shader做屏幕空间特效替代多层叠加、UI减少半透明层级、雾效用烘焙代替实时计算。2.3 Shader复杂度ALU指令与纹理采样的双重压力同一个Shader在PC高端显卡上可能跑0.1ms在中端手机上能跑2ms。为什么因为移动GPU的ALU单元数量有限指令吞吐低而且对分支、循环、动态索引这类操作极为敏感。我在项目中定过一个不成文的规定移动端Shader的数学运算要“能省则省”避免在片元着色器里做复杂计算——比如把能在顶点着色器完成的光照计算放到顶点阶段能用一张查找表LUT代替的数学函数就用纹理采样。纹理采样次数也要重点关注。移动端片元着色器里每增加一次纹理采样都会显著增加带宽压力。常见做法是把多张纹理合并到一张图集或一张通道图里减少采样次数。比如PBR材质BaseMap、NormalMap、MaskMap三张纹理是标配但如果你用的URP Lit Shader默认光效计算已经做了大量优化在移动端的表现还算可以接受。真正需要警惕的是那些从PC项目带过来的“豪华版自定义Shader”——十几次采样、复杂的切线空间变换、多个动态分支这类Shader在移动端几乎必然出问题。另一个隐蔽问题是Shader变体数量。变体膨胀会导致打包体积变大和首次加载Shader时的卡顿。PC项目可能无所谓几千个变体移动端必须在Player Settings里做Shader Strip裁剪不需要的变体配合Variant Log看哪些变体真正被用到能砍掉一半以上的加载卡顿。2.4 动态合批的悖论为什么越优化越慢这里我要泼一盆冷水Unity的动态合批Dynamic Batching在移动端经常帮倒忙。它的原理是每帧把符合条件的动态小物体复制到同一个网格里提交以达到合批目的。但逐帧复制网格顶点数据本身是有CPU开销的物体越多复制成本越高。对于顶点数量较多、或物体频繁移动的场景动态合批的开销可能超过它节省的Draw Call开销。我实测过一个项目场景里有200个移动的小石块开了动态合批后CPU耗时比不开还高了30%Draw Call确实降了从300降到120但CPU单帧总耗时反而增加了。后来改成GPU InstancingDraw Call降到个位数CPU耗时也降了。所以在移动端优先考虑SRP Batcher和GPU Instancing动态合批只在少量、低顶点数的物体上值得一用。这个取舍PC上几乎不用考虑因为PC CPU扛得住复制开销这就是“同样的策略在不同平台结果不同”的典型案例。3. 从PC到移动端一套可复用的性能优化实操流程3.1 第一步先定预算再谈优化拿到项目第一步不是瞎优化而是建立性能预算表。我在每个移动端项目启动时都会先和团队对齐一份“性能Budget”按目标帧率、目标机型分层给出硬性指标。下面是我常用的模板性能指标高端机如骁龙8系/A16中端机如骁龙7系/天玑8系低端机入门机目标帧率60 fps60 fps 或 45 fps30 fps单帧总耗时≤ 16.6 ms≤ 22 ms45帧或 16.6 ms≤ 33 msCPU主线程≤ 8 ms≤ 10 ms≤ 15 msGPU渲染线程≤ 8 ms≤ 10 ms≤ 15 msDraw Call≤ 400≤ 300≤ 200内存峰值根据具体机型不触发系统杀进程即可同上更保守同上更保守平均Overdraw≤ 3≤ 2.5≤ 2这套预算表的作用是让优化有明确靶点。比如中端机上Profiler显示GPU耗时12ms超出10ms预算就知道必须砍填充率和Shader复杂度CPU主线程9ms但预算10ms暂时可以放一放。预算制定后每次提测都要对照这个表评审不达标的版本不放行这是防止性能回退的最好机制。3.2 第二步Unity Profiler的移动端正确使用姿势很多开发者只在编辑器里开Profiler这是移动端性能分析最大的误区。编辑器里的性能数据参考价值极低——你的电脑CPU和GPU跟手机完全不同编辑器本身还有额外开销。想要真实数据必须在真机上Profile。Unity提供了几种方式。最基础的是Development Build模式在Build Settings里勾选Development Build和Autoconnect Profiler然后打包装到手机上通过Wi-Fi或USB连接到Unity Profiler。注意Wi-Fi连接会有网络延迟带来的数据抖动USB连接更稳定。如果做CPU热点分析建议用Profiler的CPU Usage模块里的Hierarchy视图可以看到每个函数的花费时间。对移动端我还喜欢用Deep Profile模式它能显示所有函数调用细节但开销很大只适合定位特定问题不能作为常规基准。GPU侧的分析Unity Profiler的Rendering模块只能看到大致的GPU耗时想看细粒度数据就得借助厂商工具高通平台用Snapdragon Profiler或Android GPU Inspector联发科用TuneriOS平台用Xcode自带的Instruments和Metal System Trace。以Android GPU Inspector为例它能看到每个Draw Call在GPU上的实际耗时、着色器占用、带宽使用情况能精确定位到底是哪个Pass在拖后腿。工具链虽然多但万变不离其宗CPU侧用Unity ProfilerGPU侧用厂商工具两边数据对齐才能判断瓶颈到底在哪一端。3.3 第三步按性价比排序的CPU侧优化清单Profiler打开后CPU侧的优化基本可以按一个固定套路来。先说收益最大的三件事。第一降低Draw Call用SRP Batcher和GPU Instancing替代默认合批。URP项目里把Project Settings的Graphics里勾选SRP Batcher然后确保Shader是兼容的Lit、PBR等内置Shader都兼容自定义Shader需要写SRP Batcher兼容代码。这一步在很多项目里能让CPU耗时直接降30%到40%是移动端性价比最高的操作。第二清理GC Alloc垃圾回收分配。Unity的Mono垃圾回收机制在移动端特别敏感原因是内存碎片化和小内存块分配会导致GC频率上升而GC触发时通常会造成显著的帧率尖峰——有时候一帧掉一半时间在GC上。打开Profiler的Memory模块按GC Alloc排序定位那些每帧都在分配内存的函数。常见元凶包括字符串拼接用到$字符串插值、LINQ查询、lambda表达式、迭代器yield return、GetComponent运行时查找等。处理办法很粗暴把每帧执行的字符串操作改为预分配StringBuilder把LINQ改为手写循环把频繁GetComponent的结果缓存起来。一个正常的移动端项目稳定帧率下每帧GC Alloc应该控制在1KB以内如果看到几十KB那基本就是卡顿尖峰的来源。第三处理代码里的低效逻辑。比如Update里反复读取PlayerPrefs、每帧FindObjectOfType、每帧实例化和销毁对象这些在PC上都是“不痛不痒”的操作在移动端CPU上就是实打实的开销。用对象池替代频繁Instantiate/Destroy用事件驱动替代每帧轮询用预计算的静态数据替代运行时启动时的重复计算都属于这一类的标准操作。3.4 第四步GPU侧优化——从Shader到后处理的降级方案GPU侧的优化核心思路就一句话减少像素工作量和带宽消耗。先说分辨率与缩放。移动端很多机型实际渲染分辨率低于屏幕分辨率这是通过动态分辨率系统实现的。URP自带Scalable Ambient Obscurance不对是URP里可以直接设置Render Scale把渲染分辨率降到屏幕分辨率的70%到80%肉眼几乎看不出画质损失但GPU负载能降一半左右。很多商业手游都这么做——高负载场景动态降分辨率负载降低后再恢复。Unity的URP里可以运行时修改Render Scale我建议在项目里做一个自适应的降分辨率逻辑当GPU耗时超过预算时逐步降低Render ScaleGPU耗时回落后再逐步恢复。再说后处理。Bloom、Depth of Field、Screen Space Reflections这些特效在PC上开满无压力在移动端每一个都是GPU杀手。URP默认支持的后处理Volume可以在移动端降级Bloom的散射采样降到最低档DOF直接关掉SSR换成Planar Reflection或干脆不做。还有抗锯齿MSAA在移动端带宽开销很大建议在URP里直接用FXAA画质损失能接受性能友好得多。然后是阴影。实时阴影是移动端最大的GPU负担之一。PC项目里一个高质量Directional Light贴满全屏阴影移动端如果不降级帧率至少掉三分之一。我的做法是移动端阴影分辨率从PC的4096降到2048或1024阴影距离从200米压缩到50米以内阴影级联从4级降到2级甚至主光源之外的方向光就不开阴影。极端情况下用Projector或Mesh圆盘伪造软阴影能省下大量GPU时间。3.5 第五步资源管线的移动端适配最后这步很多人会漏掉纹理压缩和音频格式。移动端必须用ASTCAndroid和iOS新机型都支持或ETC2兼容性更好但色彩精度略低不要再使用PC常用的DXT格式更不能用未压缩的RGBA。我接手过一个项目美术资源全都按PC习惯导成了RGBA32的TGA打包出来安装包高达2GB加载一个场景要卡好几秒换成ASTC后包体直接降到800MB场景加载卡顿也大幅好转。音频方面移动端尽量用Vorbis格式Android和MP3/AACiOS压缩格式长音乐用Streaming类型短音效用Decompress On Load。不要全都用Uncompressed否则内存和加载时间都会爆掉。资源加载方式上用AssetBundle按需加载配合Addressables做依赖管理和内存释放千万不要一个场景把所有资源都打包进内存。PC上内存够大无所谓手机内存分分钟教做人。4. 移动端帧率问题排查清单与实战记录4.1 一张可打印的排查速查表这些年在项目里踩过的坑我整理成一张速查表。真机上如果帧率不稳先对着表定位效率远高于漫无目的地调参数。表象可能原因排查工具优先处理手段帧率普遍性偏低Profiler里GPU时间高填充率/Overdraw过高、Shader太复杂Overdraw视图、Android GPU Inspector降Overdraw、换轻量Shader、降Render Scale帧率普遍性偏低Profiler里CPU时间高Draw Call过多、逻辑代码低效Unity Profiler CPU模块SRP Batcher、合批、缓存查找结果帧率周期性尖峰每隔几十帧卡一次GC触发、加载资源、AssetBundle解压Profiler Memory模块、全帧录制消除GC Alloc、预加载、资源异步加载特定画面/特效出现时掉帧粒子Overdraw、阴影密度、后处理Frame Debugger、Overdraw视图减少粒子数量/半透明层级、降阴影质量UI打开或滚动时卡顿Canvas重建、图集撕裂、动态元素过多Profiler的UI模块、Frame DebuggerUI分层、图集整理、减少动态元素长时间游戏后帧率逐步下降内存泄漏、内存碎片、温控降频Memory Profiler、设备温控日志排查泄漏、降低峰值内存、降低负载留余量特定机型卡顿、其他机型正常机型特定的驱动/架构问题Mali vs Adreno厂商GPU工具、真机全覆盖测试针对该GPU架构做Shader优化或特效降级4.2 一个真实的排查案例全屏特效为何拖垮中端机说一个我印象很深的实战案例。项目里有一个剧情演出屏幕上有一段全屏粒子特效类似能量爆发加上一层全屏Bloom。PC上一切正常帧率稳定120帧。测试中端机时这段演出帧率掉到20帧卡得一塌糊涂。先用Unity Profiler的Rendering模块看GPU时间飙到40ms。再用Overdraw视图一开好家伙整个屏幕在演出期间都是白茫茫——说明这个区域的平均Overdraw至少达到了8倍。粒子特效本身有大量半透明Quad叠加Bloom后处理又对整个屏幕做多次高斯模糊采样等于在每个像素上做了几十次读写。定位后做了三件事第一粒子数量从8000砍到3000粒子大小缩小30%同时把粒子材质从半透明改为带Alpha Blend但合并成更少图集的方式让层叠变少第二Bloom在剧情演出期间降低档位从高精度降到低精度加双线性采样第三全屏特效区域用屏幕空间Mesh替代粒子叠加把特效收敛到一个矩形区域内避免全屏Overdraw。改完中端机帧率回到45帧以上画面观感几乎没有区别。这个案例的关键是在PC上8倍Overdraw加全屏Bloom根本不构成压力所以问题永远不会暴露一到移动端带宽和填充率的短板立刻现形。4.3 容易被忽略的“非渲染”卡顿源排查卡顿不能只盯着渲染。我至少三次被同一个问题坑过UI的ScrollRect在移动端滑动时掉帧。用Profiler看CPU时间一大半花在Canvas.SendWillRenderCanvases上——每次滑动都会触发整个Canvas的网格重建。解决方法是把可滚动内容单独放在一个Canvas下背景和静态元素放另一个Canvas滑动的Canvas用RectMask2D裁剪再加上对象池复用Item。这一套下来UI滑动从50ms的尖峰降到2ms以内。另一个非渲染卡顿源是物理系统。移动端上如果开了大量Collider并启用Continuous Collision Detection物理引擎的耗时会被拉满。还有Animator——每帧更新大量骨骼动画对移动CPU同样是不小的开销建议用GPU Skinning或者减少同时播放的动画数量。最后别忘了Application.targetFrameRate很多包在PC上不设无所谓但手机上如果不显式设置一些机型会疯狂跑满帧率导致发热和降频反而适得其反。移动端标准的做法是在启动时按机型设定目标帧率比如中端机锁45帧高端机锁60帧并配合动态分辨率保证持续稳定。4.4 优化验收不能只看平均帧率最后提一个验收层面的经验。移动端优化的验收不能光看平均帧率。我的做法是看三组数据第99%帧耗时P99、帧率方差、以及持续运行10分钟后的帧率衰减曲线。平均帧率60fps的版本如果P99飙到200ms玩家实际体验依旧是卡顿不断。我会用Unity的Profiler采集一局完整游戏的数据导出后在Excel里画出P50/P95/P99三根线任何一根超出预算线都需要继续优化。持续运行测试也不能省——把游戏挂机跑20分钟记录帧率曲线如果后半程帧率明显低于前半程基本就是温控降频或内存泄漏这两个问题都要在发布前解决。另外任何优化修改都要做A/B对比。改之前记录基准数据CPU/GPU耗时、Draw Call、内存改之后再记录一次看每一项的变化是否符合预期。比如你为了降Draw Call引入了GPU Instancing结果发现CPU降了但GPU的纹理带宽上来了这种“拆东墙补西墙”的情况要当场发现别等QA提测了才看到。5. 移动端优化的后续扩展方向这个系列聊到这里移动端和PC的性能差异根源算是梳理清楚了。但优化这件事没有终点后续还可以在几个方向继续深挖一是URP与SRP Batcher的配合优化把自定义Shader全部改成兼容SRP Batcher的写法二是Addressables与AssetBundle的加载性能优化把资源加载的卡顿尖峰彻底抹平三是Job System与Burst Compiler把复杂逻辑从主线程挪到工作线程释放CPU压力。这些方向我都准备在后续文章里逐一展开每篇都会配真实项目和Profiler数据。如果你也在移动端Unity项目里被帧率问题折磨欢迎在评论区聊聊你遇到的瓶颈也许下一个案例就能帮到更多人。根据我个人的经验移动端优化的核心从来不是某个单点技巧而是建立一套“预算-监控-定位-优化-回归”的闭环习惯把这套习惯跑起来帧率保卫战才真正有了章法。
返回列表