资讯详情

资讯详情

Memory Profiler:看清内存被谁吃掉了

先分清两个工具Unity 里有两个名字相近的东西,很多人搞混:Profiler → Memory 模块(内置) · 只给总量和粗分类 · 实时曲线,看趋势 · 用来回答「内存有没有在涨」 Memory Profiler(独立包,Package Manager 安装) · 抓完整快照,精确到每个对象 · 能查引用链、找重复资源、对比两个快照 · 用来回答「内存被谁占了、为什么没释放」Window → Package Manager → Unity Registry → 搜 Memory Profiler → Install Window → Analysis → Memory Profiler内置的用来发现问题,独立包用来定位问题。下面主要讲后者。一、内存的构成动手之前得先知道 Unity 的内存分成哪几块,不然看到数字不知道该紧张哪个。进程总内存 ├─ Native 内存(C 侧,最大头) │ ├─ 纹理 ← 通常占一半以上 │ ├─ 网格 │ ├─ 音频 │ ├─ 动画 Clip │ ├─ Shader / 材质 │ └─ Unity 引擎自身 │ ├─ 托管内存(Managed Heap,C# 侧) │ ├─ 你的 C# 对象 │ ├─ 字符串 │ └─ 装箱产生的垃圾 │ ├─ 托管堆的保留空间(Reserved,已申请但空着) │ └─ 其他 ├─ IL2CPP 元数据 ├─ 图形驱动占用 └─ 插件 / 原生库关键认知:❗ 托管堆(C# 内存)在手游里通常只占总量的 10%~20% 很多人花大量时间优化 GC 和 C# 分配, 结果包体内存还是爆 —— 因为 90% 是纹理、网格、音频 → 先看资源,再看代码❗ 托管堆只增不减(Unity 的 Boehm GC 不归还内存) 峰值一次 200MB,之后降到 50MB → 系统看到的仍然是 200MB 被占着 所以要防的是「峰值」,不是「平均值」二、抓快照Memory Profiler 窗口 → 顶部选择目标 Editor 编辑器(数据不准,仅参考) Android/iOS Player 真机 ← 必须用这个 → 点 Capture New Snapshot⚠️ 必须在真机上抓 编辑器里的内存包含: · Scene 视图的额外渲染资源 · 所有 Shader 变体(真机只有打包的那些) · 未压缩格式的纹理(Editor 平台设置) · 编辑器自身占用 编辑器数据和真机能差出一倍以上抓快照会短暂卡顿,文件存在项目/MemoryCaptures/,可以留档做对比。三、三个主要视图Summary:先看总量和大头┌─ Memory Usage Overview ──────────────────────────┐ │ Total 612 MB │ │ │ │ ████████████████░░░░░░░░░ │ │ │ │ Native Objects 398 MB ← 资源,最该看的 │ │ Managed Heap 68 MB │ │ Virtual Machine 32 MB │ │ Graphics Driver 85 MB │ │ Executables 41 MB │ │ Untracked 18 MB │ └──────────────────────────────────────────────────┘判断标准(中端 Android,4GB 内存机型): 总量 600 MB 安全 600 ~ 900 MB 偏高,低端机会有风险 1 GB 大概率在部分机型上 OOM iOS 更严格,超过物理内存的一定比例就会被系统杀Unity Objects:按类型排序,这是主战场Type Count Size ▼ ────────────────────────────────────────── Texture2D 1240 312 MB ← 几乎总是第一名 Mesh 380 42 MB AudioClip 95 38 MB AnimationClip 620 24 MB Material 410 8 MB Shader 88 12 MB MonoScript 3200 6 MB点开 Texture2D 能看到每一张的明细:Name Size Format Resolution ───────────────────────────────────────────────────────────── ui_background_main 21.3 MB RGBA32 2048x2048 ❌ char_boss_albedo 5.5 MB ASTC_6x6 4096x4096 ⚠️ env_rock_01 0.7 MB ASTC_6x6 1024x1024 ✅第一行这种是最常见的问题:UI 美术直接导入 PNG,没设压缩格式,一张图吃掉 21MB。All Of Memory:看完整分布树形展开所有内存,包括 Native 分配器、图形驱动、未追踪部分。排查总量对不上时用它。四、最常见的五类问题① 未压缩纹理症状:Texture2D 里某几张异常大,Format 显示 RGBA32 / RGB24 / ARGB32 原因:导入时没设压缩,或者勾了 Read/Write Enabled 处理: · 设成 ASTC(iOS/Android 都支持)或 ETC2(Android 兜底) · 关掉 Read/Write Enabled(开着会在内存里留一份 CPU 副本,内存翻倍) · UI 大图考虑 ASTC 8x8 或切成图集#ifUNITY_EDITORusingUnityEditor;usingUnityEngine;publicstaticclassTextureAudit{[MenuItem(Tools/审查纹理内存风险)]privatestaticvoidAudit(){foreach(varguidinAssetDatabase.FindAssets(t:Texture2D)){varpathAssetDatabase.GUIDToAssetPath(guid);if(path.StartsWith(Packages/))continue;varimpAssetImporter.GetAtPath(path)asTextureImporter;if(impnull)continue;varsimp.GetPlatformTextureSettings(Android);vartexAssetDatabase.LoadAssetAtPathTexture2D(path);booluncompresseds.formatisTextureImporterFormat.RGBA32orTextureImporterFormat.RGB24orTextureImporterFormat.ARGB32;if(uncompressed||imp.isReadable||tex.width2048)Debug.LogWarning($[纹理]{path}\n${tex.width}x{tex.height}格式{s.format}$R/W{(imp.isReadable?开 ❌:关)},tex);}}}#endif② 重复资源同一张贴图被打进多个 AssetBundle,或者 Prefab 引用混乱,导致内存里存在多份。Memory Profiler 里的表现: Name Size Instance ID ──────────────────────────────────────────────── char_hero_albedo 4.2 MB -14822 ┐ char_hero_albedo 4.2 MB -31905 ├ 同名多份 ❌ char_hero_albedo 4.2 MB -42117 ┘排查: · Unity Objects 视图里按 Name 排序,找同名多实例 · Addressables 检查 Group 划分,共享资源单独一组 · AssetBundle 用依赖分析确认公共资源被正确抽出重复资源是 Bundle 划分不当的典型征兆,也是手游里最容易白白浪费上百 MB 的地方。③ 场景切换后资源没释放这是用快照对比功能的主要场景。操作流程: ① 在主界面抓快照 A ② 进战斗关卡,打完 ③ 回到主界面 ④ 抓快照 B ⑤ Memory Profiler → Compare Snapshots → 选 A 和 B对比结果: Type A B Delta ────────────────────────────────────────── Texture2D 180 MB 312 MB 132 MB ❌ 战斗资源没卸 Mesh 22 MB 48 MB 26 MB ❌ GameObject 890 2140 1250 ❌ 对象泄漏回到同一个界面,内存应该回到接近的水平。涨了就是有东西没放掉。常见原因: · static 字段持有了资源引用 · 事件没反注册,订阅者被一直引用着 · 对象池只进不出 · AssetBundle 没 Unload(true) · DontDestroyOnLoad 的对象越积越多 · Addressables 没调 Release④ 引用链:为什么没被释放这是 Memory Profiler 最有价值的功能。选中任意对象,右侧面板:┌─ References To This Object ───────────────┐ │ ▼ Texture2D boss_albedo │ │ └─ Material boss_body │ │ └─ MeshRenderer │ │ └─ GameObject Boss │ │ └─ ListGameObject │ │ └─ EnemyManager._cachedList │ │ └─ static EnemyManager.Instance ← 根源 └───────────────────────────────────────────┘顺着链往上找到持有者,就知道该清哪里了。上面这个例子的问题是单例的缓存列表没清空。⚠️ 看到引用链的终点是 static,基本就定位了 GC 不会回收被 static 引用的对象 —— 它是 GC Root⑤ 托管堆峰值Managed Heap Used 48 MB Reserved 210 MB ← 差距很大 曾经有过大峰值Reserved 远大于 Used,说明某个时刻分配了大量临时对象。Unity 不会把这部分还给系统,所以峰值一旦上去就下不来。常见的峰值来源: · 一次性 new 很大的数组 · JSON 反序列化整个配置表 · 字符串拼接(尤其在循环里) · LINQ 在热路径上 · 一帧内实例化上百个对象// ❌ 峰值杀手:一次读入全部,产生大量临时对象varallJsonUtility.FromJsonHugeConfig(File.ReadAllText(path));// ✅ 分批 / 流式处理,或改用二进制格式// ✅ 配置表考虑用 ScriptableObject 预先序列化好五、一个容易忽略的点:资源卸载的时机// 卸载不再被引用的资源Resources.UnloadUnusedAssets();// 返回 AsyncOperation,有明显耗时// 配合 GCSystem.GC.Collect();正确的调用时机: 场景切换的加载界面里 ← 唯一合适的地方 ① 先断开所有引用(清 static、清池、反注册事件) ② SceneManager.LoadScene / UnloadSceneAsync ③ Resources.UnloadUnusedAssets() ④ GC.Collect() ❌ 不要在游戏进行中调,会造成明显卡顿 ❌ 不要指望它能清掉还被引用的东西 —— 先断引用才有用顺序很重要。引用没断就调 UnloadUnusedAssets,什么都不会释放。六、配合内置 Profiler 看趋势Memory Profiler 抓的是瞬时快照,判断有没有在泄漏要看曲线。Profiler → Memory 模块,盯这几条: Total Reserved 总申请量 GC Allocated 托管堆使用量 Texture Memory 纹理内存 GC Allocation/frame 每帧分配 ← 这个最重要每帧分配量参考: 0 B/帧 理想(稳定状态下应该做到) 1 KB/帧 可接受 10 KB/帧 会频繁触发 GC,造成卡顿 100 KB/帧 严重问题典型的泄漏曲线:内存 │ │ ╱╲ ╱╲ │ ╱╲ ╱ ╲ ╱ ╲ │ ╱╲ ╱ ╲╱ ╲╱ ╲ │ ╱╲ ╱ ╲╱ │╱ ╲╱ └────────────────────────────── 时间 锯齿在整体上升 → 每次 GC 清不干净 → 泄漏 锯齿高度稳定 → 正常的分配回收循环七、排查流程① 真机抓快照(Release 包) ↓ ② Summary 看总量,确认是否超标 ↓ ③ Unity Objects 按 Size 排序 → 90% 的情况下第一名是 Texture2D ↓ ④ 点开大头,逐个检查: 格式对不对 / 尺寸合理吗 / Read-Write 关了吗 / 有重复吗 ↓ ⑤ 怀疑泄漏 → 抓前后两个快照做 Compare ↓ ⑥ 找到异常对象 → 看 References 引用链 → 顺着找到 static 或未释放的持有者 ↓ ⑦ 改完重新抓快照验证,别靠感觉八、常见误区说法实际情况“优化 GC 就能降内存”GC 影响的是卡顿和托管堆,占总量通常不到 20%“内存降下来了就没事”托管堆峰值不会归还,系统看的是峰值“关 Mipmap 省内存”省 1/3 纹理内存,但带宽暴涨,通常不值“编辑器里看着不高”编辑器数据和真机差一倍以上,无参考价值“UnloadUnusedAssets 能解决泄漏”引用没断它什么也清不掉“Addressables 自动管理内存”它需要你显式 Release,不然一样泄漏“总内存没涨就没泄漏”可能是泄漏和释放抵消了,要看分类明细要点1. 两个工具分工:内置 Profiler 看趋势,Memory Profiler 包定位对象 2. 必须真机 Release 包,编辑器数据不可信 3. 内存大头永远是纹理。先查格式、尺寸、Read/Write、重复 托管堆通常只占 10%~20% 4. 托管堆只增不减,要防峰值而不是平均值 5. 泄漏排查靠 Compare Snapshots: 回到同一界面,内存应该回到接近水平 6. References 引用链是定位泄漏的核心功能 终点是 static 基本就确诊了 7. UnloadUnusedAssets 只在加载界面调,且必须先断引用 8. 每帧分配量目标是 0 B,超过 10KB/帧就会有可感知的卡顿
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →