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

资讯详情

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

Unity手游“越玩越烫”排查:发热与性能劣化的定位方法

Unity手游“越玩越烫”排查:发热与性能劣化的定位方法 打一局《原神》手机端三分钟后背面发烫帧率从60掉到40把自己开发的Unity Demo装到真机上滑动两分钟机身温度明显上升——这两件事的体感其实是一回事但如果把“烫”这个字拆开看背后藏的东西完全不同。做Unity开发的人大概率都经历过这样的场景电脑上跑得丝滑流畅的游戏一到真机就成了暖手宝或者刚开始玩完全不卡越往后帧率越低最后烫得根本拿不住手机。用户只会说“这游戏有问题发热太严重”但作为开发者我们必须搞清楚一个最核心的问题发热是性能消耗的物理结果还是性能持续劣化的信号这篇文章是系列第1篇我打算先把“越玩越烫”这件事的排查方法论讲清楚。我会从“烫”的本质开始拆解发热和性能劣化的关系然后给出完整的测量链路最后整理一份可以照着执行的检查清单。不堆理论全部是我在实际项目里踩过坑、验证过的方法。1. “越玩越烫”不等于电量焦虑先分清两类发热场景在动手优化之前必须先建立一个大前提不是所有发热都需要优化。1.1 高压场景发热 VS 持续劣化发热我习惯把Unity手游的发热问题分成两类两者的处理思路完全不一样。第一类是高压场景发热。比如进入战斗关卡、切换大型场景、播放高精度特效这些瞬间CPU和GPU负载陡增发热是物理定律决定的你的优化目标是降低峰值负载、缩短高负载持续时间而不是消灭发热本身。这类发热的特征是温度升得快但离开高压场景后能降下来帧率虽然掉但能恢复。第二类是持续劣化发热。这才是真正需要警惕的问题也是本系列文章要重点解决的对象。它的典型特征是设备刚开机跑游戏时温度正常但越玩越热帧率持续走低甚至卡顿越来越频繁。即使回到低负载场景温度也降不回去。这种“越玩越烫”指向一个结论游戏运行过程中有资源在被持续消耗但没有被及时释放。我举个生活化的例子。高压场景发热就像你全速跑一百米气喘吁吁是正常的歇一会儿就恢复了。持续劣化发热就像你的身体带着一个缓慢漏气的气球在跑步一开始没感觉但气漏得越来越多你越来越累最后哪怕停下来身体也在持续消耗能量。这个“漏气的气球”在Unity游戏里就是那些没被正确回收的资源。1.2 为什么“越玩越烫”比“闪退”更隐蔽闪退很粗暴出了问题立刻弹窗用户骂一句你收到崩溃日志问题直观。但“越玩越烫”是慢性病用户很难给出准确的反馈节点——“玩了大概十几分钟开始卡的”这种描述对定位问题几乎没有任何帮助。更麻烦的是发热会导致系统级降频。现在的移动设备都有完备的温控策略SoC温度到达阈值后系统会自动降低CPU/GPU频率来保护硬件。这意味着你看到的“帧率下降”很可能不是游戏逻辑的问题而是硬件被限频后的被动表现。如果不区分这一点你可能会花大量时间去优化渲染管线结果发现真正的问题出在内存泄漏导致的持续GC压力。这就引出一个关键认知在移动端发热、掉帧、卡顿是三个互相联动的指标必须先测清楚谁是因谁是果才能对症下药。2. 从“烫”反向定位帧耗时测量与观察的完整链路面对“越玩越烫”的问题不要一上来就瞎猜。先建立完整的测量链路用数据说话。2.1 第一步确认发热阶段有没有发生降频在接到“游戏越玩越烫”的反馈后我的第一反应不是打开Profiler抓帧而是先确认设备有没有降频。手头的Android设备可以通过adb shell查询CPU频率和温度# 持续监控CPU各核心的频率 adb shell while true; do cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_cur_freq; echo ---; sleep 2; done # 查看电池温度多数Android设备以毫摄氏度为单位 adb shell dumpsys battery | grep temperatureiOS设备没有这么直接的命令行接口但我通常用Xcode自带的Energy Log配合MetricKit来获取发热和CPU占用数据。Unity的Profiler里也能看到Thermal Warning相关的日志设备发热但系统还没强制降频时引擎会先收到thermal状态变化的回调。如果你在测试时发现发热节点前CPU频率4×A77大核跑满发热后频率被压到只剩一半同时帧率同步下降——恭喜你已经找到了“越玩越烫”的第一个实锤。降频本身不是bug但它是性能劣化的放大器。你优化的目标就是让系统没有降频的必要或者在降频前把负载降下来。2.2 第二步用Profiler抓取完整时间线上的帧耗时确认降频之后接下来要把Unity Profiler接上但直接开Profiler跑游戏是没有意义的因为Profiler自身有开销会干扰数据。我的做法是两类数据分开抓第一类是宏观数据用Unity Profiler的Player Log模式Development Build Autoconnect Profiler记录5分钟到10分钟的完整时间线重点看这几个曲线Frame Time曲线是否有持续上升的趋势Memory曲线Managed Heap Used是否只增不减GC Allocation曲线单位时间内的GC分配量是否稳定第二类是微观数据在发现Frame Time异常升高的时间点暂停并放大到单帧查看PlayerLoop中各System的开销占比。这里特别要注意脚本Update和渲染管线的耗时比例。我见过一个很典型的案例某休闲游戏玩到第8分钟左右开始卡顿微观数据一抓发现ScriptRunBehaviourUpdate的耗时从1ms涨到了8ms再进一步查看是一个每帧遍历怪物列表的Update逻辑而怪物的数量因为对象池泄漏从40个涨到了600个。这种问题在PC上几乎不会引起注意但在移动端就是持续劣化的直接推手。2.3 第三步区分“真卡顿”和“假卡顿”很多开发者看到帧率掉就以为是性能问题但实际上一部分掉帧是“假”的——不是游戏逻辑变慢而是热降频后的被动表现。怎么区分很简单看Frame Time曲线和温度曲线的相位关系。如果是热降频Frame Time的上升通常滞后于温度上升而且所有核心的频率是整体下降的。如果是游戏内资源泄漏Frame Time的上升往往先于温度上升因为负载变高导致热量累积。这个顺序很重要。如果发现“先卡后热”说明游戏逻辑已经出了问题如果发现“先热后卡”说明是设备温控策略触发你要关注的是如何降低整体功耗。3. 越玩越烫的核心原因排查三大表现对应三类问题测量做完数据摆出来接下来要回答的是“为什么”。在Unity手游里“越玩越烫”的原因高度集中在以下三类问题里我逐个展开讲。3.1 表现一Managed Heap只增不减——内存泄漏导致的GC压力这是Unity手游“越玩越烫”排名第一的原因。Unity的Mono和IL2CPP环境都使用增量化GC但GC的触发是需要开销的。当你的Managed Heap不断增长GC触发时会扫描越来越多的对象STWStop The World时间越来越长单帧耗时被拉高。同时GC频繁执行会占用CPU时间导致整体功耗上升热量累积。我见过最夸张的一个案例某个放置类游戏玩家每小时点一次收集按钮结果每次点击都会通过UnityWebRequest下载几个图标并实例化到场景里。开发时没人发现上线后玩家反馈“玩两个小时手机像暖水袋”。一查ProfilerManaged Heap从启动时的20MB涨到两小时后的400MBGC Alloc曲线每隔几分钟就出现一个巨大的尖峰。这里要特别提醒一个新手容易踩的坑Managed Heap在Profiler里只增不减不代表你的代码有泄漏。Unity的Mono分配器倾向于不归还内存——它只会在内存不足时才真正释放页给操作系统。所以判断泄漏的唯一标准是持续运行的场景中GC Alloc是否持续发生且总量是否不断增长。如果GC Alloc曲线稳定在低位即使Managed Heap看上去很大也未必需要过度担心。排查内存泄漏的工具链我推荐这个组合Unity Profiler的Memory模块看Managed Heap Used和GC Alloc曲线配合三张Memory快照启动时、运行5分钟、运行15分钟用Memory Profiler包对比堆对象数量和引用关系重点关注事件系统的监听器、静态集合、协程、资源引用。3.2 表现二场景内对象越积越多——对象池失效与静态引用长命第二种典型表现是场景中的GameObject数量随时间线性增长每一帧的Update调用数量越来越多CPU负载越来越大。这类问题的根源主要有两个**对象池没有正确实现归还逻辑。**比如从池中取对象时忘记注册到对应的逻辑列表导致对象返回池后依然在被Update遍历。**静态字段或单例持有已“回收”对象的强引用。**比如一个全局的事件管理器在某个对象被Destroy时没有注销其监听方法导致对象虽然被Disable了但依然被事件系统持有。Unity的C#侧只要存在强引用对象就不会被GC回收。这里给出一个简单有效的自查方法。在游戏运行一段时间后打开Hierarchy窗口看Active Objects数量再对比你预期中的对象总数。如果数值差异巨大多半有对象泄漏。更进一步写一个统计脚本周期性输出public static void LogObjectCount() { var allObjects GameObject.FindObjectsOfTypeGameObject(true); UnityEngine.Debug.Log($Total GameObjects: {allObjects.Length}); // 按名称分类统计方便快速定位异常对象 var groups allObjects.GroupBy(o o.name) .OrderByDescending(g g.Count()) .Take(20); foreach (var group in groups) { Debug.Log(${group.Key}: {group.Count()}); } }在真机上跑一段时间每5分钟调用一次然后看输出列表里哪些对象数量在持续增长。这个方法不高级但极其有效我几乎每次排查“越玩越烫”都靠它初步定重点。3.3 表现三GPU负载逐帧攀升——UI过度重建与Shader分支第三种容易被忽略的情况是脚本侧看起来一切正常CPU占用稳定但GPU负载持续走高。这通常是两件事导致的其一UI的Canvas重建开销持续累积。Unity的UGUI在内容变化时会重新生成顶点和批处理数据如果游戏里有高频刷新的UI元素比如每秒更新一次的战斗飘字、计时器文本且它们都被放在同一个Canvas下会导致整个Canvas持续重建。打开Profiler的Rendering模块如果Canvas.SendWillRenderCanvases耗时异常且不稳定就重点查UI。其二Shader的运行时开销被放大。有些Shader在编辑器里看不出差别但到了移动端GPU上会出现严重的分支发散或贴图采样性能下降。比如在Fragment Shader里写了依赖动态变量的if branchGPU无法进行有效的并行执行预测效率会成倍下降。针对UI重建我能给的最直接建议是把常变和常静的UI拆到不同Canvas高频变动的元素尽量用RawImage替代Text或者用TextMeshPro的Dirty Flag控制。针对Shader问题用Unity的Frame Debugger逐Draw Call检查重点关注那些在运行过程中参数变化特别频繁的材质——这通常是GPU负载升高的元凶。3.4 额外需要注意的坑资源加载/卸载不彻底很多“越玩越烫”的情况不单独归因于以上三类而是资源泄漏。这是AssetBundle时代的老大难问题在Addressables时代同样存在。如果你用Resources.Load加载资源却从不卸载或者用Addressables加载了Asset但忘记Release资产会一直驻留在内存里。这样的问题通常会联动表现一和表现二——资产对应的Texture、Mesh、Material都会在GPU上占资源GPU显存被挤爆后系统会频繁的进行内存交换性能随之劣化。排查方法很简单在Profiler的Memory模块下查看Asset相关条目如果运行一段时间后Texture或Mesh的数量还在上涨基本就是资源泄漏了。定位到具体资源ID再在代码里搜索加载入口基本能锁定问题。4. 一套能落地到项目的“越玩越烫”排查清单把上面所有内容整理成可以直接执行的清单。这份清单是我在项目里反复使用的也推荐给身边做Unity的同行。4.1 上线前做一次Soak Test浸泡测试“越玩越烫”这类问题最大的特点是时间累积性所以你必须在项目上线前安排专门的Soak Test而不是仅做功能测试或者打一局流程就走。Soak Test的操作要点使用低端真机或者中端Android机开启Profiler和日志输出模拟真实玩家的操作模式比如每30秒切一次场景、每5秒点一次UI按钮持续跑30分钟每5分钟记录一次帧率、温度、Managed Heap用量和GC Alloc总量测试结束后对比数据曲线看是否有持续上升的趋势我一般会把这个测试定时安排在每周五下午跑一次拿数据周一例会看结果。这个节奏让团队能及时发现引入的劣化问题而不是拖到版本发布后靠用户反馈去发现。4.2 日常开发中的三条硬性要求除了例行测试我还会在项目里立几条开发约定从源头减少“越玩越烫”的可能禁止在Update里直接new对象。改为对象池或预分配。如果在Profiler里看到GC Alloc尖峰出现在某个Update函数里review时直接打回。所有AddListener必须有对应的RemoveListener。这个要求在代码评审时逐行检查尤其关注静态事件和单例这是长命对象持短命对象引用的重灾区。UI文本变色、改值必须按需。不要每帧刷新相同内容的Text一次赋值前后先用比较再做修改。这三条不复杂但对整体性能提升非常明显。说实话大多数“越玩越烫”的项目问题并不出在复杂的算法或渲染技术上而是这些基础规范没有执行到位。4.3 如果问题已经存在按什么顺序去查我给一个我自己的排查顺序实测下来效率最高先确认热降频看/查温度曲线和CPU频率排除系统因素。再抓Managed Heap和GC Alloc曲线确认是否存在持续的内存增长。检查Hierarchy对象数量用我上面给的LogObjectCount脚本输出对象清单锁定异常对象。查UI重建频率看Canvas.SendWillRenderCanvases是否存在异常尖峰。最后查资源泄漏用Memory Profiler对比启动和中后期的资源占用快照。这个顺序的核心逻辑是先花最少的时间排除外部因素然后用最直观的数据锁定问题类型最后用工具深挖根因。不要一上来就开Memory Profiler跑快照对比那样耗时且容易淹没在海量数据里。4.4 关于XR设备的一个提醒现在Unity主要用于PICO等XR设备开发的同行也很多。XR项目里“越玩越烫”更明显——因为头显空间封闭散热条件比手机差同时双屏渲染的GPU压力本来就大。在XR项目里分辨率、渲染ScaleRenderScale和Mali/Adreno的GPU调优是“烫”的重灾区。排查逻辑和手机端完全一致但优化优先级要调整先保证帧率稳定重要到让人不眩晕再谈发热控制。而且XR设备上的性能预算比手机严苛得多几乎没有冗余空间。这些内容后续有时间我可以单独写一篇XP方面。本文先把手机端的排查思路打扎实这个框架在XR上一样适用。排查“越玩越烫”这个问题本质上就是在建立一套“数据驱动的性能观测体系”。我见过不少团队花大量时间调渲染参数、做热力图分析结果都没解决问题——因为没有先找准病因。从本文的方法入手先测再分最后定位大多数“慢热”型性能问题都能在两天内找到方向。下一篇我会专门拆一个具体的Case从数据异常到根因定位的完整过程到时候见。
返回列表