Unity AssetBundle堆内存优化方案

📅 2026/7/19 17:17:04
Unity AssetBundle堆内存优化方案
Unity AssetBundle堆内存优化方案1. 背景与目标1.1 问题传统 xasset / AssetBundle 热更链路中资源与 Bundle 身份大量依赖字符串全路径 / 逻辑名清单versions.bundle中存储目录、文件名或完整路径运行时构建Dictionarystring, string如 path → bundleName请求缓存、依赖展开、下载 URL、PlayerPrefs 等均以字符串为 key。在商业化项目中资源量达到数千数万时托管堆上会出现大量重复或长生命周期的路径字符串Dictionary 以 string 为 key 的额外开销频繁拼接路径带来的短生命周期分配与碎片感。1.2 目标目标说明降低托管内存热路径查找与缓存改为uint32减少路径字符串常驻ID 跨版本稳定同资源多次打包 ID 不变保障热更与业务常量可用API 简洁业务侧仅通过LoadAsset(uint)/LoadSceneAsync(uint)加载Bundle 落盘数字化磁盘 / CDN 文件名为稳定数字 ID缩短路径、便于去重Bundle 内加载名数字化利用addressableNamesPlayer 侧LoadAsset(12)即可非目标消除 Unity 底层所有字符串LoadFromFile/ 场景名等边界仍可能存在短字符串。2. 总体架构┌─────────────────── 打包期 ───────────────────┐ │ BuildRules 资源列表 │ │ ↓ │ │ IdRegistry持久化 path ↔ uint32 │ │ ↓ │ │ Unity BuildAssetBundles │ │ · assetNames 工程路径 │ │ · addressableNames assetId.ToString() │ │ ↓ │ │ Bundle 文件重命名为 {bundleId} │ │ ↓ │ │ versions.bundle AssetIds.g.cs │ └───────────────────────────────────────────────┘ ↓ ┌─────────────────── 运行时 ───────────────────┐ │ LoadAsset(uint id) │ │ ↓ │ │ Versions.GetAsset(id) → bundleId loadPath* │ │ ↓ │ │ LoadBundle(bundleId) → …/{bundleId} │ │ ↓ │ │ AssetBundle.LoadAsset(id.ToString(), type) │ │ * loadPathEditor / 场景名Player 可裁剪 │ └───────────────────────────────────────────────┘3. 稳定 ID 注册表IdRegistry3.1 职责持久化映射建议纳入版本库路径示例Assets/XAsset/Editor/IdRegistry.asset资源assetPath如Assets/UI/MessageBox.prefab→uint32Bundle逻辑名BuildRules 生成的 AB 名→uint32字段示意nextAssetId/nextBundleId从 1 递增assets: List{key, id}bundles: List{key, id}3.2 分配规则场景行为已存在路径 / 逻辑名复用原 ID跨次打包不变新增next分配新 ID删除不回收、不复用避免旧客户端串资源仅改文件内容、路径不变ID 不变变的是 crc / 是否热更重命名路径视为新资源拿新 ID需业务上「同一资源」时手工迁移绑定3.3 代码生成打包或 Editor 进 Play 时生成AssetIds.g.cs供业务使用常量避免魔法数字publicstaticclassAssetIds{publicconstuintMessageBox3;publicconstuintTitle2;// ...}业务加载示例Assets.LoadAsset(AssetIds.MessageBox,typeof(GameObject));Assets.LoadSceneAsync(AssetIds.Level);4. 清单格式Versions4.1 核心结构AssetRef字段类型说明iduint稳定资源 IDbundleIduint所属 Bundle 稳定 IDloadPathstring完整工程路径Editor / 场景名推导用Player 可裁剪BundleRef字段类型说明iduint稳定 Bundle ID磁盘文件名 id.ToString()childrenuint[]依赖 Bundle IDlenlong文件长度crcuint内容校验由 hex 字符串改为值类型hashstringUnity Hash128可选保留locationbyte是否打进包内等Versions去掉旧版dirs[]路径拆分activeVariants改为uint[]运行时查找表Dictionaryuint, AssetRef/Dictionaryuint, BundleRefBuildLookup()。4.2 破坏性说明新清单与旧versions.bundle、旧 CDN 文件名、旧 PlayerPrefs key不兼容。上线需全量重打 AB清空客户端本地 Bundles 缓存与相关 PlayerPrefsCDN 同步新数字文件名资源。5. 打包管线改造5.1 流程BuildRules分析依赖得到逻辑 Bundle 名与资源列表IdRegistry.GetOrAlloc*分配 / 复用 IDAssetBundleBuildassetNames Unity 工程路径打包输入addressableNames[i] assetId.ToString()Bundle内加载名BuildPipeline.BuildAssetBundles将输出文件由逻辑名重命名为{bundleId}及对应.manifest写入versions.bundle生成AssetIds.g.cs保存IdRegistry.asset。5.2 Bundle 内为何能用 uint 字符串加载Unity 标准 AB 默认以资源路径作为LoadAsset名称。通过AssetBundleBuild.addressableNames可指定自定义地址名bundle.LoadAsset(3,typeof(GameObject));// 对应 MessageBox 的稳定 ID注意必须使用带addressableNames重新打出的 AB旧包内仍是全路径名用数字加载会失败。5.3 场景特殊说明场景进入使用SceneManager.LoadScene(场景名)场景名通常来自文件名如Title不是addressableName。因此清单中仍可能保留loadPath或单独短字段sceneName用于推导场景名。6. 运行时 API 与查找6.1 对外 API仅 uintAssetRequestLoadAsset(uintid,Typetype);AssetRequestLoadAssetAsync(uintid,Typetype);SceneAssetRequestLoadSceneAsync(uintid,booladditivefalse);辅助uintFindAssetIdByName(stringfileNameWithoutExt);// 调试 / Demo 兼容AssetRefGetAssetRef(uintid);6.2 热路径数据结构改造前改造后Dictionarystring, string AssetToBundlesDictionaryuint, uint AssetIdToBundleIdDictionarystring, string[] BundleToChildrenDictionaryuint, uint[] BundleIdToChildren请求缓存 key path请求缓存 key uintBundle 文件名 逻辑名Bundle 文件名 {id}6.3 加载边界上的字符串环节是否还有字符串说明业务 API否只传uint查找 / 缓存否热路径uint字典AssetBundle.LoadFromFile短路径…/Bundles/{bundleId}bundleId.ToString()AssetBundle.LoadAsset短数字名id.ToString()addressableNameEditorAssetDatabase全路径开发模式仍用loadPath场景LoadScene场景短名从loadPath或专用字段推导7. Editor 开发模式未打 AB 时EditorInitializer用同一IdRegistry构建内存VersionsbundleId 0必须调用Versions.BuildLookup()否则GetAsset(id)查内部字典会失败list 有数据但索引为空assetLoader AssetDatabase.LoadAssetAtPathEditor 下建议强制development trueTitle 热更页跳过网络更新避免空清单 MessageBox 二次失败。Disable Domain Reload 时进 Play 前ExitingEditMode需重新挂载versionsLoader避免第二次 Play 清单为空。8. 内存收益评估8.1 子系统内Versions 查找表去掉 path 字典重复、Bundle 长名并将热路径改为uint后清单与查找相关托管内存经验上可降约40%70%视资源条数与平均路径长度而定。粗算参考平均路径约 60 字符含对象头约 ~130B/条资源量仅路径字符串量级叠加旧 path 字典后2,000~0.25 MB再约 0.30.6 MB10,000~1.3 MB再约 1.53 MB50,000~6.5 MB再约 815 MB8.2 相对整包 C# 堆路径优化通常只占整包托管内存的一小部分多数项目约1%5%资源极多、业务堆较「轻」时可能到5%15%。贴图 / Mesh 等资源本体多在原生侧不受本方案直接影响。8.3 若 Player 去掉loadPath当前为兼容 Editor / 场景清单仍可携带loadPath。正式包若改为不写、不读loadPath场景另存短名可进一步逼近上述上限。这是性价比最高的下一步。9. 进一步优化NativeC映射层动机将path → uint或只读 ID 表放到 C可减少托管小对象与 GC 压力在超大规模清单时更有价值。10. 总结本方案以稳定 uint32 身份贯穿打包与运行时注册表保证 ID 不漂移磁盘与 Bundle 内加载名数字化业务 API 只认 ID。内存收益主要来自去掉路径字典与长字符串热路径Unity 边界仍保留最短必要字符串。商业化落地时优先保证注册表进库与全量热更兼容策略再视 Profiler 数据推进 Player 去路径与可选 Native 层。