1. 发烫问题的排查思路与整体框架
设备发烫这件事,做过移动端或者桌面端性能优化的人应该都不陌生。尤其是 Unity 项目跑在手机上,玩个十分钟机身就开始温热,二十分钟后直接烫手,帧率跟着往下掉,电池肉眼可见地掉电。很多人第一反应是“渲染太重了”,然后拼命砍 Draw Call、降分辨率,结果发现温度还是下不来。问题出在哪?出在没有系统性地定位热源。
发烫的本质是能量转换。CPU、GPU、内存、电池、屏幕、射频模块,任何一个部件在高负载下工作都会把电能转化成热能。Unity 项目里,热源可能来自渲染管线、物理计算、脚本逻辑、GC 分配、网络通信、甚至是不合理的资源加载策略。你如果不做排查,只凭感觉去优化,大概率是在做无用功。
这篇内容要聊的就是一套完整的排查方法论:七大热源排查清单加上功耗测量实战。所谓七大热源,是我在实际项目中反复验证后总结出来的七个高频发热来源,覆盖了从 CPU 到 GPU、从脚本到渲染、从内存到 IO 的主要方向。功耗测量部分则会讲清楚怎么用工具量化功耗,怎么把“感觉烫”变成“数据烫”,从而精准定位问题。
这套方法适合谁?适合所有在做 Unity 性能优化的人——不管你是刚入行的新手,还是已经做过几个项目的老手。新手可以按清单逐项排查,老手可以拿它当检查表,避免遗漏。整个思路不依赖特定引擎版本,Unity 2020 到 Unity 2022 甚至更新的版本都适用,移动端和桌面端都能参考。
我踩过的坑是:早期做优化时只看 Profiler 的 CPU 曲线,发现主线程耗时不高就以为没问题,结果设备照样烫。后来才意识到,GPU 的负载、内存带宽的占用、甚至屏幕亮度这些因素,Profiler 默认视图根本看不全。所以这套清单的第一条原则就是:不要只盯着一个指标看。
2. 七大热源排查清单逐项拆解
2.1 热源一:CPU 主线程脚本逻辑
CPU 主线程是 Unity 项目里最容易出问题的地方。MonoBehaviour 的 Update、LateUpdate、FixedUpdate 里但凡有一点耗时操作,累积起来就是灾难。常见的发热元凶包括:每帧都在做 Find 查找、每帧都在 GetComponent、字符串拼接、LINQ 查询、频繁的 Debug.Log。
排查方法很直接:打开 Unity Profiler,切到 CPU Usage 视图,看主线程的耗时分布。重点看PlayerLoop下面的各个阶段,尤其是Update.ScriptRunBehaviourUpdate和FixedUpdate.ScriptRunBehaviourFixedUpdate。如果这两项占比超过 30%,基本可以确定脚本逻辑有问题。
我一般会按下面的顺序排查:
- 检查所有 Update 里有没有
GameObject.Find、FindObjectOfType这类调用。有的话,改成在 Awake 或 Start 里缓存引用。 - 检查有没有每帧
GetComponent<T>()。同样改成缓存。 - 检查字符串操作。
string + string在循环里会产生大量 GC,改成StringBuilder。 - 检查 LINQ。
Where、Select、OrderBy这些在 Update 里用就是自找麻烦,改成手写循环。 - 检查
Debug.Log。发布版本里一定要关掉,或者用条件编译包起来。
注意:Profiler 本身有开销,在真机上跑 Profiler 会让数据失真。建议用 Development Build 加 Autoconnect Profiler,或者用 Profiler 的 Deep Profile 模式只在定位问题时开,平时关掉。
2.2 热源二:GPU 渲染负载
GPU 发热通常比 CPU 更猛,因为 GPU 的功耗墙更高,一旦跑满,温度上升非常快。Unity 里 GPU 负载高的原因主要有几类:Overdraw 太严重、Shader 太复杂、分辨率太高、后处理堆太多、阴影质量过高。
Overdraw 是移动端 GPU 发热的头号杀手。简单说就是同一个像素被画了多次。UI 层叠、半透明特效、粒子系统,都是 Overdraw 的重灾区。排查 Overdraw 可以用 Scene 视图的 Overdraw 模式,或者用 RenderDoc 抓帧分析。
Shader 复杂度方面,重点看 Fragment Shader 的指令数。移动端上,一个像素着色器超过 50 条指令就要警惕了。标准着色器(Standard Shader)在移动端上开销很大,建议换成 Mobile 系列的 Shader,或者自己写轻量级的。
分辨率这块,很多人忽略了一个点:Unity 的Screen.SetResolution或者 Quality Settings 里的分辨率缩放,直接影响 GPU 的填充率压力。把渲染分辨率降到 0.8 倍,GPU 负载能降 30% 以上,肉眼几乎看不出区别。
阴影也是大头。实时阴影的 Shadow Map 渲染会额外跑一遍场景,如果阴影距离设得远、级联层数多,GPU 压力翻倍。移动端建议用硬阴影、单级联、短距离。
2.3 热源三:内存分配与 GC 压力
GC 本身不直接发热,但频繁 GC 会导致 CPU 峰值飙升,间接推高温度。Unity 的 Boehm GC 是 Stop-The-World 的,每次 GC 都会卡主线程。如果每帧都在分配堆内存,GC 就会频繁触发。
排查 GC 分配用 Profiler 的 Memory 视图,看GC Alloc那一列。理想情况下,每帧的 GC Alloc 应该是 0。如果看到几百字节甚至几 KB,就要找源头了。
常见的 GC 分配来源:
- 字符串拼接和格式化
- 装箱拆箱(比如把 int 传给 object 参数)
- 闭包和 lambda 捕获
- 数组和 List 的频繁创建
foreach遍历某些集合(老版本 Mono 的 foreach 会分配)
我一般会用Profiler.BeginSample和Profiler.EndSample把可疑代码段包起来,然后在 Profiler 里看具体是哪一段在分配。定位到之后,用对象池、缓存、结构体替换类等方式消除分配。
2.4 热源四:物理计算与碰撞检测
Unity 的物理引擎(PhysX 或 Box2D)在场景复杂时开销很大。刚体数量多、碰撞体形状复杂、碰撞检测频率高,都会让 CPU 的物理线程跑满。
排查物理开销,Profiler 里看Physics.Processing和Physics.Simulate的耗时。如果这两项占比高,就要优化物理场景了。
优化手段包括:
- 减少刚体数量,静态物体用 Static Collider,不要加 Rigidbody。
- 碰撞体尽量用基本形状(Box、Sphere、Capsule),Mesh Collider 开销大。
- 调整 Fixed Timestep,默认 0.02 秒(50Hz),如果不需要高精度物理,可以降到 0.033 秒(30Hz)。
- 用 Layer 和 Layer Mask 过滤碰撞检测,避免不必要的碰撞对。
- 开启
Physics.autoSimulation = false,手动调用Physics.Simulate,控制模拟频率。
2.5 热源五:资源加载与 IO 操作
资源加载本身不持续发热,但如果在主线程上同步加载大资源,会导致 CPU 峰值和 IO 阻塞,间接推高温度。尤其是Resources.Load和AssetBundle.LoadFromFile在主线程调用时,卡顿和发热都很明显。
排查方法:Profiler 里看Loading.UpdatePreloading和UnloadUnusedAssets的耗时。如果加载耗时超过 16ms(一帧的时间),就会造成卡顿。
优化建议:
- 大资源用异步加载
Resources.LoadAsync或AssetBundle.LoadFromFileAsync。 - 加载时机放在场景切换的 Loading 界面,不要在游戏进行中加载。
- 用 Addressables 系统管理资源,它自带引用计数和异步加载。
- 避免频繁
Resources.UnloadUnusedAssets,这个操作很重,建议在场景切换时调用一次。
2.6 热源六:网络通信与序列化
网络通信的发热主要来自频繁的数据包收发和序列化反序列化。如果每帧都在发网络请求,或者用 JSON 做序列化,CPU 开销会很高。
排查方法:看 Profiler 里Network相关的耗时,或者自己用Stopwatch打点。重点看有没有每帧发送的请求、有没有大包传输、序列化格式是不是低效。
优化建议:
- 网络请求合并,不要每帧发,改成定时发送或事件驱动。
- 用二进制序列化替代 JSON,比如 MessagePack、Protobuf。
- 大包分片传输,避免单帧处理大量数据。
- 用对象池复用网络包对象,减少 GC。
2.7 热源七:屏幕与设备硬件因素
这一条容易被忽略,但很关键。屏幕亮度、刷新率、设备温度墙,都会影响实际发热表现。高刷新率屏幕(120Hz)比 60Hz 功耗高不少,如果游戏锁 60 帧,就没必要让屏幕跑 120Hz。
排查方法:在真机上测试不同亮度、不同刷新率下的温度变化。用Application.targetFrameRate锁定帧率,用Screen.SetResolution控制分辨率。
另外,设备本身的散热设计差异很大。同样的项目,在散热好的手机上可能只是温热,在散热差的手机上就烫手。所以测试时要用多台设备对比,不要只看一台。
3. 功耗测量实战:从感觉到数据
3.1 为什么需要量化功耗
“感觉烫”是主观的,不同人手感不一样,环境温度也不一样。要做优化,必须把功耗变成可量化的数据。量化之后,你才能知道优化有没有效果,效果有多大。
功耗测量的核心指标有三个:电流、电压、功率。功率等于电流乘以电压。手机上一般用 mAh(毫安时)或者 mW(毫瓦)来衡量。桌面端可以用功率计直接读整机功耗。
3.2 移动端功耗测量方案
移动端测量功耗,最准确的方式是用硬件功率计,比如 Monsoon 或者类似的专业设备。但这类设备价格不低,个人开发者不一定有。退而求其次,可以用软件方案。
方案一:Android Battery Historian
Android 自带的 Battery Historian 可以导出电池消耗数据,能看到各进程的功耗占比。用法是先用adb shell dumpsys batterystats > batterystats.txt导出数据,然后上传到 Battery Historian 网页版分析。
方案二:Unity Profiler 的 GPU 和 CPU 耗时
虽然 Profiler 不直接给功耗数据,但 CPU 和 GPU 的耗时和功耗强相关。耗时越高,功耗越大。所以可以用耗时作为功耗的代理指标。
方案三:adb 读取电流
部分 Android 设备支持通过adb shell cat /sys/class/power_supply/battery/current_now读取实时电流。不同设备路径可能不一样,需要自己找。读到的单位一般是微安(uA)或毫安(mA)。
方案四:iOS 的 Energy Log
iOS 上用 Instruments 的 Energy Log 模板,可以看设备的能耗等级。Xcode 里连上真机,选 Energy Log,跑一段时间就能看到能耗曲线。
3.3 桌面端功耗测量方案
桌面端测量功耗相对简单,因为可以用外接功率计。把电脑插在功率计上,跑游戏,读功率计的数字就行。但要注意,整机功耗包含显示器、主板、硬盘等,不全是 GPU 和 CPU 的。要精确测量,可以用 GPU-Z 或者 HWiNFO 读显卡和 CPU 的单独功耗。
Windows 上还有一个工具叫PresentMon,可以抓取帧的呈现时间,结合功耗数据可以分析每帧的能耗。
3.4 功耗测量的实操步骤
以 Android 为例,我一般按下面的流程走:
- 设备充满电,拔掉充电器,关闭其他后台应用。
- 开启飞行模式(如果不需要网络),关闭蓝牙、GPS。
- 屏幕亮度固定到 50%,关闭自动亮度。
- 用
adb shell dumpsys batterystats --reset重置电池统计。 - 启动游戏,跑固定的测试场景,比如 10 分钟。
- 测试结束后,用
adb shell dumpsys batterystats > after.txt导出数据。 - 用 Battery Historian 分析,看各模块的功耗占比。
- 同时用 Profiler 记录 CPU、GPU、内存的曲线,和功耗数据对照。
注意:测试时要保证每次条件一致,否则数据没有可比性。环境温度、设备温度、后台应用都会影响结果。
3.5 功耗数据的解读
拿到功耗数据后,重点看几个东西:
- 总功耗曲线:是平稳还是波动?波动大说明有周期性负载。
- CPU 功耗占比:如果 CPU 占比高,重点优化脚本和物理。
- GPU 功耗占比:如果 GPU 占比高,重点优化渲染。
- 屏幕功耗占比:屏幕功耗通常占大头,如果屏幕功耗高,考虑降亮度或降刷新率。
- 网络功耗占比:网络功耗高说明通信频繁,优化网络策略。
我一般会把功耗数据和 Profiler 数据叠在一起看,找出功耗峰值对应的帧,然后分析那一帧发生了什么。这样定位问题非常准。
4. 常见问题与排查技巧实录
4.1 为什么 Profiler 显示 CPU 不高但设备还是烫
这是最常见的问题。原因可能有几个:
- GPU 负载高,但 Profiler 默认不显示 GPU 耗时。需要在 Profiler 里添加 GPU 模块,或者用平台专用的 GPU 工具(比如 Android 的 GPU Inspector、iOS 的 Metal System Trace)。
- 内存带宽占用高。CPU 和 GPU 共享内存带宽,带宽跑满时,即使计算单元不满载,功耗也会高。
- 屏幕功耗高。屏幕是独立功耗大户,Profiler 看不到。
- 射频模块功耗高。WiFi、蓝牙、蜂窝网络在持续通信时功耗不低。
排查方法:用平台工具看 GPU 和内存带宽,用功耗计看整机功耗,逐项排除。
4.2 发热导致降频后帧率暴跌怎么办
设备温度到一定程度会触发降频保护,CPU 和 GPU 频率下降,帧率跟着掉。这是硬件保护机制,软件层面无法绕过,只能减少发热。
应对策略:
- 降低渲染分辨率,减少 GPU 负载。
- 降低目标帧率,比如从 60 帧降到 30 帧,功耗能降一半。
- 减少每帧的计算量,把一些逻辑改成定时执行。
- 优化散热,比如去掉手机壳、避免边充边玩。
4.3 怎么判断是 CPU 热还是 GPU 热
简单方法:用手摸设备的不同区域。CPU 通常在设备上半部分,GPU 在中间或下半部分。但这个方法不精确。
精确方法:用工具分别测 CPU 和 GPU 的功耗。Android 上可以用adb shell cat /sys/class/thermal/thermal_zone*/temp读多个温度传感器的值,不同传感器对应不同部件。iOS 上用 Instruments 的 Energy Log 可以看到 CPU 和 GPU 的能耗占比。
4.4 优化后功耗没降反升是什么原因
这种情况我也遇到过。可能的原因:
- 优化引入了新的开销。比如为了减少 Draw Call 用了合批,但合批本身有 CPU 开销。
- 优化改变了负载分布。比如把 CPU 的活挪到 GPU,CPU 功耗降了但 GPU 功耗升了,总功耗没变。
- 测量误差。测试条件不一致,或者设备温度不同导致降频程度不同。
排查方法:每次只改一个变量,改完立刻测,对比数据。不要一次改多个地方,否则不知道是哪个改动起了作用。
4.5 常见问题速查表
| 问题现象 | 可能原因 | 排查工具 | 解决方向 |
|---|---|---|---|
| CPU 耗时高 | 脚本逻辑重、GC 频繁 | Unity Profiler | 缓存引用、消除 GC |
| GPU 耗时高 | Overdraw、Shader 复杂 | RenderDoc、GPU Inspector | 简化 Shader、降分辨率 |
| 内存占用高 | 资源未释放、泄漏 | Memory Profiler | 卸载无用资源、修泄漏 |
| 物理耗时高 | 刚体多、碰撞复杂 | Profiler Physics 模块 | 简化碰撞体、降频率 |
| 网络功耗高 | 请求频繁、包大 | 网络抓包工具 | 合并请求、压缩数据 |
| 屏幕功耗高 | 亮度高、刷新率高 | 系统设置 | 降亮度、锁帧率 |
| 设备烫但指标正常 | 散热差、环境温度高 | 温度传感器 | 改善散热、降负载 |
5. 工具选型与实操配置参考
5.1 Unity Profiler 的正确用法
Profiler 是排查发热的核心工具,但很多人用不对。几个关键点:
- Development Build + Autoconnect Profiler:真机调试时用这个组合,数据最接近真实。
- Deep Profile 慎用:Deep Profile 会注入大量检测代码,开销极大,只在定位具体函数时开,平时关掉。
- GPU 模块要手动加:Profiler 默认不显示 GPU 耗时,需要在 Profiler 窗口的 Add Profiler 里加上 GPU。
- Memory 模块看 GC Alloc:重点关注
GC Alloc列,目标是每帧 0 分配。 - Rendering 模块看 Batches 和 SetPass Calls:这两个指标反映渲染开销。
5.2 平台专用工具
Android:
- GPU Inspector:抓 GPU 帧,分析 Overdraw 和 Shader 耗时。
- Battery Historian:分析电池消耗。
- Systrace / Perfetto:系统级性能追踪,能看到 CPU 调度、频率变化。
iOS:
- Instruments 的 Metal System Trace:分析 GPU 渲染。
- Instruments 的 Energy Log:分析能耗。
- Xcode 的 GPU Frame Capture:抓帧分析。
桌面端:
- RenderDoc:跨平台抓帧工具,分析渲染管线。
- GPU-Z / HWiNFO:读硬件功耗和温度。
- PresentMon:分析帧呈现时间。
5.3 功耗测量的硬件方案
如果预算允许,建议配一个硬件功率计。Monsoon 的功率计是行业标准,能精确到毫瓦级。国产的也有类似产品,价格便宜一些。硬件功率计的好处是数据准确、实时性好,不受软件开销影响。
如果没有硬件功率计,可以用智能插座带功率统计功能的,虽然精度差一些,但看趋势够用。
5.4 测试场景的设计
功耗测试一定要有固定的测试场景,否则数据没法对比。我一般会设计一个“标准测试场景”,包含:
- 一段固定的游戏流程,比如跑图、战斗、UI 操作各 3 分钟。
- 固定的设备设置:亮度 50%、音量 50%、飞行模式。
- 固定的环境温度:25 度左右,避免阳光直射。
- 固定的测试时长:10 分钟,从冷机开始。
每次优化后,跑同样的场景,记录功耗曲线和温度曲线,对比变化。
6. 优化落地的优先级与节奏
6.1 先优化什么后优化什么
优化要有优先级,不能眉毛胡子一把抓。我的经验是按下面的顺序:
- 先降 GPU 负载:GPU 发热最猛,优化效果最明显。降分辨率、简化 Shader、减 Overdraw,这三招下去,温度通常能降 3 到 5 度。
- 再降 CPU 负载:消除 GC、缓存引用、减少 Update 里的操作。CPU 优化见效快,但降温幅度不如 GPU。
- 然后降帧率:如果前两步做完还是烫,考虑锁 30 帧。帧率减半,功耗差不多减半。
- 最后调屏幕:降亮度、降刷新率。这是最后手段,因为影响用户体验。
6.2 优化节奏的控制
优化不是一次做完的,要迭代。每次改一两个点,测一次,记录数据,再改下一批。这样能清楚知道每个改动的效果,避免改了一堆不知道哪个有用。
我一般会做一个优化记录表,每次改动记录:改了什么、预期效果、实测效果、是否保留。这样积累下来,就是一套针对自己项目的优化经验库。
6.3 避免过度优化
优化要有度。有些优化会牺牲画质或体验,比如降分辨率降太多、关掉所有特效。要在功耗和体验之间找平衡点。我的原则是:用户感知不到的优化才做,用户能感知到的优化要谨慎。
比如把渲染分辨率从 1.0 降到 0.9,用户基本看不出来,但 GPU 负载降了 20%,这种就值得做。但把分辨率降到 0.5,画面糊了,用户肯定不干,这种就不行。
6.4 长期监控与回归测试
优化不是一劳永逸的。项目迭代过程中,新功能可能引入新的发热点。所以要建立长期监控机制,每次版本更新后跑一遍功耗测试,确保没有回归。
我一般会在 CI 流程里加一个功耗测试环节,自动跑标准场景,记录功耗数据,和基线对比。如果功耗上升超过 10%,就报警,人工介入排查。
这套七大热源排查清单加功耗测量实战的方法,我在多个项目上验证过,效果稳定。核心就一句话:用数据说话,别凭感觉优化。把功耗量化了,问题就解决了一半。剩下的就是按清单逐项排查,找到热源,针对性优化。