拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Unity Memory Profiler深度指南:从内存错配到引用链诊断

1. 这不是“看内存”的工具而是Unity项目健康诊断的听诊器Unity Memory Profiler不是个简单的数字显示器它是一套嵌入在编辑器内部的、实时可交互的内存病理分析系统。我用它给上百个从2D休闲到3A级Demo的项目做过“体检”最深的体会是90%以上的内存问题根本不是泄漏而是“错配”——资源没被释放是因为你根本不知道它被谁悄悄持有了对象没被回收是因为你误以为它已经脱离了引用链。它解决的从来不是“内存爆了怎么办”而是“为什么这个纹理明明没在场景里却占着32MB不放”、“为什么切换关卡后GC次数飙升三倍”。关键词“Unity Memory Profiler”背后是每个中大型项目必经的性能临界点——当美术资源量突破500个Prefab、脚本逻辑超过10万行时靠Log打点和手动Debug.Log查引用效率会断崖式下跌。它适合两类人一是刚接手老项目的程序员面对一堆“祖传代码”和不明所以的内存曲线需要快速建立认知地图二是技术美术或主程需要向策划和制作人解释“为什么这个新UI动效会导致iOS端闪退”用可视化证据代替抽象术语。它不教你怎么写代码但能让你一眼看清代码在内存里的真实行为——就像给你的C#对象装上X光机血管引用、骨骼类型、器官堆分配全部透明可见。我见过最典型的误用是把它当成“内存清理按钮”一通Snapshot拍完就去点“Force GC”结果发现下次Snapshot里同一块内存还在于是怀疑工具失效。其实问题出在思维惯性我们习惯把内存当垃圾桶而Memory Profiler逼你把它当活体组织来观察——垃圾不会自己消失它只是暂时被遮蔽了。2. 核心设计逻辑为什么它不叫“Memory Monitor”而叫“Profiler”2.1 三层穿透式架构从宏观趋势到微观引用链Memory Profiler的底层不是简单地调用System.GC.GetTotalMemory()它构建了一个三层穿透式数据采集与呈现架构这是它区别于所有第三方内存监控插件的根本原因第一层运行时快照Runtime Snapshot这是最常被误解的一层。很多人以为Snapshot就是“拍张内存照片”实际上它捕获的是托管堆Managed Heap的完整拓扑结构快照包含每个对象的地址、类型、大小、以及所有直接引用的对象指针。关键在于它不是静态快照而是带有时序标记的增量快照——你可以对比两个Snapshot之间哪些对象被新增、哪些被销毁、哪些被迁移如GC后内存重排。我实测过在一个加载了10个场景的AR项目中单次Snapshot耗时约180msi7-10875H但生成的.snapshot文件只有2.3MB因为Unity做了智能压缩只存储对象ID和引用关系而非原始二进制数据。这解释了为什么它能在编辑器内流畅操作——它不是在读内存而是在读一张高度优化的“内存关系图”。第二层内存分类视图Memory Classification View这是真正体现Unity工程思维的设计。它不按传统OS内存分类如Heap/Stack/Code而是按Unity引擎生命周期和资源管理模型划分Assets资源所有通过Resources.Load、Addressables或AssetBundle加载的资源实例包括Texture、Mesh、AudioClip等。这里能看到“未被引用但未卸载”的资源——比如一个Texture被SpriteRenderer.sprite.texture持有但Sprite本身已被Destroy而Texture因引用计数未归零无法Unload。Objects引擎对象GameObject、Component、ScriptableObject等Unity原生对象。重点看MonoBehaviour实例数如果某个自定义脚本实例数持续增长基本锁定为泄漏源。Managed Heap托管堆纯C#对象如ListT、DictionaryK,V、自定义类实例。这里暴露了最隐蔽的问题——比如一个全局static Dictionarystring, object不断Add却从不Remove或者协程IEnumerator被意外长期持有。Native Memory原生内存这部分常被忽略但它直接关联崩溃。例如Mesh.vertices数组在托管堆只占8字节指针但其背后原生内存可能高达16MB。Memory Profiler会显示Mesh的Native Size字段这才是真实开销。第三层引用链追踪Reference Chain Traversal这是诊断的灵魂功能。选中一个可疑对象比如一个占着48MB的Texture2D点击“Show References”它会生成一条从GC Root如static字段、线程栈到该对象的最短强引用路径。我曾用它揪出一个隐藏极深的BugUI系统里一个CanvasGroup组件被设为alpha0但它的CanvasRenderer仍持有Material而该Material的mainTexture又引用着一个未压缩的4K PBR贴图。路径显示为GC Root → UIManager.Instance → _activeCanvasGroups List → CanvasGroup[0] → CanvasRenderer → Material → mainTexture → Texture2D。没有这个引用链你只会看到“CanvasGroup没Destroy”永远想不到是alpha设置触发了材质缓存机制。提示引用链默认只显示“强引用”但勾选“Show Weak References”能看见WeakReference、GCHandle等弱持有者这对排查AsyncOperation回调泄漏至关重要——很多协程泄漏源于yield return new WaitForSeconds()后回调委托被Coroutine对象强引用而Coroutine又被MonoBehaviour的m_Coroutines列表持有。2.2 与Unity其他性能工具的本质差异很多人混淆Memory Profiler和Profiler Window的Memory模块它们定位完全不同对比维度Memory ProfilerProfiler Window → Memory Module数据粒度对象级Object ID 引用链分类汇总Texture: 120MB, Mesh: 85MB时间维度快照对比Delta Analysis实时流式监控每帧采样诊断深度可追溯到具体C#变量名如PlayerController._inventory只显示类型名如System.Collections.Generic.List1适用阶段中后期深度优化需稳定复现场景日常开发即时反馈如改UI后看内存峰值平台支持编辑器内全功能真机需额外配置见3.3节所有平台实时连接含WebGL我建议的工作流是先用Profiler Window的Memory模块发现“Texture内存异常增长”再用Memory Profiler拍两个Snapshot加载前/加载后用Delta模式聚焦新增的Texture实例最后用引用链定位到具体哪个脚本的public Texture2D字段在反复Load却未Unload。这种组合拳比单用任何一种工具都高效。2.3 为什么它必须集成在编辑器里离线分析的致命缺陷市面上有若干第三方内存分析库如Unity.MemoryProfiler开源版但它们都无法替代官方工具核心在于上下文耦合性。举个典型例子当你在Memory Profiler里看到一个Mesh对象双击它不仅能查看顶点数、三角面数还能直接跳转到Inspector面板看到它的Read/Write Enabled状态、Optimize Mesh设置甚至一键Select in Project定位到原始FBX文件。而离线工具只能告诉你“这个Mesh占12MB”却无法告诉你“因为它开启了Read/Write导致Unity为其分配了双份内存GPUCPU”。再比如ScriptableObjectMemory Profiler会显示它是Asset还是Instance如果是后者会标注“Created from ScriptableObject.CreateInstance()”这直接指向代码中的CreateInstance调用点——而离线工具只能显示类型名你得手动grep整个工程找创建位置。这种编辑器内深度集成让内存问题不再是一个抽象数字而是一个可点击、可编辑、可调试的具体资产。3. 实操全流程从安装到精准定位泄漏源的7个关键动作3.1 安装与基础配置避开三个隐形陷阱Memory Profiler作为Unity Package ManagerUPM官方包安装看似简单但有三个极易踩坑的配置点Unity版本兼容性陷阱官方文档说“支持2019.4”但实际在2020.3 LTS中若项目启用了Scripting Runtime Version: .NET 4.x必须安装com.unity.memoryprofiler2.0.0或更高版本。低版本如1.2.0在.NET 4.x下会报MissingMethodException。我遇到过最诡异的案例一个2021.3.15f1项目安装最新版Memory Profiler后Snapshot按钮灰色不可用——最终发现是项目ProjectSettings/EditorSettings.asset里scriptingRuntimeVersion被错误设为3对应.NET Standard 2.0而正确值应为4。解决方案在菜单栏Edit → Project Settings → Player → Other Settings → Configuration → Scripting Runtime Version中确认为.NET 4.x并重启编辑器。IL2CPP平台的符号表陷阱在iOS或Android真机调试时若使用IL2CPP后端Memory Profiler默认无法解析C#方法名引用链里只显示Unknown Method。这是因为IL2CPP剥离了调试符号。必须在Player Settings → Publishing Settings → Strip Engine Code设为Disabled并在Other Settings → Script Compilation → Enable Internal Profiler勾选。注意这会使APK/IPA体积增加15%-20%仅用于诊断阶段发布前务必关闭。我曾帮一个微信小游戏团队排查WebGL内存暴涨他们启用了Enable Internal Profiler但忘了在Build Settings里勾选Development Build导致Profiler数据完全不可用——Development Build是启用所有调试符号的前提。多线程渲染的快照时机陷阱若项目启用了Player Settings → Other Settings → Graphics Jobs即多线程渲染Memory Profiler的Snapshot可能捕获到不一致的状态。因为渲染线程和主线程内存操作不同步。解决方案在拍Snapshot前临时禁用Graphics Jobs编辑器内可热切换拍完再启用。或者更稳妥的做法在代码中插入MemoryProfiler.ForceGarbageCollection();后立即拍Snapshot确保GC完成后再采集。注意安装后首次启动编辑器右上角会出现Memory Profiler窗口按钮。但别急着点先执行Window → Analysis → Memory Profiler检查是否弹出“Initialize Memory Profiler”对话框。若没有说明Package未正确激活——此时需在Package Manager窗口右上角点⋮ → Advanced → Show Preview Packages找到Memory Profiler并点击Refresh。3.2 建立基准快照不是随便拍而是设计实验拍Snapshot不是按下快照按钮就完事而是一场受控实验。我给自己定的铁律是每次Snapshot必须有明确的“操作-预期-对比”闭环。以一个常见的UI资源泄漏为例Step 1准备阶段Baseline Snapshot确保场景干净关闭所有非必要窗口Animation、Timeline清空Console Log执行Resources.UnloadUnusedAssets()。然后进入目标场景如主城界面等待所有异步加载完成观察Addressables.ResourceManager日志再拍第一个Snapshot命名为S1_Baseline_MainCity。此时记录关键指标Managed Heap Size如128MB、Texture Count如247、GameObject Count如1892。Step 2扰动阶段Action Snapshot执行你要测试的操作打开背包UI → 点击“装备”Tab → 滑动物品列表到底部触发动态加载→ 关闭背包。关键动作在关闭背包后等待3秒让所有协程、事件回调完成再执行Resources.UnloadUnusedAssets()然后拍第二个Snapshot命名为S2_AfterCloseBag。Step 3对照阶段Delta Analysis在Memory Profiler窗口选中S1_Baseline_MainCity按住CtrlWin或CmdMac点击S2_AfterCloseBag选择Compare Snapshots。此时出现Delta视图重点关注Added Objects新增的Texture、Mesh、GameObject数量。若Texture新增12个且未减少基本锁定UI预制体加载逻辑。Removed Objects应有大量GameObject和Component被移除。若Removed GameObjects为0说明UI未被Destroy。Retained Objects被保留但未新增的对象。这里藏着最狡猾的泄漏——比如一个Singleton类持有的ListGameObject不断Add却未Clear。我曾用此法在一个MMO手游中发现每次打开技能面板ShaderVariantCollection实例数1且永不减少。Delta分析显示这些实例的Retained By指向Shader.Find(Custom/UI/Effect)最终定位到技能图标Shader的Variant预编译未开启导致每次加载都动态生成新Variant。3.3 真机调试实战iOS/Android/WebGL的差异化配置Memory Profiler在真机上的使用远比编辑器复杂不同平台有专属通道iOS真机Xcode连接必须在Player Settings → Publishing Settings中开启Autoconnect Profiler并在Xcode的Scheme → Run → Arguments Passed on Launch里添加-enable-profiler。最关键的是iOS设备需在Settings → Privacy → Analytics Improvements → Share iPhone Analytics中开启否则Profiler数据无法回传。我遇到过一次Xcode控制台显示Profiler connected但Memory Profiler窗口无数据——排查3小时才发现是iOS 16.4的隐私开关默认关闭。Android真机ADB连接需要adb shell setprop debug.unity.profiler 1命令开启但更可靠的方式是在Player Settings → Other Settings → Configuration → Script Debugging勾选然后用adb logcat | grep MemoryProfiler监听日志。注意Android 10要求应用声明android.permission.INTERNET否则Profiler Socket连接失败。WebGLChrome DevTools联动这是最易被忽视的场景。WebGL构建需在Player Settings → Publishing Settings → Development Build勾选并在Build Settings中选择WebGL后点击Player Settings → Publishing Settings → Compression Format设为Disabled避免Brotli压缩干扰内存映射。然后在Chrome中打开chrome://inspect找到你的WebGL页面点击Configure...添加localhost:8000Unity WebServer端口最后在Memory Profiler窗口选择WebGL作为Target。此时Snapshot会通过WebSocket传输延迟约200-500ms需耐心等待。实操心得真机调试时务必关闭Profiler → CPU Usage等其他Profiler模块否则内存数据会被干扰。我曾在一个Pico Neo 3项目中同时开启Rendering Profiler导致Memory Profiler的Snapshot耗时从200ms飙升至2.3秒且数据严重失真。3.4 引用链深度挖掘从“谁持有我”到“我持有谁”引用链是Memory Profiler的核武器但多数人只用到第一层。真正的高手会进行三级挖掘第一级GC Root到目标对象Who holds me?如前述UI Texture案例路径显示GC Root → UIManager.Instance → _uiCache Dictionary → BagPanel → GameObject → Canvas → Image → Sprite → Texture2D。这时不要停继续点Texture2D右侧的Show Referenced Objects。第二级目标对象到其引用的对象What do I hold?展开后你会看到这个Texture2D持有m_TextureData原生内存、m_ReadableTextureData若Read/Write Enabled、以及m_SpriteAtlas如果被打入图集。其中m_SpriteAtlas可能引用着其他Texture形成“蝴蝶效应”——修复一个Texture泄漏可能连带解决5个相关资源。第三级跨域引用Cross-Domain Hold最危险的是跨域引用。比如一个MonoBehaviour的OnDestroy里写了EventSystem.current.SetSelectedGameObject(null)而EventSystem是DontDestroyOnLoad对象这就形成了Scene Object → EventSystem → Selected GameObject的长链。Memory Profiler会用红色高亮DontDestroyOnLoad对象提示你“此处存在跨场景引用”。我处理过一个典型案例AR项目中每次扫描新平面ARPlaneManager会创建GameObject但OnDestroy里忘记调用plane.Destroy()。引用链显示GC Root → ARSessionOrigin → ARPlaneManager → m_TrackedPlanes List → ARPlane GameObject → MeshFilter → Mesh → m_Vertices Array。关键发现是m_Vertices Array的Native Size为32MB而Managed Size仅128KB——这说明泄漏主体是原生内存必须从Mesh层面优化而非单纯Destroy GameObject。3.5 资源泄漏的黄金排查清单基于百个项目经验我总结出一份可直接执行的泄漏排查清单覆盖95%常见场景序号检查项检查方法典型症状解决方案1static集合类在Memory Profiler的Managed Heap中搜索System.Collections.Generic.List1、Dictionary2按Size排序实例数随操作次数线性增长改用ObjectPool或在OnDisable/OnDestroy中Clear2Event.AddListener未移除搜索UnityEngine.Events.UnityEvent查看其m_PersistentCalls.m_Calls长度UI频繁开关后内存持续上涨严格遵循AddListener/RemoveListener配对或用/-语法糖3Coroutine未终止搜索UnityEngine.Coroutine检查m_MonoBehaviour是否为null切换场景后仍有协程运行在OnDestroy中调用StopAllCoroutines()或用StopCoroutine(coroutine)精确终止4Texture2D.LoadImage未释放在Assets视图中筛选Texture2D按Native Size排序找大尺寸未引用Texture加载图片后内存不降LoadImage后立即Texture2D.Apply()不再需要时Destroy(texture)5Mesh未优化在Assets中筛选Mesh查看Vertex Count和Triangle Count模型面数超标但内存占用异常高启用Optimize Mesh关闭Read/Write Enabled用Mesh.CombineMeshes合并静态网格注意清单中第4项“Texture2D.LoadImage”是高频雷区。很多开发者用www.texture加载图片却不知www.texture返回的Texture2D是只读的且www.Dispose()后Texture仍驻留内存。正确做法是Texture2D tex new Texture2D(256,256); tex.LoadImage(www.bytes); Destroy(www);然后在不需要时Destroy(tex)。4. 高阶技巧与避坑指南那些文档里不会写的实战经验4.1 “假泄漏”识别如何区分真问题与正常内存抖动Memory Profiler常被误报“泄漏”实则是Unity内存管理的正常抖动。我归纳出三种典型“假阳性”GC抖动GC Thrashing表现为Managed Heap Size在100MB-150MB间剧烈波动GC Alloc列显示每帧数百KB分配。这不是泄漏而是频繁的小对象分配如new Vector3()、string.Substring()触发了高频GC。解决方案用Profiler Window → CPU Usage的GC Alloc列定位具体函数将Vector3.zero改为静态字段Substring改为Spanchar切片。资源预热抖动Asset Warm-up首次加载场景时Texture和Mesh数量暴增但后续加载稳定。这是Unity的资源预热机制——为提升后续加载速度会缓存部分资源元数据。判断标准第二次加载同一场景Added Objects数量锐减80%以上。无需干预但可在Addressables中设置AutoRelease策略控制缓存生命周期。原生内存延迟释放Native Memory DelayNative Memory在UnloadUnusedAssets()后不立即下降滞后2-3帧。这是因为GPU驱动层有内存池管理Unity无法强制立即释放。只要Managed Heap已清理且Native Memory在10秒内回落即属正常。若持续不降则需检查Graphics.Blit、RenderTexture.Release()等显式释放调用。我曾帮一个VR项目排除“泄漏”Memory Profiler显示RenderTexture实例数持续增长。深入分析发现所有RenderTexture的m_UseCount均为1且Retained By指向Camera.targetTexture。原来项目中多个相机共用同一RenderTexture但未在OnDisable中置空camera.targetTexture null导致引用链未断。这不是泄漏而是资源复用设计缺陷。4.2 自定义内存监控用Memory Profiler API做自动化巡检Memory Profiler提供IMemoryAnalyzer接口可编写自动化脚本。以下是我用于CI流水线的巡检模板// MemoryLeakDetector.cs using Unity.MemoryProfiler; using UnityEngine; public class MemoryLeakDetector : MonoBehaviour { [Header(巡检配置)] public int baselineDelaySeconds 5; // 基准快照延迟 public int actionDelaySeconds 3; // 操作后延迟 public float maxTextureGrowthMB 5f; // Texture允许增长阈值 private string baselinePath; private string actionPath; public void StartDetection() { // 拍基准快照 baselinePath MemoryProfiler.TakeSnapshot($Baseline_{Time.time}); // 延迟后执行操作如加载场景 StartCoroutine(DelayedAction()); } private System.Collections.IEnumerator DelayedAction() { yield return new WaitForSeconds(baselineDelaySeconds); // 执行你的测试操作 SceneManager.LoadScene(TestScene, LoadSceneMode.Additive); yield return new WaitForSeconds(actionDelaySeconds); // 拍操作后快照 actionPath MemoryProfiler.TakeSnapshot($Action_{Time.time}); // 自动对比 var delta MemoryProfiler.CompareSnapshots(baselinePath, actionPath); // 检查Texture增长 float textureGrowth delta.GetAddedObjectsOfTypeTexture2D().Sum(t t.NativeSize) / (1024f * 1024f); if (textureGrowth maxTextureGrowthMB) { Debug.LogError($Texture内存增长超限{textureGrowth:F2}MB {maxTextureGrowthMB}MB); // 发送告警到企业微信/钉钉 } } }此脚本可集成到Jenkins或GitHub Actions中在每次PR提交后自动运行将内存增长量化为可衡量的CI指标彻底告别“感觉内存变大了”的模糊判断。4.3 与Addressables深度协同解决资源管理的终极矛盾Addressables是现代Unity项目的资源管理基石但与Memory Profiler结合时存在一个经典矛盾Addressables.ReleaseInstance()后资源在Memory Profiler中仍显示为“Loaded”。这是因为Addressables的引用计数机制——只有当所有Addressables.InstantiateAsync()返回的AsyncOperationHandle都被Addressables.Release()资源才会真正Unload。我的协同工作流在Memory Profiler中筛选Assets视图按Ref Count排序找出Ref Count 1的资源。对每个高引用计数资源右键Show References路径中会显示AsyncOperationHandle及其m_ReferenceCount。在代码中搜索该资源的InstantiateAsync调用点确认是否遗漏Release。关键技巧在Addressables.InstantiateAsync()后立即保存AsyncOperationHandle到字典OnDestroy时统一Release避免分散管理。曾有一个项目Addressables.LoadAssetAsyncSprite()后未Release导致100个Sprite常驻内存。Memory Profiler的Ref Count列直接标红显示12点击后路径清晰指向InventorySystem.LoadIcon()函数——这比翻1000行代码高效百倍。4.4 WebGL背景透明的内存陷阱一个被忽视的渲染层泄漏标题中提到的“unity webgl 背景透明”热搜词背后藏着一个深度内存陷阱。当WebGL构建启用TransparencyPlayer Settings → Publishing Settings → TransparencyUnity会创建RenderTexture作为后台缓冲其大小等于Canvas分辨率。若Canvas设为Scale With Screen Size在高分屏设备上RenderTexture可能达4096x2160单张占用32MBRGBA32格式。Memory Profiler诊断路径Assets视图中筛选RenderTexture按Native Size排序找到超大尺寸实例。Show References显示Retained By: Camera.main.targetTexture。进一步发现Camera的clearFlags为Dont Clear导致RenderTexture被长期持有。解决方案降低Canvas缩放比例或固定分辨率。在Camera.OnPreRender中动态创建/释放RenderTexture而非全局持有。或改用Camera.RenderToCubemap等轻量替代方案。这个案例说明Memory Profiler的价值不仅在于找Bug更在于揭示引擎特性与业务需求间的隐性冲突。5. 常见问题速查表从报错到误操作的全场景应对问题现象根本原因排查步骤解决方案我的实操备注Snapshot按钮灰色不可用Scripting Runtime Version不匹配或Package未激活1. 检查Player Settings → Other Settings → Scripting Runtime Version2. 在Package Manager中刷新Preview Packages将Runtime设为.NET 4.x重启编辑器2022.3版本中若项目使用Universal RP需确保com.unity.render-pipelines.universal版本≥14.0.0否则Memory Profiler依赖冲突真机连接后无数据设备端Profiler服务未启动或网络不通1. iOS检查Settings → Privacy → Analytics2. Android执行adb shell getprop debug.unity.profiler3. WebGL检查Chromechrome://inspect配置iOS开启AnalyticsAndroid执行adb shell setprop debug.unity.profiler 1Pico Neo 3需在Developer Options中开启USB Debugging和Unity Profiler双开关缺一不可引用链显示Unknown MethodIL2CPP符号未导出或Development Build未启用1. 检查Player Settings → Publishing Settings → Strip Engine Code2. 确认Build Settings中勾选Development Build关闭Strip Engine Code确保Development Build启用符号导出会使APK增大建议在CI中单独构建Debug-Profiler版本与发布版分离Delta分析中Added Objects为0但内存上涨原生内存增长未被计入Managed Heap1. 切换到Native Memory视图2. 筛选Mesh、RenderTexture、Texture2D按Native Size排序优化Mesh顶点数关闭Read/Write Enabled及时Release()RenderTextureTexture2D的Native Size才是真实开销Managed Size仅指托管堆中的指针大小常被严重低估UI元素关闭后GameObject未减少DontDestroyOnLoad或事件系统持有引用1. 在Objects视图筛选GameObject按Ref Count排序2. 对高引用数对象Show References检查DontDestroyOnLoad调用点EventSystem的selectedGameObjectInputField的onEndEdit委托InputField的onEndEdit若绑定匿名函数会隐式持有this引用改用命名方法或weak reference包装Resources.Load资源在Snapshot中显示为Not LoadedResources文件夹结构错误或路径拼写错误1. 在Project Window中右键资源→Reimport2. 检查Resources.Load(path/to/asset)路径是否含扩展名Resources.Load路径不含扩展名且必须在Resources子文件夹内Resources.LoadAll会加载整个文件夹易造成批量泄漏优先用Addressables替代Addressables资源Ref Count始终为1AsyncOperationHandle未被Release或AutoRelease未启用1. 搜索InstantiateAsync调用点2. 检查Addressables.Release()是否执行启用Addressables的AutoRelease策略或在OnDestroy中统一ReleaseAddressables.InstantiateAsync返回的AsyncOperationHandle必须Release否则引用计数永不归零最后分享一个血泪教训我在一个微信小游戏项目中为优化启动速度将所有UI Prefab打包进Addressables但未设置AutoRelease。Memory Profiler显示GameObject实例数随页面切换持续增长Delta分析发现Ref Count始终为1。根源是Addressables.InstantiateAsync().Completed回调中InstantiateAsync返回的AsyncOperationHandle被闭包捕获而闭包又持有MonoBehaviour形成循环引用。解决方案在回调中立即handle.Release()或改用await handle.Task避免闭包捕获。这个工具的价值从来不在它能显示多少数字而在于它把抽象的内存问题还原成一行行可触摸、可编辑、可验证的代码事实。当你第一次从引用链里看到自己的变量名出现在GC Root路径上那种“原来罪魁祸首是我自己”的顿悟感就是性能优化最真实的开始。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门