ARTICLE DETAIL

资讯详情

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

15-02-工具-Unity-Profiler与Memory-Profiler实战

15-02-工具-Unity-Profiler与Memory-Profiler实战 Unity Profiler 与 Memory Profiler从慢帧到引用链的实战方法系列C# 与常用数据结构源码剖析 · 实战工具篇固定编辑器基线Unity 2022.3 LTS具体 patch、目标平台和脚本后端必须记录包边界Memory Profiler 是 Package Manager 包版本必须由项目 manifest/lock 固定不能用latest代替实验条件一、先决定要解释帧时间还是要解释内存存活Unity Profiler 与 Memory Profiler 回答不同问题Profiler 的 CPU/GPU/Rendering/Memory 等模块记录运行时间线和计数器适合解释某一帧为什么慢、哪里分配、调用次数为何上升Memory Profiler 抓取进程内存快照适合解释某时刻哪些托管/原生对象仍存活、由谁引用、场景循环后哪些类别增长操作系统或平台 GPU 工具负责驱动分配、驻留、带宽等更底层问题Unity 快照并不总能完整解释 GPU/原生插件内存。不要从GC.Alloc为零推导“内存没有增长”也不要从快照中某类型很大推导“它造成了慢帧”。时间线和对象图需要用统一场景、帧编号和操作步骤关联。本文所有工具名称以 2022.3 LTS 为基线不同 patch、Memory Profiler 包版本和平台 UI 可能变化。每份报告都应记录 Editor version、包版本、Build Target、Development Build、Mono/IL2CPP、设备、画质与分辨率。二、建立可重复的 Player 回放性能排查从问题定义开始目标设备上哪个场景、何种输入、哪类帧超过预算持续还是尖峰内存问题是峰值过高、场景退出不回落、重复循环持续增长还是 GC 暂停。建立固定回放启动到稳定点预热 shader/对象池/JIT 或 IL2CPP代码路径执行相同战斗、UI开关或场景切换持续相同帧数再回到稳定点。记录随机种子、资产版本、角色数量和摄像机路径。正确性断言必须保持否则删除逻辑会“优化”帧时间。Editor Play Mode 混有编辑器窗口、序列化、Domain/Scene Reload、Gizmos 和其他工具开销。先用 Editor 快速定位再在目标设备 Player 复现最终预算只由目标 Player 判断。三、Development Build、Autoconnect 与 Deep Profile3.1 Development Build构建设置中的 Development Build 让 Player 支持开发诊断和连接 Profiler生成行为不等于最终非 Development 发布。它适合获得符号、marker 和连接能力却可能改变代码生成、检查与时序。完成定位后使用尽量接近 Release 的构建和平台工具确认收益。3.2 Autoconnect ProfilerAutoconnect Profiler 让 Player 启动时自动连接编辑器适合捕获启动/首场景。连接依赖网络、USB、平台权限和防火墙未连上时应从 Profiler 的 Active Profiler 下拉选择目标或按平台文档配置。不要把某个固定端口/adb 命令写成跨版本通用答案。连接和数据传输本身产生开销尤其启用大量模块、调用栈或长时间录制时。为同一回放保留“无 Profiler”基线例如通过 Player 内置计时/平台采样确认采集没有改变问题性质。3.3 Deep ProfileDeep Profile 对大量脚本调用插桩能暴露没有 marker 的细粒度栈但会显著改变执行时间、调用成本和分配表现。它不是“最精确模式”也没有可跨项目保证的固定开销倍数。使用顺序应是普通采样/marker 找到小范围再短时 Deep Profile关闭后重新确认。Call Stacks for GC.Alloc 也会增加捕获成本和数据量只在追踪分配来源时开启。不要同时打开所有模块、Deep Profile 和全调用栈后把采集结果当原始性能。四、CPU UsageHierarchy 与 Timeline 互相校验4.1 Timeline 回答“这帧按时间发生了什么”Timeline 按线程显示 marker 嵌套适合查看 Main Thread、Render Thread、Job Worker、加载线程和 GC 在同一帧的关系。主线程某段 WaitFor... 很长可能表示等待渲染、Job、锁或 GPU而非该 marker 自己做 CPU 计算。先看关键线程的先后关系再钻调用树。慢帧调查步骤在帧图中选择问题帧及其前后帧判断 CPU 主线程、Render Thread 还是 GPU 超预算在 Timeline 找最长工作/等待 marker 和线程依赖检查同帧 GC、加载、Instantiate、shader或资源上传回到 Hierarchy 查看该 marker 的累计/自身时间与调用数在相同回放中复现不凭单个偶发帧下结论。4.2 Hierarchy 回答“时间累计在哪个调用树”Hierarchy 聚合选择帧的 marker可按 Total Time、Self Time、Calls 和 GC Alloc 等排序。Total 包含子项Self 只看 marker 自身未归属子项的时间递归和跨线程工作可能让直觉失效。合并多帧后总时间会受样本数影响比较应使用相同帧窗口。若一个 Update marker调用次数意外增长先检查对象数量/重复订阅/重复组件而不是微优化每次调用。若 Self 很低但 Total 很高热点在子调用。若脚本 marker 等待 Job 完成真正工作可能在 Worker Timeline。4.3 GPU 边界GPU 模块和 marker 可用性取决于图形 API、设备和构建。CPU 主线程快但 Present/等待 GPU 时需 Frame Debugger、RenderDoc、Xcode GPU、Android GPU Inspector 等平台工具。CPU Profiler 不能独立证明 shader、带宽或 GPU residency 原因。五、GC.Alloc找到分配调用栈不把分配等同于 GCGC.Alloc 列/marker 表示托管分配发生在所记录路径单位和展示方式以当前 Profiler 为准。一次分配不会立即等于一次 GCGC 暂停由分配率、存活对象、堆状态和运行时策略共同决定。排查流程在问题帧确认 GC.Alloc marker/列是否出现看分配发生在哪个线程与 marker短时启用 allocation call stacks重跑最小场景从调用栈回到业务代码识别数组/List 扩容、字符串、LINQ、装箱、委托/闭包或序列化做单变量修复并在关闭调用栈采集后复测同时看帧时间与 retained memory避免池化把短命分配变成长期峰值容量。原文“foreach 通过接口一定装箱”“struct 就无分配”等都只能按具体编译器、BCL、Mono/IL2CPP路径验证。GC.Alloc 调用栈与 IL/反编译可共同证明不靠口诀。周期性 GC 与慢帧重合只是相关证据。还要判断是分配速度过高、长期存活过多、显式GC.Collect还是场景加载触发。不要用每帧GC.Collect验证修复。六、自定义 ProfilerMarker把业务阶段变成证据ProfilerMarker比字符串式 Begin/EndSample 更适合长期埋点并能用using保证作用域结束using Unity.Profiling; public sealed class EnemySystem { private static readonly ProfilerMarker UpdateMarker new(Game.Enemy.UpdateVisible); public void UpdateVisible() { using (UpdateMarker.Auto()) { Cull(); Simulate(); } } }marker 命名应稳定、按系统分层避免包含每帧变化的 ID 导致数据碎片。埋点不要包裹完全不同的工作也不要在最内层海量循环为每项创建细粒度 samplemarker 本身有观察成本。可添加自定义 counter 描述实体数、队列深度或缓存命中让时间与工作量同时可见。API 和 Player 支持以 Unity 2022.3 文档/目标平台核验。七、ProfilerRecorder把性能回放变成数据ProfilerRecorder 可在运行时读取受支持 marker/counter 的样本适合自动化测试和构建趋势using Unity.Profiling; public sealed class PerfCapture : IDisposable { private readonly ProfilerRecorder _mainThread ProfilerRecorder.StartNew( ProfilerCategory.Internal, Main Thread, capacity: 2048); public void Dispose() _mainThread.Dispose(); public long[] Snapshot() { var samples new ListProfilerRecorderSample( _mainThread.Capacity); _mainThread.CopyTo(samples); return samples.Select(s s.Value).ToArray(); } }示例为了清晰在 Snapshot 中分配 List/数组和 LINQ不应在被测帧内调用回放结束后导出。marker 名、单位、capacity 和平台支持需查询ProfilerRecorderHandle.GetAvailable/目标版本文档不能假定所有 Player 相同。自动报告保存原始帧样本离线计算中位数、P95/P99、慢帧比例和最大连续慢帧。平均 FPS 会掩盖尖峰。预先定义暖机和测量窗口首次加载、稳态战斗、场景退出分别统计不混成一个平均值。八、Memory Profiler 快照快照看到的是一个时刻安装包后从对应窗口连接 Player并 Capture Snapshot。包版本由项目Packages/manifest.json与 lock 固定。快照通常会暂停 Player、遍历内存并生成较大文件因此不是实时采样捕获期间的峰值和时序也可能受扰动。Memory Profiler 的All Of Memory等视图可按 Unity Objects、Managed Objects、Native Allocations、Graphics等分类查看具体命名随包版本。快照能展示 Unity 已知内存和引用关系但“不在分类里”不等于不存在原生插件私有 allocator、驱动/GPU residency、共享系统页和 OS 统计可能需要平台工具。8.1 托管对象与 Native Unity ObjectMonoBehaviour/Texture等 UnityEngine.Object 常涉及托管 wrapper 与原生对象。托管引用消失不必然代表原生资源立即卸载Destroy、Resources.UnloadUnusedAssets、Addressables/AssetBundle 引用计数和场景生命周期各有规则。反过来Missing/Destroyed wrapper 也可能仍由事件或静态集合引用。使用 References/Paths to Root 一类视图追踪静态字段、事件委托、DontDestroyOnLoad、单例、协程状态机、缓存、ScriptableObject/资产依赖。引用路径显示“为什么可达”不自动判断这条引用是否错误。8.2 快照本身不是完整时间线一次快照无法证明持续增长也可能错过瞬时峰值。Profiler 时间线/平台指标负责峰值多个语义一致的快照负责存活差异。快照前是否主动 GC/Unload 会改变口径可以建立“自然状态”和“显式清理后”两套实验但不能只清一个样本。九、双快照 diff基线、操作、清理后对齐最稳妥的不是随意截两张而是固定状态点启动并预热 - 回到空闲基线点 ACapture - 执行战斗/UI/场景循环 - 走正常退出与清理 - 回到与 A 相同状态点 BCapture - Compare A/B - 再重复一轮得到 C判断是否持续累积A 与 B 必须处于同一场景、UI、相机和加载状态。若 B 仍在战斗中更多敌人和纹理是合理工作集不是泄漏。按 Size/Count Difference 排序只是入口继续检查增长对象的引用链、归属场景、生命周期和容量。快照比较的基线也要版本化相同 Unity patch、包、平台与资产。跨版本 diff 往往包含引擎/包布局变化不适合直接归因于业务提交。保存快照文件、构建 commit 和重现步骤而不只保存截图。十、泄漏、留存、峰值和碎片不是同义词泄漏对象因意外引用持续可达重复生命周期后累积例如静态事件持有已关闭面板。合理留存缓存、对象池、预加载资产有意保留可能改善帧时间但需要容量和淘汰预算。峰值加载/解压/构建期间短暂同时拥有旧数据和新数据操作结束后回落可能造成 OOM 即使不泄漏。未及时回收/卸载对象已不可达但 GC 尚未运行或原生资产等待显式卸载。它不必是引用泄漏。碎片/allocator保留逻辑对象释放后堆/allocator/OS 工作集不立即下降。仅看进程 RSS 会误判。诊断要问Count 是否每轮增长retained size由谁引用清理后是否稳定在新高水位池容量是否符合预算峰值时是否旧新副本共存native/GPU是否需要其他工具。Clear()只移除集合元素通常不缩容量TrimExcess会有重新分配/复制成本也不能替代修复引用源。不要在每次战斗后机械 Trim。十一、场景案例反复打开背包后内存和帧尖峰增长11.1 观察固定设备上自动打开/关闭背包多轮。ProfilerRecorder 显示关闭后的稳定帧逐轮变差Memory 曲线高水位上升CPU Timeline 中某全局事件发布调用越来越多。11.2 假设InventoryPanel在 OnEnable 订阅全局 InventoryChanged但某关闭路径没有 OnDisable/退订。长寿命服务通过委托持有 Panel重复打开还产生更多处理器因此既留存对象又增加每次事件调用数。11.3 证据Hierarchy 中事件处理器 Calls 随循环增长A/B/C 快照中 InventoryPanel 数量增长References 显示InventoryService - event delegate - panel修复只改变订阅生命周期功能测试仍通过。11.4 修复public sealed class InventoryPanel : MonoBehaviour { private void OnEnable() InventoryService.Changed OnInventoryChanged; private void OnDisable() InventoryService.Changed - OnInventoryChanged; private void OnInventoryChanged() Refresh(); }还需验证OnEnable 是否可能重复而 OnDisable 未发生服务是否在 domain reload 关闭配置下保留静态状态Destroy路径是否调用相应生命周期异步/协程是否捕获 Panel。若业务要求禁用时仍接收就改在 Awake/OnDestroy 成对管理而不是照抄模板。11.5 回归重复同一轮次确认 Calls 不累积、B/C 同状态快照趋于稳定、帧尖峰下降、UI仍正确刷新。关闭 Profiler和调用栈后在非 Development/接近发布构建复测用户指标。十二、其他常见引用链协程/async状态机捕获 MonoBehaviour、资产或大数组场景退出后任务仍未取消建立生命周期 token/StopCoroutine并观察引用对象池池中对象合理留存但偶发峰值污染容量设上限/淘汰并重置状态Addressables/AssetBundlehandle/release不配对依赖资产原生内存保留用资源系统诊断工具与 Memory Profiler联合静态 Dictionary键或值引用场景对象Clear只在部分路径执行封装所有权并测试场景循环闭包/UnityEventlistener捕获整个界面/场景保存并移除运行时监听检查 persistent listener语义RenderTexture/ComputeBuffer/NativeArray非托管资源未 Release/Dispose托管 GC.Alloc 可能为零需看 native分类和资源生命周期。每个“修复”都要先确定谁拥有资源。贸然 Dispose/Release 共享对象可能制造 use-after-free。十三、观察者效应控制Profiler会占用 CPU、内存、网络/USB带宽并可能改变线程时序Deep Profile、allocation stacks、快照影响更强。控制方式先用低开销 Recorder/平台帧统计确认问题普通 Profiler只开必要模块定位缩小范围后短时开调用栈/Deep ProfileMemory Snapshot只在固定状态点抓对同一回放保留无采集、轻采集、重采集三份结果修复后关闭重诊断在目标 Player重测。Profiler marker本身也会改变极短函数的成本日志更可能导致字符串分配和 I/O。性能版本关闭无关日志诊断代码不要运行在被测窗口。十四、自动性能与内存回归自动场景测试可使用 Unity Performance Testing package/自建 harness 驱动固定回放ProfilerRecorder采样 marker和帧计数输出 JSON/测试结果。包版本同样固定。CI机器硬件差异大时以同一 runner趋势或基准设备实验室为主。不要只断言平均 FPS。可设置预热窗口采样窗口主线程/GPU分位数慢帧比例GC Alloc总量/有分配帧数加载峰值场景循环后的对象计数守护。阈值基于预算和历史噪声不从单次本机结果拍脑袋。完整 Memory Snapshot昂贵不适合每次提交。可在每日/发布候选于基准设备运行 A/B/C 场景循环并归档快照普通 CI 使用轻量代理指标。代理变化触发深度快照而不是把 RSS差值直接判泄漏。性能回归需保存commit、Unity/包版本、构建设置、设备、温度/电源、脚本后端、画质、原始样本和正确性结果。出现回归先重跑排除噪声再二分提交修复后把重现用例留在套件中。十五、实战检查单准备固定 Unity 2022.3 patch、Memory Profiler/Performance Testing包版本定义目标设备、场景、主指标、预算与正确性建立可重复回放、暖机和同状态快照点区分 Editor快速诊断与 Player最终证据。CPU/帧Timeline先判断线程与等待关系Hierarchy再看聚合热点CPU、Render Thread、GPU瓶颈未混淆GC.Alloc调用栈只在短时需要时开启marker命名稳定调用次数和工作量counter同时观察分位数/慢帧而不只是平均 FPS。内存A/B/C快照在同场景/清理状态按对象数/大小差异后继续看引用链托管、Native、GPU/驱动和插件边界明确泄漏、缓存留存、峰值、未回收与碎片没有混用事件、协程/async、池、Addressables和原生资源所有权已审查。验证一次只改一个核心变量并保留修复前原始数据功能、生命周期、场景循环和目标设备回归通过关闭 Deep Profile/调用栈/Profiler后收益仍存在Mono 与 IL2CPP差异按实际发布后端验证自动回归记录版本、设备、原始样本和噪声范围。十六、结论Unity Profiler最擅长建立帧时间证据链Timeline说明线程和先后关系Hierarchy汇总热点GC.Alloc调用栈追到分配源ProfilerMarker/Recorder把业务阶段变成可回归指标。Memory Profiler则在固定状态点解释对象图哪些对象增长谁持有它们托管 wrapper与Native资源如何关联。可靠流程不是“看到内存不降就 Clear/Trim”而是固定 Player回放捕获基线与问题帧形成引用/时序假设只改一个变量再以同状态双/三快照和帧分位数复测。Editor、Development Build、Deep Profile与快照都有观察者效应最终结论要在目标 Mono/IL2CPP Player和接近发布构建成立。把快照文件、Profiler数据、包版本和自动回归一起归档性能问题才从一次人工截图变成团队可重复的工程资产。下一篇dotMemory、PerfView 与 .NET 运行时诊断
返回列表