ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Unity游戏发热原因与优化:从CPU、GPU到DrawCall

2026/9/5 15:25:51 拓冰建站 浏览量
Unity游戏发热原因与优化:从CPU、GPU到DrawCall 1. 先说结论烫的不是手机是你的游戏逻辑做 Unity 开发的朋友应该都有过这种体验开发机上跑得好好的一装到真机上玩个三五分钟手机背面就开始发烫。你要是开的还是高画质 高帧率那妥妥可以当暖手宝。遇到这种情况很多人第一反应是“手机不行”但说实话大部分时候锅不在手机而在咱们自己写的项目上。我先给一个大部分项目都适用的判断基准正常 3D 手游在跑的时候机身温度应该保持温热但你拿得住而不是烫得想立刻放下。手持设备表面温度超过 45℃ 就已经开始影响体感超过 50℃ 就算严重发热。如果你的游戏“越玩越烫”尤其是持续玩 10 分钟以上温度还在往上走那基本可以断定项目里有什么东西在持续消耗资源它不该存在或者它不该这么干。这篇是系列第 1 篇我先不急着堆优化技巧而是先把一条主线讲清楚——发热到底是什么引起的以及你怎么从项目里去定位那些把手机变成暖手宝的元凶。很多人一上来就搜“Unity 游戏优化”的帖子跟着改几个参数结果发现没多大用原因就是不知道到底哪里在烧电。我们先把问题拆开发热的本质是功耗功耗的本质是在人眼看不到的时间片里CPU、GPU、内存带宽这三样东西有活儿在干。活儿多功耗自然高温度自然往上爬。所以思路就一句话你想让手机凉下来就得让 CPU 和 GPU 干更少的活或者让它们干同样多的活但干得更高效。我见过太多项目第一步就跑偏了——跑去调画质、降分辨率结果问题根本不在 GPU而在 CPU 上的某个死循环逻辑。所以咱们这篇的重点不是教你怎么改某项参数而是先学会怎么判断问题出在哪一层。在开始动任何一个优化手段之前你要先做一件事把 Project Settings 里的Development Build勾上然后用 Profiler 连真机跑一遍你的游戏。我后面会专门讲怎么去读 Profiler 的数据但你现在至少要建立一个概念没有性能数据支撑的优化都是盲人摸象。2. 搞懂发热的底层逻辑CPU、GPU 与功耗的关系2.1 手机为什么热从能量消耗讲起很多人一说到手机发热第一反应就是“CPU 占用率太高”。这个理解大方向是对的但不完整。手机发热的本质是整机功耗高而功耗由几个大件共同决定SoC 里的 CPU 核心、GPU、内存控制器、Modem射频模块、屏幕背光以及各类传感器。对游戏来说前三样是绝对的大头。拿一颗常见的手机 SoC 举个例子它的 CPU 大核比如 A710 或 X2 架构在满频运行时单核功耗能飙到 4~5WGPU 在满负载下更是能跑到 6~8W。要知道一台手机正常亮屏待机的整机功耗也就 1~2W。你算算如果游戏让 CPU 和 GPU 全速跑整机功耗直接翻好几倍热量怎么可能不堆积起来。这里有个比较反常理的点主板上的发热大户往往不是 CPU而是 GPU。原因很简单移动端游戏画面的像素填充、着色计算、纹理采样几乎全压在 GPU 上。而 GPU 一旦长时间处于高负载发热是非常可观的。所以你在 Profiler 里看到 CPU 占用率只有 30%觉得“不高啊”但手机依然烫那大概率是 GPU 的 load 已经爆了。2.2 Unity 的帧循环一切问题的集中营Unity 引擎的核心是一个死循环顺序大概是处理输入 → 调用 Update → 物理模拟 → 渲染 → 提交命令到最后 GPU 执行。这个循环会在每一帧里不断重复。你要明白一件事Unity 的 CPU 逻辑和 GPU 渲染是并行流水线的关系但它们之间需要同步点也就是“等待显卡完成当前帧的命令”。这个等待很容易导致一种假象CPU 看着占用率不高但 fps 就是上不去。因为线程在等 GPU卡在了同步点上。反过来如果 CPU 侧的脚本逻辑跑得慢GPU 早就把活干完了就会出现“GPU 空转”这时候功耗虽然没那么高但画面照样卡。移动端发热的典型路径常见有两种CPU 侧一些脚本逻辑在 Update 里反复跑高开销操作比如 GetComponent、字符串拼接、频繁实例化对象每秒执行几十次甚至几百次CPU 被拖到高负载。GPU 侧画面里塞了大量半透明物体、动态阴影、没有合批的小物件导致每帧的渲染指令数爆炸GPU 满载发热。真正麻烦的是第三种CPU 和 GPU 同时都在高负载状态下。这时候手机基本上就是一个小烤炉掉电速度肉眼可见。2.3 耗电 发热别再把它们分开看有个概念需要先建立电量消耗和发热是同一个物理过程的两个面。功耗高了电池输出电流大内部电阻发热再加上 SoC 本身的发热整机温度就起来了。所以优化游戏发热本质上是优化游戏功耗。功耗从哪里来一半来自 CPU 周期一半来自 GPU 周期。CPU 周期贵的操作包括代码里频繁的类型转换、反射调用、List 扩容、GC 分配和回收。GPU 周期贵的操作包括过多的 DrawCall、过大的 overdraw一个像素被画了多次、无压缩的纹理导致的带宽浪费、全屏后处理叠加。这些术语听起来多但落到实际项目里就对应几个非常具体的场景我后面会逐个拆。3. 热源第一梯队DrawCall、Overdraw 与渲染管线的代价3.1 DrawCall 数量为什么会成为发热大户先给个定性结论如果你在真机上看到 DrawCall 数量老是维持在 500 以上手机的 CPU 侧已经开始吃紧GPU 也会因为频繁命令切换发热。这个数值在 PC 上根本不值一提但在移动端每一次 DrawCall 都伴随着 CPU 侧调用图形 API 的开销以及 GPU 侧切换渲染状态的成本。讲个真实例子。我之前接手过一个 DEMO场景里放了几百个模型单个模型的面数都不高但材质球完全不共用。这个项目在编辑器里看流畅得很一上骁龙中端芯片的真机掉帧加发热电池像漏了一样。用 Profiler 一抓好家伙每帧 1500 多个 DrawCall。为什么会导致发热因为每一次 DrawCall 都不是白白执行的。从 CPU 的角度看它要把顶点数据、纹理、材质参数打包成 GPU 能识别的命令然后塞进命令缓冲区从 GPU 的角度看它每切换一次渲染状态比如切纹理、切着色器内部管线就要 flush 一次缓存。当设备上有大量状态切换时GPU 的利用率反而下降但是功耗上升了——因为它必须在更短的时间内处理更多的状态切换指令。优化方向也很清晰把 DrawCall 的数量压下去。比如让多个物体共用同一份材质、同一张纹理图集这样 Unity 就能把它们合批成一次 DrawCall。再比如启用静态合批或 GPU Instancing前者适合不会动的场景物体后者适合大量同名同材质的动态物体。值得专门强调的是很多人压 DrawCall 会直接跑到网上抄一个“把全部物体合批”的配置结果发现物体是合并了但出现问题——物体位置对不上、灯光效果错乱、内存占用暴涨。DrawCall 不是越低越好而是要在场景复杂度、内存占用和渲染状态切换三者之间取一个平衡。3.2 Overdraw每个像素被重复绘制了几次我在移动平台调试时经常打开一个选项叫Overdraw 视图。它会用颜色标识屏幕上每个区域被绘制的次数绿色表示只画了 1~2 次红色表示被重复绘制了 5 次以上。正常情况下一张 1080p 的屏幕有大约 200 万个像素。如果某个区域被画了 5 遍就相当于真实渲染了 1000 万个像素的着色工作量。GPU 的 Fillrate填充率就那么多多画的部分全是白耗电。最容易造成 Overdraw 的几种情况我都踩过半透明粒子系统叠了太多层比如技能特效里三层雾、五层光晕叠在一起。UI 界面上大量使用了带透明度的图片一层叠一层尤其是全屏的暗色遮罩后面还叠了一堆图。场景里摆了很多大的半透明面片比如假烟雾、体积光模拟贴片从相机看过去全是重叠的。要查 Overdraw 很简单Scene 视图的右上角切到Overdraw模式你会立刻看到一片一片的红色区域。优先处理那些红色最密集的屏幕区域尤其是 UI 界面和使用半透明特效的技能演出。把不必要的层删掉把透明改成不透明注意图片格式Overdraw 就能降一截。3.3 动态合批与静态合批的正确用法合批这个词网上一搜一大把但真正能讲清楚什么时候用哪种的人不多。静态合批的本质是在构建时把多个静态物体的网格合并成一个大网格这样运行时只需要一次 DrawCall。它对场景中不动的墙体、地面、装饰物非常管用。代价是内存占用上升因为合并后的网格是单独存一份的原来独立的小网格也在内存直接翻倍。所以静态合批的开和关要按场景内存预算来权衡。动态合批则是引擎在每一帧运行时把符合条件的多个动态物体临时拼成一个批次前提是它们使用同一个材质、网格顶点数不能超过上限Unity 早期版本是 900 顶点后续有调整。如果不符合条件合批失败引擎反而会因为合批判定逻辑产生额外 CPU 开销得不偿失。我的建议不要把合批当成“开了就省”的选项而是先在 Profiler 的Rendering标签页里看下当前帧的 Batches 数量再对照场景里的物件构成去判断值不值得开。还有一点同一材质球才能合批这是王道。想让大量的物体合批就要合理复用材质而不是每个物体都 new 一个材质的实例。4. 热源第二梯队UI 与脚本的隐藏开销4.1 UI 重建与动静分离UIPanel 的隐形负担Unity 的老牌 UI 系统UGUI用起来很顺手但它有一个非常经典的性能陷阱只要某个 UI 元素的任何属性发生变化系统就可能触发一次或多次网格重建。网格重建的逻辑很简单——它要把可见的 UI 元素重新生成顶点数据然后重新提交给 GPU。这里有一个非常关键但新手不怎么知道的概念Canvas 之间的动静分离。每个 Canvas 下的所有 UI 元素共享一个渲染批次只要 Canvas 下面任意一个元素把它惹毛了比如动态改变了位置、尺寸、文字内容这个 Canvas 下的所有元素都会参与重建。所以你要做的第一件事是去检查 UI 层级的布局把会频繁变化的元素比如飘字、冷却图标、滚动列表单独放到一个 Canvas 里让那块区域的重建不至于带动整个 UI。具体做法我再说细一点想象你有一个 HUD 界面背景是固定的血条边框中间是实时变化的血量数字右下角是一个正在转圈的技能 CD 图标。如果这三样东西都在同一个 Canvas 下那么每一帧血量数字在变整个 HUD 都要重建。一旦把它拆成三个 Canvas数字变了只重建数字所在的那块背景和 CD 图标完全不受影响。这一步操作简单但收益立竿见影。另外我还想提一个大家在用 UGUI 时经常忽略的问题文字导致了大部分 UI 性能问题。TextMeshPro 的字符数据结构和网格信息比老版字体系统复杂得多再加上字体动态生成图集、阴影、描边每个文字都是一个微型渲染单元。如果你的 HUD 里有大段实时刷新的日志文本比如每秒更新 10 次的那个调试输出框相信我把它取消勾选或者移除温度立刻下去。4.2 Update 里的“定时炸弹”与主线程阻塞很多项目里的脚本写的时候图省事把所有能跑的全都放进了 Update()。Update 每帧都会执行60fps 下就是每秒执行 60 次这本来没什么问题但如果你在里面写了高开销操作就成了定时炸弹。来几个我真实见到的反面写法在 Update 里调 FindObjectOfType这玩意儿的实现原理是全局遍历场景里所有符合条件的对象Find 一次可能要遍历几百个 GameObejct在 Update 里叠加字符串比如把玩家名字 血量 伤害值拼成一个日志字符串会产生大量 GC 垃圾导致频繁触发垃圾回收CPU 卡顿在 Update 里 new 一个 List 或者 Dictionary每一帧都在堆内存上分配空间GC 压力巨大。如果这些代码里的任何一段出现在你的项目里那么它每帧都在做无用功。这种无用功的代价不是瞬间的掉帧而是持续的 CPU 占用CPU 占用高功耗就高温度就上去了。正确的做法是把这些每帧重复的调用抽出来。不是在 Update 里找物体而是在 Start 或 Awake 里缓存引用不是用字符串拼接来做状态显示而是用 StringBuilder 或者直接拼 Int 转字符串的缓存不是每帧 new 容器而是提前定义好容器并在每帧之前 Clear 后复用。另外一个经常被忽略的点协程Coroutine并不等于异步线程。协程依然运行在主线程里它只是在主线程的时间片上被切成了多个片段执行。如果你在协程里写了死循环或者长时间的密集计算主线程一样被卡死。某些项目里为了做延迟一个协程里套着 while(true) 然后里面跑复杂计算这在中低端机上就是灾难。5. 实操如何用 Profiler 定位发热元凶5.1 Profiler 连接真机的基本操作废话不多说直接上操作。用 Profiler 挂真机是定位性能瓶颈的必备技能。步骤非常简单但我见过很多人在第一步就栽了——连不上设备。打开 Edit → Project Settings → Player在 Other Settings 里把Development Build和Autoconnect Profiler勾上然后 Build 一次工程装到手机上。接着用数据线连着电脑在 Unity 里打开 Window → Analysis → Profiler点右上角的Record旁边的下拉选择你的手机设备开始录制。如果设备列表里找不到手机检查一下有没有开启手机的 USB 调试Android或者是不是用了不支持的数据线。注意自动连接的 Profiler 会拖慢一些帧率性能数据会和最终发布版有差异但定位方向没问题。5.2 三张关键图表CPU Usage、Rendering、Memory连接成功后你会看到 Profiler 里一列一列的数据。我一般只盯着三个标签页看。第一个是CPU Usage。在这里你能看到每帧各个系统模块消耗了多长时间。我最先看的两项是Scripts玩家脚本和Rendering渲染。如果Scripts的耗时占比非常高超过 40%那优先怀疑代码逻辑的问题。如果Rendering特别高就去 GPU 侧找原因。第二个是Rendering标签。这里会明确列出每一帧的 Batches、Triangles、SetPassCalls 这些数据。Batches 是合批后的 DrawCall 数量Triangles 是三角形数量。假如 Batches 数量四五百以上Triangles 有几十万甚至上百万那大概率是渲染负载过高发热的锅在 GPU。第三个是Memory关注两个数字GfxDriver 和 ManagedHeap。GfxDriver 是显存相关的分配量ManagedHeap 是托管堆也就是脚本新分配的对象所在的空间。如果 ManagedHeap 在不断上涨并且触发了 GC界面会显示 GC Alloc 的尖峰说明脚本在持续分配垃圾对象频繁 GC 是 CPU 发热的常见元凶。我个人抓性能曲线的习惯是这样的先跑 3 分钟的游戏流程然后暂停回放找到最高的一帧逐个模块看谁最突出。如果那一帧刚好对应你操作最频繁的时刻比如放技能、切界面、开箱子说明这个操作本身有性能漏洞。5.3 用 Frame Debugger 审查每一帧Profiler 看到了宏观数据但你要知道具体是哪一步渲染最费钱就需要打开Window → Analysis → Frame Debugger。Frame Debugger 的原理很简单你可以逐条翻阅当前帧里 GPU 执行的每一步渲染命令看它的批次、使用的材质、渲染了哪些顶点。它就像一个逐帧的渲染命令审查器。这个工具最适合回答一个问题“为什么这一帧这么贵”你可以从上往下翻看渲染命令列表找到那些顶点数量特别大、或者被反复使用的半透明物体渲染项。看到某个怪物模型的渲染命令占了几万个顶点你自然就知道该去压它的面数还是禁用它的投影。我一般把 Profiler 和 Frame Debugger 搭配着用先用 Profiler 定位问题出现在 CPU 还是 GPU再用 Frame Debugger 具体到是哪一个物体或哪一步操作拖累了帧时间这样修复起来就有的放矢而不是瞎猜。6. 常规优化手段先别急着上高级技巧6.1 画质分级不是所有手机都能全特效“为什么开发机上不卡真机上卡”这个问题很多人问过。其实原因很简单开发机是一个高性能的台式机或顶配笔记本而你的真机是骁龙中端处理器 有限的散热结构。拿 PC 的水平去要求手机手机当然会烫。正确的做法是在你的项目里做画质分级。不是做不做的问题而是怎么做才不费劲的问题。最简单的分级方式是根据设备的平台和处理器能力在启动时选择一组预设的 QualitySettings 配置。Unity 的 Project Settings → Quality 里已经默认配了几档Low、Medium、High、Ultra。你可以按设备性能去匹配。比如中端机给 Medium 档关闭动态阴影、降低实时反射、调低 LOD 距离。关键点在于降到某一档之后画面观感不要有明显劣化。很多项目一刀切把阴影关了结果画面一片死白玩家反而更不满意。我的做法是分级时优先动硬件消耗大但人眼不敏感的选项抗锯齿降一档、实时阴影换成烘焙阴影贴图、后处理特效里砍掉景深或色差。这些对画面品质的影响相对小但省掉的硬件消耗非常大。6.2 贴图与内存压缩是王道但别无脑压移动端的贴图纹理格式和内存占用是一直被低估的发热源。一张 2048×2048 的 RGBA32 贴图在内存里占 16MB如果是多张贴图叠加内存占用立刻爆炸。内存大了内存控制器的负载就高功耗上升发热也随之而来。而且贴图带宽——GPU 每一次采样贴图都要走内存带宽带宽是移动端芯片的稀缺资源消耗越高GPU 功耗越大。移动端能选的纹理压缩格式很多ASTC、ETC2、ETC1 等等。我的建议是Android 优先用 ASTC如果 GPU 支持iOS 用 ASTC 也完全没问题。ASTC 在相同质量下的压缩比和画质表现都比较理想。有个常见的坑要提醒一下很多项目的 UI 图集没做压缩因为 UI 需要较高的清晰度但真机上 UI 图集占用的内存也不小。UI 图集通常可以压缩到显存友好的格式同时把“生成 Mipmap”关掉——UI 在屏幕上的缩放变化不大不需要 Mipmap 来抗闪烁保留 Mipmap 反而白白多占 33% 的内存。6.3 物理与碰撞别让引擎白跑Unity 的物理引擎在移动端是个隐形消耗大户尤其是大量碰撞体活跃在场景里的时候每一帧都要做碰撞检测和物理模拟CPU 占用蹭蹭上涨。我见过一个项目场景里散落了几百个空的碰撞体Collider它们根本不参与任何交互但物理引擎依然要每帧检测它们是否与其他物体碰撞。解决方案是给它们加一个角色标识或者干脆取消碰撞体的 IsTrigger 属性直接把不参与的碰撞体禁用SetActive false。物理优化的核心原则是把物理计算量降到必要的范围。不需要物理互动的物体不要挂 Rigidbody不需要精确碰撞的物体用一个大致匹配的 Box Collider 代替精贵的 Mesh Collider多层级的场景里分层碰撞检测Layer Collision Matrix能大幅减少无效检测次数。7. 实测排查流程按这个顺序走基本不会漏给大家一套我经常用的排查顺序跟着走能避开 90% 的坑。第一步建立基线。装一个 Release 包不带 Development Build 的版本把手机调成飞行模式连着电源同一场景同一操作流程记录帧率和手机温度。这一步的目的是获得一个可对比的基准没有这个基准后面任何优化都说不清是有效还是无效。第二步跑 Profiler。用 Development Build 连接 Profiler运行相同的流程分别记录 CPU Usage、Rendering、Memory 三个模块的数据。目标是找出哪个模块占用率最高。第三步分类施策。如果 CPU 的 Scripts 占比最高去优化代码逻辑缓存、合批、GC如果 Rendering 的 Batches 和 Triangless 很高去做场景和材质的合并、压缩、降级如果 Memory 的 GC Alloc 持续上涨去排查脚本里高频率的临时对象分配。第四步验证优化效果。改完以后不要只看编辑器里不卡一定要重新打 Release 包装到真机上再跑一遍相同的流程。对比帧率和温度的基线如果温度降低了 3~5℃ 或者帧率稳定性明显上升说明方向是对的。这四步每一步都很重要。我见过太多开发者在没有基线、没有数据的情况下随手改了几个参数折腾一天也不知道有没有效果。有了这套流程你的优化效率会高很多。8. 系列预告和一点个人的真心话这是“为什么你开发的 Unity 游戏越玩越烫”系列的第 1 篇先把发热的原因和整体排查思路讲清楚了。我在实际项目里看到过太多的“瞎优化”——不知道问题在哪就忙着上各种插件、改各种参数结果往往事倍功半。这一篇的内容对想入行或刚入行 Unity 开发的初学者来说最重要的不是记住那些具体参数而是学会一套看问题的方法发热 → 功耗 → CPU/GPU 负载 → 具体开销来源 → 针对性优化。这个思路能贯穿你整个 Unity 开发生涯。后续的系列文章里我打算针对几个高频热点专门展开Assects 资源加载到底该怎么管理、UI 系统的终极优化手法、粒子特效和 Shader 的移动端最佳实践、新版本 Render Pipeline 选型取舍、以及 Addressables 加载和资源释放的深度拆解。我个人在里面最偏好的优化手段永远是把大的问题拆成小的、可量化的单元去逐一解决。等你把渲染负载、脚本开销、内存占用和 UI 重建这几座大山都平掉了你会发现手机温度自然而然就下来了而且不用付出画质上的妥协。这就是用心和技巧之间的差别。