Unity内存优化实战:从对象池到资源管理的系统性解决方案

Unity内存优化实战:从对象池到资源管理的系统性解决方案
1. 项目概述为什么Unity内存优化是每个开发者的必修课如果你在Unity开发中遇到过游戏运行一段时间后越来越卡、偶尔出现不明原因的闪退或者打包成WebGL后加载时间长得让人失去耐心那么你大概率正在经历内存问题的折磨。这不仅仅是“优化一下”那么简单它直接关系到玩家的留存率和产品的口碑。今天我们不谈那些泛泛而谈的“少用Find”、“多用缓存”而是深入到引擎底层和代码设计层面从最经典的对象池技术到更高级的缓存策略为你梳理一套完整、可落地的Unity内存优化解决方案。无论你是正在被GC垃圾回收卡顿困扰的移动端开发者还是为WebGL包体初始化缓慢而头疼的前端工程师这套方法都能帮你找到症结所在。内存优化不是一个孤立的技巧而是一个系统工程。它贯穿于资源加载、运行时对象管理、渲染管线乃至脚本编写的每一个环节。很多开发者知道“对象池”这个概念但往往只停留在“复用GameObject”的层面忽略了数据缓存、AssetBundle管理、纹理压缩策略等更深层次的内存消耗点。我们将从原理出发解释为什么这些地方会成为性能瓶颈然后给出具体的、带有参数和代码示例的实操方案。你会发现优化之后不仅帧率更稳定包体更小整个项目的可维护性也会大大提升。2. 核心思路拆解从“治标”到“治本”的优化层次2.1 第一层运行时对象管理——对象池的精髓与误区对象池几乎是所有Unity性能优化文章都会提到的第一课但很多人对其理解过于片面。它的核心目标并非仅仅是“减少Instantiate和Destroy的调用”更深层次的目标是平抑由GC垃圾回收引发的帧率波动。为什么GC是万恶之源Unity使用的C#托管内存环境当堆内存上不再被引用的对象积累到一定程度时GC线程会启动来回收这些内存。这个过程是“停止-复制”或“标记-清除”会挂起所有托管代码线程在大多数情况下包括你的游戏逻辑线程导致游戏卡顿。你看到的Profiler里那根突然飙升的GC.Collect帧时间线就是它造成的。一个基础的对象池实现大家都会写预创建一批对象用的时候取出不用的时候放回。但这里有几个关键误区池子大小是静态的很多教程的池子初始化后大小固定。但在实际游戏中不同关卡、场景对同一类对象如子弹、敌人的需求量差异巨大。静态池要么造成内存浪费池子过大要么导致运行时动态扩容依然触发GC。只池化GameObject这是最大的浪费。一个复杂的敌人Prefab可能包含MeshRenderer、Animator、多个脚本组件。当你把整个GameObject放回池子并SetActive(false)时这些组件大部分仍在内存中活跃。更精细的做法是将数据与表现分离。将敌人的生命值、位置等数据放在一个可复用的结构体或类对象中池化而渲染实体只在需要时关联或生成。忽视池对象的“重置”成本从池中取出的对象必须被完全重置到一个干净的初始状态。这不仅仅是transform.position Vector3.zero还包括所有脚本内部变量的重置、动画状态机的重启、粒子系统的停止与清理。忘记重置任何一个状态都可能引发诡异的Bug。正确的对象池设计思路它应该是一个智能的、可伸缩的、分层的管理系统。对于高频创建/销毁的简单对象如子弹、伤害数字使用传统的GameObject池。对于复杂的实体如敌人、NPC采用“数据池实体工厂”的模式数据部分池化表现部分按需加载或卸载。2.2 第二层资源生命周期管理——超越Resources与AssetBundle资源是内存占用的大头包括纹理、网格、音频、动画等。Unity提供了Resources、AssetBundle和Addressables等加载方式但如果不加以管理极易造成内存泄漏或冗余。Resources文件夹的陷阱所有放在Resources文件夹下的资源在构建时会被打包到一个巨大的序列化文件中。游戏启动时这个文件的索引结构会被全部加载到内存。这意味着即使你从未加载过Resources里的某个纹理你依然在为它的文件名和路径信息支付内存开销。更糟糕的是使用Resources.Load同步加载资源后如果你没有正确调用Resources.UnloadAsset或Resources.UnloadUnusedAssets这些资源会一直驻留内存。因此对于大型项目最佳实践是彻底放弃使用Resources文件夹。AssetBundle的精细化管理AssetBundle是更主流的方式但它同样需要精细的策略。依赖关系多个AssetBundle可能共享同一个材质或着色器。如果你加载了Bundle A和B它们都依赖材质M那么M会被加载两次吗不会Unity会智能管理。但如果你先加载A加载了M然后卸载AM会因为被B引用而保留。但如果你再卸载BM才会被真正卸载。理解这种依赖链至关重要。卸载策略AssetBundle.Unload(false)和AssetBundle.Unload(true)是天壤之别。Unload(false)只卸载AssetBundle文件本身在内存中的镜像但已经从中加载出来的资源如Texture、GameObject会保留。如果你之后还想通过另一个Bundle加载同名资源就会产生重复。Unload(true)则暴力卸载所有从中加载出的资源可能导致场景中正在使用的对象丢失引用而变“粉红”丢失材质。通常推荐使用Unload(false)并配合引用计数或场景生命周期来管理资源卸载。Addressables的现代化方案Addressable Asset System是Unity官方推荐的下一代资源管理系统。它本质上是对AssetBundle的封装和增强提供了异步加载、依赖管理、内存管理通过引用计数等开箱即用的功能。它的核心优势在于将资源与路径解耦你通过一个“地址”字符串来加载资源系统自动处理底层Bundle的加载、缓存和卸载。对于新项目强烈建议直接使用Addressables。2.3 第三层渲染与图形内存——隐藏的“内存杀手”即使你的代码和资源管理得天衣无缝图形API如OpenGL ES、Metal、Vulkan和GPU驱动仍然会占用大量内存这部分通常在Unity Profiler的“GPU”或“Graphics”部分体现。纹理内存这是图形内存的大头。一张1024x1024的RGBA32纹理在CPU端Read/Write enabled和GPU端各占约4MB内存。如果开启了Mipmap内存占用会增加约33%。优化策略包括使用合适的压缩格式在移动端使用ASTC、ETC2或PVRTC可以将纹理内存减少到原来的1/4甚至更少。在Unity导入设置中根据平台选择。关闭不必要的Read/Write只有需要在脚本中通过GetPixels或SetPixels动态修改的纹理才需要开启此选项它会保留一份CPU端的拷贝。Mipmap策略对于UI纹理或永远靠近相机的2D精灵关闭Mipmap可以节省内存。对于3D场景中的物体Mipmap能提升渲染性能和视觉质量需要权衡。最大尺寸限制不要无脑使用4K纹理。根据物体在屏幕上的最大可能显示尺寸来设定纹理的Max Size。一个在手机上最多显示256像素的物体用1024的纹理就是浪费。网格内存确保导入的网格没有不必要的多边形数量。使用LODLevel of Detail系统在物体远离相机时使用面数更少的网格版本。帧缓冲区与渲染目标屏幕分辨率越高帧缓冲内存越大。后处理效果如Bloom、Depth of Field会创建中间渲染纹理RenderTexture这些也是内存消耗点。在移动设备上需谨慎使用高消耗的后处理。2.4 第四层托管堆与代码实践——防微杜渐这一层关注C#脚本本身如何分配和管理内存。避免在Update中分配新对象这是最经典的准则。常见的陷阱包括Debug.Log在发布版本中虽然不会输出但字符串拼接操作依然会发生产生GC Alloc。应使用条件编译#if UNITY_EDITOR包裹。foreach循环在Unity的老版本Mono编译器或某些IL2CPP配置下foreach在值类型集合上可能产生装箱Boxing分配。对于性能关键的循环优先使用for循环。字符串操作string.Format、拼接都会产生新的字符串对象。在频繁调用的地方如UI文本更新考虑使用StringBuilder。Lambda表达式与闭包在帧更新中定义匿名方法或捕获外部变量可能会在堆上分配内存。如果该操作每帧都执行应考虑将其提取为类的成员方法。使用结构体struct替代类class对于小型、短暂存在的数据如坐标、颜色、简单的状态数据使用结构体。结构体是值类型分配在栈上或作为其他对象的一部分在堆上其生命周期结束后自动回收不经过GC。但要注意避免结构体过大导致的拷贝性能开销。数组与列表复用与对象池思想类似对于需要频繁扩容的ListT可以预估一个容量或者复用已有的列表对象使用Clear()方法清空内容而不是new ListT()。3. 核心方案实现一套可落地的组合拳3.1 实现一个智能、可伸缩的对象池系统我们不再实现一个简单的ListGameObject池而是设计一个更通用的、支持任意组件类型、可自动伸缩的池子。using System.Collections.Generic; using UnityEngine; public class GameObjectPoolT where T : Component { private QueueT pool new QueueT(); private T prefab; private Transform parent; // 构造函数指定预制体、初始大小和父节点 public GameObjectPool(T prefab, int initialSize, Transform parent null) { this.prefab prefab; this.parent parent; for (int i 0; i initialSize; i) { T obj CreateNewObject(); obj.gameObject.SetActive(false); pool.Enqueue(obj); } } private T CreateNewObject() { T newObj Object.Instantiate(prefab, parent); // 可以在这里为对象添加一个标识组件方便管理和调试 newObj.gameObject.AddComponentPooledObject().pool this; return newObj; } public T Get() { if (pool.Count 0) { // 池空自动扩容。这里可以加入扩容策略比如按百分比扩容。 Debug.LogWarning($Pool for {prefab.name} is empty, creating new one.); return CreateNewObject(); } T obj pool.Dequeue(); obj.gameObject.SetActive(true); // 发送一个消息通知对象已被取出可以进行自定义重置 obj.SendMessage(OnPoolGet, SendMessageOptions.DontRequireReceiver); return obj; } public void Return(T obj) { obj.gameObject.SetActive(false); // 发送一个消息通知对象即将被回收可以进行清理 obj.SendMessage(OnPoolReturn, SendMessageOptions.DontRequireReceiver); // 可选重置Transform到池根节点下避免场景杂乱 if (parent ! null) { obj.transform.SetParent(parent); } pool.Enqueue(obj); } // 清空池子谨慎使用通常用于场景切换时 public void Clear() { while (pool.Count 0) { T obj pool.Dequeue(); if (obj ! null) { Object.Destroy(obj.gameObject); } } pool.Clear(); } } // 标识组件用于对象自我管理回池 public class PooledObject : MonoBehaviour { public GameObjectPoolComponent pool; // 这里用Component泛型实际使用需强转 public void OnDisable() { // 不推荐在OnDisable中自动回池因为SetActive(false)可能由多种原因触发。 // 最佳实践是由生成该对象的控制器显式调用回池方法。 } // 提供一个方法供对象自己请求回池 public void ReturnToPool() { if (pool ! null) { // 这里需要根据实际池化对象的类型进行转换设计上可以更优雅此处为示例 if (this is Component comp) { // 需要一种方式调用泛型池的Return这里简化处理实际项目可能需要反射或接口。 // 更常见的做法是池管理器持有所有池的引用通过GameObject或ID来归还。 } } } }使用示例与注意事项public class BulletManager : MonoBehaviour { public Bullet bulletPrefab; private GameObjectPoolBullet bulletPool; void Start() { // 初始化一个容量为20的子弹池 bulletPool new GameObjectPoolBullet(bulletPrefab, 20, this.transform); } void Update() { if (Input.GetMouseButtonDown(0)) { Bullet bullet bulletPool.Get(); bullet.transform.position transform.position; bullet.transform.rotation transform.rotation; bullet.Shoot(transform.forward); // 调用子弹的初始化方法 } } // 这个方法由子弹对象在击中目标或超出范围后调用 public void ReturnBulletToPool(Bullet bullet) { bulletPool.Return(bullet); } } // Bullet.cs public class Bullet : MonoBehaviour { private Rigidbody rb; void Awake() { rb GetComponentRigidbody(); } public void Shoot(Vector3 direction) { rb.velocity direction * 50f; // 可以在这里启动一个协程3秒后自动回池避免忘记回收 StartCoroutine(AutoReturn(3f)); } private System.Collections.IEnumerator AutoReturn(float delay) { yield return new WaitForSeconds(delay); // 通知管理器回收自己。更好的方式是用事件这里简单演示。 FindObjectOfTypeBulletManager()?.ReturnBulletToPool(this); } // 可选实现池化消息接口 void OnPoolGet() { rb.velocity Vector3.zero; rb.angularVelocity Vector3.zero; // 重置所有状态 } void OnPoolReturn() { StopAllCoroutines(); // 停止可能还在运行的协程 } }注意上述示例中的PooledObject组件和自动回池逻辑是一种设计思路。在大型项目中更推荐使用一个中央化的池管理器来管理所有类型的对象池并通过唯一ID或类型来存取避免FindObjectOfType这类性能消耗较大的操作。同时自动回池的协程也可能带来额外的GC AllocWaitForSeconds对于超高频对象需谨慎。3.2 构建基于Addressables的资源加载与缓存策略Addressables的核心是“按地址加载”和“引用计数”。我们设计一个简单的资源服务类来封装常用操作并加入自定义的缓存层。using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.ResourceManagement.AsyncOperations; using System.Collections.Generic; public class ResourceCacheService : MonoBehaviour { private static ResourceCacheService instance; public static ResourceCacheService Instance { get { return instance; } } // 缓存字典Key为资源地址Value为加载句柄和引用计数 private Dictionarystring, CachedAsset assetCache new Dictionarystring, CachedAsset(); private class CachedAsset { public AsyncOperationHandle handle; public int refCount 0; } void Awake() { if (instance ! null instance ! this) { Destroy(gameObject); return; } instance this; DontDestroyOnLoad(gameObject); } // 异步加载资源并增加引用计数 public AsyncOperationHandleT LoadAssetAsyncT(string address) { if (assetCache.TryGetValue(address, out CachedAsset cached)) { // 已缓存增加引用计数并返回句柄 cached.refCount; Debug.Log($Asset {address} fetched from cache, refCount: {cached.refCount}); return cached.handle.ConvertT(); } // 未缓存发起异步加载 AsyncOperationHandleT handle Addressables.LoadAssetAsyncT(address); assetCache[address] new CachedAsset { handle handle, refCount 1 }; handle.Completed (op) { if (op.Status AsyncOperationStatus.Succeeded) { Debug.Log($Asset {address} loaded successfully.); } else { Debug.LogError($Failed to load asset {address}: {op.OperationException}); // 加载失败从缓存中移除 assetCache.Remove(address); } }; return handle; } // 释放资源减少引用计数计数为0时真正释放 public void ReleaseAsset(string address) { if (assetCache.TryGetValue(address, out CachedAsset cached)) { cached.refCount--; Debug.Log($Asset {address} released, refCount: {cached.refCount}); if (cached.refCount 0) { Debug.Log($Asset {address} reference count is zero, releasing...); Addressables.Release(cached.handle); assetCache.Remove(address); } } else { Debug.LogWarning($Trying to release an asset not in cache: {address}); } } // 场景切换时可以强制释放所有未被引用的资源谨慎使用 public void ReleaseUnusedAssets() { Liststring toRemove new Liststring(); foreach (var kvp in assetCache) { if (kvp.Value.refCount 0) { Addressables.Release(kvp.Value.handle); toRemove.Add(kvp.Key); } } foreach (var key in toRemove) { assetCache.Remove(key); } Debug.Log($Released {toRemove.Count} unused assets.); // 同时也可以调用Addressables自身的清理 // Addressables.CleanBundleCache(); // 清理不再被任何句柄引用的AssetBundle缓存 } }使用模式public class CharacterLoader : MonoBehaviour { public string characterPrefabAddress CHAR_Hero_01; private GameObject loadedCharacter; private AsyncOperationHandleGameObject loadHandle; async void Start() { // 加载角色预制体 loadHandle ResourceCacheService.Instance.LoadAssetAsyncGameObject(characterPrefabAddress); loadedCharacter await loadHandle.Task; // 使用async/await等待需引用UnityEngine.ResourceManagement库 if (loadedCharacter ! null) { Instantiate(loadedCharacter, transform.position, Quaternion.identity); } } void OnDestroy() { // 当这个加载器销毁时如切换场景释放对资源的引用 if (loadHandle.IsValid()) { ResourceCacheService.Instance.ReleaseAsset(characterPrefabAddress); } } }缓存策略进阶上述缓存是基于“地址”的。你还可以扩展为基于“标签”或“分类”的缓存策略。例如所有“UI”资源常驻内存所有“场景_关卡1”的资源在离开关卡1时整体卸载。Addressables本身支持通过标签加载资源组可以结合使用。3.3 纹理与网格内存的针对性优化配置这部分优化主要在Unity Editor的导入设置Import Settings中完成。纹理优化清单在Project窗口选中纹理在Inspector中设置Texture Type根据用途选择正确类型。UI用Sprite (2D and UI)普通贴图用Default法线贴图用Normal map光照贴图用Lightmap。选错类型可能导致压缩格式不可用或产生不必要的数据。Max Size绝对不要无脑2048。问自己这个纹理在屏幕上最大会显示多大对于手机图标128或256足矣对于背景图根据Canvas分辨率设置。CompressionPC/主机通常使用BC7RGBA高质量或BC1RGB无Alpha。BC3RGBA有Alpha。Android优先使用ASTC根据设备支持选择块大小如ASTC 6x6是质量和内存的较好平衡。老设备回退到ETC2OpenGL ES 3.0以上或ETCES2.0。iOS优先使用ASTC。老设备iPhone 5s以前可使用PVRTC。注意Crunch压缩是一种基于DXT或ETC的有损压缩能显著减小磁盘上的包体大小但运行时需要解压到GPU内存其内存占用与未压缩格式相同。它主要用于减小下载包体积。Generate Mip Maps3D物体通常开启2D UI精灵和粒子纹理通常关闭。Read/Write Enabled务必关闭除非你明确需要在运行时通过脚本修改纹理像素数据。网格优化清单Model选项卡Read/Write Enabled和纹理一样除非需要运行时修改网格顶点数据否则必须关闭。开启会使网格数据在内存中保留两份CPU一份GPU一份。Optimize Mesh勾选让Unity重新排序网格的顶点和三角形索引以提高GPU缓存命中率。Generate Colliders按需勾选。如果使用简单的碰撞体如Box、Capsule不要在这里生成而是手动添加碰撞体组件因为这里生成的往往是高精度的Mesh Collider性能开销大。Rig选项卡对于人形角色正确配置Avatar。对于非人形或静态网格Animation Type选择None可以节省相关开销。Animations选项卡对于非动画模型直接Import Constraints、Import Animation全部取消勾选。对于动画模型检查并优化动画剪辑的压缩设置Anim. Compression使用Optimal或Keyframe Reduction并调整Rotation Error和Position Error容忍度在视觉不失真的前提下减少关键帧数量。3.4 代码层面的内存分配规避实战在Update或FixedUpdate中使用ProfilerWindow Analysis Profiler的CPU Usage模块勾选Deep Profile或使用Profiler.BeginSample/EndSample来定位托管内存分配GC Alloc的热点。常见陷阱与优化示例字符串处理// 坏每帧都分配新字符串 void Update() { scoreText.text Score: currentScore; } // 好使用StringBuilder复用 private System.Text.StringBuilder sb new System.Text.StringBuilder(32); void Update() { sb.Clear(); sb.Append(Score: ); sb.Append(currentScore); scoreText.text sb.ToString(); // 这里仍有分配但频率和大小可控 } // 更好对于固定格式使用缓存字符串或直接赋值如果currentScore变化不频繁 // 或者仅在分数变化时更新UI而不是每帧。装箱Boxing// 坏枚举值在添加到ArrayList或作为object参数时会发生装箱 enum State { Idle, Walk, Run } ArrayList stateList new ArrayList(); stateList.Add(State.Walk); // 装箱发生在这里 // 好使用泛型集合 ListState stateList new ListState(); stateList.Add(State.Walk); // 无装箱Lambda与闭包// 坏每帧都定义一个新的委托和闭包 void Update() { someList.RemoveAll(item item.isExpired); // 这个lambda会产生GC Alloc } // 好将谓词定义为类的静态或成员方法 private static bool IsExpired(Item item) item.isExpired; void Update() { someList.RemoveAll(IsExpired); // 无额外分配 }Unity API调用GetComponentT()本身分配很小但频繁调用仍需优化。在Awake或Start中缓存引用。FindGameObjectsWithTag、FindObjectOfType等函数开销巨大绝对禁止在每帧中调用。必须在初始化时缓存结果。Camera.main内部相当于FindGameObjectWithTag(MainCamera)也应缓存。4. 性能剖析与问题排查实战指南理论再好不如实战。当游戏出现内存问题卡顿、闪退时你需要一套系统的排查方法。4.1 使用Unity Profiler进行内存快照分析Profiler是你的第一道防线。打开Window Analysis Profiler切换到Memory模块。获取并比较快照在游戏启动后、进入主菜单时点击Take Sample获取一个基线快照Baseline。进行你认为可能产生内存泄漏或增长的操作如进入一个关卡然后退出。再次点击Take Sample获取第二个快照。点击Compare to baseline或Compare to previousProfiler会高亮显示两次快照之间的差异。分析差异Managed Heap增长关注System.Object[]、String、YourScriptClass等。如果退出关卡后本该释放的脚本对象或资源引用仍然存在说明存在非预期的引用导致GC无法回收。这是托管内存泄漏的典型标志。Native/Graphics增长关注Texture2D、Mesh、Material等。如果退出场景后纹理内存没有下降可能是AssetBundle没有正确卸载或者静态变量、单例中持有了对这些资源的引用。注意“Other”项有时内存被归类到“Other”这可能包括Unity引擎内部数据结构、第三方插件分配的内存等。如果这里异常增长需要结合代码和插件文档分析。使用Simple和Detailed模式在Memory模块的顶部可以选择模式。Simple模式给你一个总览Detailed模式可以展开看到每一个具体的资源实例、GameObject实例以及它们的引用链。这是查找“谁还在引用这个对象”的利器。4.2 使用Unity Frame Debugger分析渲染内存如果怀疑是渲染导致的内存或性能问题打开Window Analysis Frame Debugger。启用后游戏会暂停你可以逐帧、逐绘制命令Draw Call地查看渲染过程。关注SetPass Calls这是Draw Call数量的一个近似。数量过多如超过100-200是性能瓶颈的常见原因。可以通过静态批处理Static Batching、动态批处理Dynamic Batching、GPU Instancing、SRP Batcher等技术来合并。渲染纹理RenderTexture查看每一帧创建了哪些临时RT它们的大小和格式是什么。不必要的全屏RT或高精度RT会消耗大量内存和带宽。材质和着色器变体频繁的材质切换SetPass Call高和大量的着色器变体Shader Variants也会影响性能。使用Shader Variant Collection来打包和预热需要的变体。4.3 常见内存问题排查清单问题现象可能原因排查工具与方法游戏运行越久越卡偶尔卡顿一下托管堆内存碎片化与GC触发。频繁的小对象分配导致堆碎片GC频繁运行。Profiler Memory模块查看GC Alloc曲线和Managed Heap大小。使用UnityEngine.Profiling.Profiler.enabled在代码块前后记录。切换场景后内存不下降多次切换后崩溃资源泄漏。AssetBundle未卸载或静态类、单例、事件监听器持有对场景中对象的引用。Profiler对比快照查看Texture2D,Mesh,Material实例数是否只增不减。使用WeakReference或确保在OnDestroy中取消事件订阅。WebGL版本初始化/加载时间极长资源压缩与传输。未使用合适的纹理压缩或AssetBundle未进行压缩LZ4/LZMA。检查Build Settings中Compression Method是否为LZ4推荐平衡大小与解压速度。检查纹理导入格式是否为平台专用压缩。移动设备上纹理内存超标纹理格式与大小不当。使用了RGBA32未压缩纹理或Max Size设置过大。在Editor中Stats窗口查看纹理内存。使用Texture2D.GetRawTextureData等API在运行时检查纹理格式需开启Read/Write。实例化大量相同物体时帧率骤降缺少对象池或对象池重置成本过高。Profiler CPU模块查看Instantiate和Destroy的耗时。优化池化对象的OnPoolGet重置逻辑避免在其中进行昂贵的查找或计算。游戏中有大量材质实例Material Instances动态修改材质属性导致Unity创建了新的材质实例。在Frame Debugger或通过代码检查material和sharedMaterial的区别。尽量修改sharedMaterial的属性或使用MaterialPropertyBlock来修改渲染器属性而不创建新实例。4.4 高级工具Memory Profiler与Heap Explorer对于更深入的内存分析Unity官方提供了Memory Profiler包通过Package Manager安装。它比内置Profiler的Memory模块更强大可以生成完整的内存快照并可视化对象之间的引用关系图。捕获快照在游戏运行时点击Capture按钮。建议在问题发生前后各捕获一次。分析引用链在快照中你可以搜索任何一个对象类型如你的MonoBehaviour脚本查看所有存活实例并展开其引用树Referenced By。这能清晰地告诉你是哪个根对象Root一直保持着对这个对象的引用导致其无法被GC回收。常见的根对象包括静态变量、活跃的MonoBehaviour、被委托Delegate引用的对象等。比较快照和Profiler一样它可以对比两个快照精确找出哪些对象被意外地保留了下来。实操心得内存优化是一个持续的过程而不是一蹴而就的任务。建议在项目早期就建立性能基线Baseline并在每个重要的开发里程碑如Alpha, Beta进行全面的性能剖析。将内存和性能检查纳入你的日常测试流程就像写单元测试一样。记住预防永远比治疗更有效。当你养成了“分配意识”和“引用意识”很多内存问题在编码阶段就已经被避免了。

最新新闻

日新闻

周新闻

月新闻