资源加载、热更新与生命周期模块 private
资源加载、热更新与生命周期模块
本篇回答六个问题:资源从哪里来、路径怎样解析、Editor 与 Player 为什么走不同分支、AssetBundle 依赖怎样加载、角色/特效为什么还要经过对象池,以及切关卡时资源到底怎样释放。
1. 先给结论
奇门客户端的资源系统不是单独一层 Resources.Load,也不能简单称为“Unity Addressables”。项目实际存在两层资源框架和一层业务索引:
flowchart LR
ID[业务 ID / GUID / 别名] --> IDX[CfgAssetsPath]
IDX --> AMF[AMF.AssetManager]
PATH[字符串路径] --> AMF
AMF --> TYPE[ASSETTYPE 分类]
AMF --> POOL[Role / VFX 对象池]
AMF --> AX[AddressablesX 兼容门面]
AX --> XA[XAsset.Assets]
XA --> DEV[Editor: AssetDatabase]
XA --> AB[Player: Bundle + 依赖]
VER[versions.bundle] --> XA
CDN[更新服务器] --> CACHE[persistentDataPath/Bundles]
CACHE --> XA
BUILTIN[StreamingAssets/Bundles] --> XA
- CfgAssetsPath:把策划/协议中的 ID 或地图 GUID 翻译成资源路径。
- AMF.AssetManager:面向游戏业务,区分 Role、VFX、Scene、Text、UIAtlas 等类型,并管理实例池。
- AddressablesX:名字是历史遗留;当前实现内部调用的是 Indieleague.XAsset.Assets,Unity Addressables 代码已经被注释。
- XAsset.Assets:真正负责路径/别名解析、versions.bundle、Bundle 依赖、下载、本地缓存和引用计数。
- ResourceDestroyer:在退关卡或退游戏时按业务边界销毁实体、工厂对象、池、Lightmap 和资源缓存。
当前检出的 AppConst.BundleMode 是 false。因此 AMF 自己维护的老式 AssetBundleManifest 分支默认不启用;但 AddressablesX -> XAsset 仍然是实际资源入口。不要因为 BundleMode=false 就判断“工程完全不用 Bundle”。Player 构建和热更新仍由 XAsset 的 versions.bundle 和 Bundle 请求体系负责。
2. 源码和配置索引
| 职责 | 实际入口 |
|---|---|
| 客户端资源启动 | Assets/Code/ClientProxy.cs |
| 业务资源门面 | Assets/Code/Core/AMF(assets manage framework)/AssetManager.cs |
| Bundle/Resources 分流与依赖 | Assets/Code/Core/AMF(assets manage framework)/AssetManagerInternal.cs |
| AMF 的 AssetUnit/BundleUnit 生命周期 | Assets/Code/Core/AMF(assets manage framework)/AssetUnit.cs |
| ID/GUID 到路径 | Assets/Code/Core/Database/Class/CfgAssetsPath.cs |
| XAsset 兼容门面 | Packages/com.indieleague.common/Runtime/Common.Unity/AddressablesX.cs |
| XAsset 运行时 | Packages/com.indieleague.xasset/Runtime/Assets.cs |
| XAsset 请求与引用计数 | Packages/com.indieleague.xasset/Runtime/AssetRequest/、Runtime/Reference.cs |
| Editor 资源规则装配 | Packages/com.indieleague.xasset/Editor/EditorInitializer.cs |
| 收集、分组、打 Bundle | Packages/com.indieleague.xasset/Editor/Settings/XAssetSettings.cs、Editor/BuildScript.cs |
| 首包初始化与热更新 | Packages/com.indieleague.init/Runtime/Init.cs、Runtime/Hotfix/XAssetUpadater.cs |
| 关卡/游戏资源清理 | Assets/Code/Game/OLogic/ResourceDestroyer.cs |
| UI 请求集中释放 | Assets/Code/Game/Starlit/UIManager/AssetsHolder.cs |
| 资源打包配置 | Assets/XAssetData/XAssetSettings.asset |
| 初始化场景配置 | Assets/Init/Init.unity |
Unity MCP 当前连接到 client 工程,版本为 Unity 2021.3.38f1、目标平台为 Android。当前 Editor 未处于 Play Mode,Console 没有 Error/Warning。以下“当前值”来自这套工程,不是通用示例。
3. 启动阶段:资源系统何时可用
3.1 Init 场景先初始化 XAsset
Packages/com.indieleague.init/Runtime/Init.cs 把场景中的配置写入 XAsset:
Assets.development = development;
Assets.updateAll = updateAll;
Assets.downloadURL = ConfigUrl;
Assets.CustomVersion = CustomVersion;
Assets.verifyBy = verifyBy;
Assets.searchPaths = searchPaths;
Assets.patches4Init = patches4Init;
Assets.updateUnusedAssetsImmediate = updateUnusedAssetsImmediate;
Assets.Initialize(...);
当前 Assets/Init/Init.unity 的真实序列化值:
development: 1
dontDestroyOnLoad: 1
updateAll: 1
updateUnusedAssetsImmediate: 0
launchScene: Game
searchPaths:
- Assets/Resources_moved
- Assets/Maps/Resources_moved
- Assets/Main
- Assets/ResourcesX
patches4Init: []
Editor 下 EditorInitializer 注入 AssetDatabase.LoadAssetAtPath 作为 Assets.assetLoader,并生成开发态 Versions 映射。所以开发模式可直接读取工程资产,不要求先把每个 Bundle 构建出来。
3.2 XAsset 初始化版本表
Assets.Initialize 创建常驻 Assets GameObject,并设置:
basePath = Application.streamingAssetsPath/Bundles/
updatePath = Application.persistentDataPath/Bundles/
随后加载 versions.bundle,调用 ReloadVersions 建立三张关键表:
- AssetToBundles:完整资产路径 -> Bundle。
- AliasToPath:短别名 -> 完整资产路径。
- BundleToChildren:Bundle -> 依赖 Bundle。
没有完成这一步就开始加载资源,常见结果不是“网络报错”,而是别名无法解析、Bundle 名为空或请求落到不存在的文件路径。
3.3 ClientProxy 再初始化 AMF
进入游戏主逻辑时,ClientProxy 执行:
AMF.AssetManager.Instance.Initialize();
AMF.AssetManager.Instance.LoadAssetBundleManifest();
UIManager.Instance.Initialize();
Initialize 创建 [PoolManager],并初始化 [Role]、[VFX] 常驻池。资源更新检查结束后,ClientProxy 会 Uninitialize -> Initialize -> LoadAssetBundleManifest 再走一次,避免更新前缓存、Manifest 与更新后文件混用。
主循环中还会调用 AMF.AssetManager.Instance.Update(),推进 BundleUnit、AssetUnit 和 AtlasUnit 的延迟清理。因此不能只在启动时创建 AssetManager,却漏掉 Update 驱动。
4. 一次业务资源加载的完整链路
sequenceDiagram
participant Logic as 业务 Logic/View Factory
participant Index as CfgAssetsPath
participant AMF as AMF.AssetManager
participant AX as AddressablesX
participant XA as XAsset.Assets
participant Source as AssetDatabase/Bundle
Logic->>Index: assetId 或 GUID 查询
Index-->>Logic: 无扩展名资源路径
Logic->>AMF: GetAssetAsync(path, ASSETTYPE, async)
AMF->>AMF: GetAssetUnit + 类型/池选择
AMF->>AX: Load(path)
AX->>XA: LoadAsset(path, type)
XA->>XA: alias/searchPaths/AssetToBundles
XA->>Source: Editor 直读或加载 Bundle 及依赖
Source-->>XA: UnityEngine.Object
XA-->>AMF: AssetRequest.asset
AMF->>AMF: Role/VFX 则从池 Spawn
AMF-->>Logic: asset 或实例 Transform
4.1 ID/GUID 先翻译成路径
CfgAssetsPath 启动时读取地图索引和普通资源索引,构建地图 GUID 与 path 的双向表,以及 Prefab、VFX、Sound 的 assetID 到 path 映射。
var prefabPath = CfgAssetsPath.GetPrefabAssetPath(assetId);
var mapAssetPath = CfgAssetsPath.GetAssetPath(mapGuid);
var vfxPath = CfgAssetsPath.GetVFXLoadPath(assetId);
这层把配置稳定 ID 与 Unity 目录解耦,前提是 Assets/Resources_moved/Database 中的索引和当前资源版本同批生成。出现“配置 ID 有值但 Prefab 为 null”,第一步应检查索引,而不是先怀疑对象池。
4.2 AMF 根据 ASSETTYPE 决定返回语义
| 类型 | 返回/处理方式 |
|---|---|
| ROLE | 加载 Prefab 后从 Role 池 Spawn,返回 Transform |
| VFX | 从 VFX 池 Spawn,返回 Transform |
| UIPANEL/UIUNIT | UI 池或实例化,通常返回 GameObject |
| TEXT/SCENE/LIGHTMAP/TEXTURE/SPRITE | 返回原始 asset;Sprite 有专门泛型分支 |
| OTHERPREFAB | 非池化实例路径,返回 Transform |
同一路径不能随意换 ASSETTYPE。GetAssetUnit 会在同一路径已缓存为另一类型时输出“不支持类型不一样”。
4.3 XAsset 怎样找到真实资源
GetSearchPath 的顺序是:
- 把输入路径转小写,查 AliasToPath。
- 用完整路径查 AssetToBundles。
- Editor development 模式下尝试别名对应的真实文件。
- 逐个拼接 searchPaths/path,再检查磁盘或 Bundle 映射。
例如业务传入 Prefab/Role/Hero_4/Hero_4_1_1,在当前设置下可解析到 Assets/Resources_moved/Prefab/Role/Hero_4/Hero_4_1_1.prefab。
XAssetSettings.asset 同时记录 alias、bundle 与分组方式。路径大小写、扩展名和 searchPaths 都可能影响命中,不能只确认 Project 窗口里“看得到文件”。
5. Editor 直读与 Player Bundle 模式
5.1 Editor development 模式
当前 Init 场景的 development=1。EditorInitializer 将:
Assets.assetLoader = AssetDatabase.LoadAssetAtPath;
Assets.versionsLoader += () => developVersions;
普通 AssetRequest.Load 最终直接使用 AssetDatabase。优点是资源修改立即可见、迭代快;缺点是它无法证明 Bundle 分组、依赖、首包和热更文件正确。
5.2 Player/非开发模式
构建态由 versions.bundle 决定资源在哪个 Bundle。XAsset 创建 BundleAssetRequest 或 BundleAssetAsyncRequest,先拉起依赖 Bundle,再从目标 Bundle 取 asset。
本地文件优先级应理解为:
- persistentDataPath/Bundles 中已下载的新版本。
- 包内 StreamingAssets/Bundles 的首包内容。
- 版本表判断本地内容不匹配时,从下载地址取得对应文件。
因此“Editor 正常、真机丢资源”几乎总要检查:资源是否进入规则、alias 是否写入版本表、目标 Bundle 是否属于首包或已下载 patch、依赖是否完整、平台产物是否正确。
5.3 AMF 的 BundleMode 与 XAsset 不要混为一谈
AMF 保留一套旧式 AssetBundleManifest、.ab 和 abbatchnamemap 路径;AppConst.BundleMode=false 会让 LoadAssetBundleManifest 直接返回。在它的 Resources 分支里,旧 Resources.Load 已注释,实际又调用 AddressablesX.Load,最终落到 XAsset。
所以当前主路径是:
AMF 业务门面 -> AddressablesX 历史命名门面 -> XAsset
排错时必须指出是哪一层的缓存、路径或引用出了问题,不能笼统称为“Addressables 问题”。
6. 同步、异步与每帧预算
XAsset 提供同步和异步请求:
Assets.LoadAsset(path, type);
Assets.LoadAssetAsync(path, type);
它用“最终路径 + 类型名”作为请求缓存 Key。同一资源重复请求会复用 AssetRequest 并 Retain。若已有请求仍在加载,而新调用要求同步,代码会执行 LoadImmediate。
AMF 的接口虽然返回 UniTask,但内部是否异步由 async 参数决定。当前 Resources/XAsset 分支即使 async=true,也直接调用 AddressablesX.Load;旧异步 Addressables/Resources 实现已注释。因此该分支在接口形态上异步,核心仍可能同步完成并造成主线程尖峰。性能排查必须看 Profiler 与真实调用栈,不能只看方法名。
AssetsHolder 的异步陷阱
当前 AssetsHolder.LoadAssetAsync 的顺序是:
var request = XAsset.LoadAssetAsync(name, typeof(T));
if (request.asset != null)
{
m_listRequest.Add(request);
await UniTask.WaitUntil(() => request.isDone);
return (T)request.asset;
}
return null;
它在等待 isDone 之前先判断 request.asset。对尚未完成的异步请求,asset 通常为空,所以可能直接返回 null,也不会把 request 加入释放列表。这是当前代码风险;后续若修复,应改成“保存请求 -> 等待完成 -> 检查 error/asset”,并处理取消与 owner 销毁竞态。
7. 对象池与资源缓存不是一回事
角色和特效的获取包含两层复用:
XAsset/AMF 缓存 Prefab 资产
↓
SpawnPool 缓存 Prefab 实例
资产缓存避免反复读 Bundle、解析 Prefab;实例池避免反复 Instantiate/Destroy GameObject。Despawn 只是把实例还池,不等于卸载底层纹理、材质和 Bundle。
eternal=true 只适用于 Role/VFX 的池标记,会设置 PrefabPool.eternalPool;它不是“永不释放所有依赖资源”的通用开关。
8. 引用计数与释放责任
8.1 XAsset 请求必须成对释放
XAsset 每次 LoadAsset 都会 request.Retain()。调用者结束使用时必须 request.Release() 或 Assets.UnloadAsset(request)。refCount <= 0 后,请求才具备进入 unused 队列和卸载的条件。
UI 的 AssetsHolder 是清晰的所有权示例:它保存全部 AssetRequest,面板关闭时统一 Release。
8.2 AddressablesX 的路径级持有
AddressablesX.Load 把第一次请求存入 Dictionary<string, AssetRequest>。释放接口是 AddressablesX.Unload(name/request)。引用归零后,它延迟 5 秒调用 Assets.RemoveUnusedAssets,合并频繁清理请求。LoadAutoRelease 则三帧后自动 Release,适合短期读取 TextAsset,不适合仍被界面或组件长期使用的资源。
重要边界:AddressablesX 的字典按 name 只保留一个请求对象,而 XAsset 每次重复加载都会 Retain。多个 owner 共享同名资源时若没有对称 Unload,引用计数会长期偏高。新代码最好显式保存 AssetRequest,或建立明确的 owner/Release 生命周期。
8.3 AMF 的引用语义
AMF 的 AssetUnit 与 BundleUnit 也有自己的 refcount。加载时 AddRef,向调用方返回 Unity 对象后 DelRef;GameObject 生命周期更多由 SpawnPool 和 Factory 管理。Bundle 依赖在主 Bundle 释放时递归 DelRef。
当前 BundleUnit.Init 将释放间隔设为 float.MaxValue,意味着 AMF 旧 Bundle 分支基本不会依靠 Update 的时间阈值自然卸载 Bundle,更多依赖显式清缓存或重初始化。这也是不能把 AMF 和 XAsset 两套引用计数混作一套的原因。
9. 切关卡与退出游戏怎样清理
ResourceDestroyer.DestroyLevel 的真实顺序:
flowchart TD
A[World/Gameplay OnLevelDestroy] --> B[销毁 MapEntity 与 CoreEntity]
B --> C[重置 Context CreationIndex]
C --> D[Prop/Nav/Collider/Light Factory 清理]
D --> E[清 World Entity Pool]
E --> F[清 Role/VFX 实例池]
F --> G[清 WebAsset / AMF AssetUnit]
G --> H[音频缓存保留正在使用项]
H --> I[清 LightmapSettings 并 Unload 纹理]
I --> J[Resources.UnloadUnusedAssets]
关键调用:
AMF.AssetManager.Instance.ClearPool(ASSETTYPE.VFX);
AMF.AssetManager.Instance.ClearPool(ASSETTYPE.ROLE);
AMF.AssetManager.Instance.ClearWebAssetCache();
AMF.AssetManager.Instance.ClearAssetUnits();
UnloadLightmapTextures();
Resources.UnloadUnusedAssets();
DestroyGame 还会 TearDown Systems、销毁所有 Context 与通用 ObjectFactory。两者语义不同:切一关不应误调用完整退游戏清理;完整退出也不能只清对象池而留下 Context 与系统订阅。
Resources.UnloadUnusedAssets 只处理 Unity 判定为不可达的资源,不能替代业务层解除引用、工厂销毁、对象池清理和 XAsset Request.Release。
10. 构建、分包与热更新链路
10.1 当前 XAssetSettings
scenesFolders:
Assets/Init
Assets/Main
Assets/Maps/Lightmap
autoGroupByDirectories:
Assets/Resources_moved
Assets/Maps/Resources_moved
Assets/Dlls
allAssetsToBuild: 1
patchesInBuild: [InitRes]
extension: .sb
nameByHash: 0
options: 256 # ChunkBasedCompression
XAssetSettings.Build 依次执行 Collect、Analysis、Optimize、Save,生成 assets、patches、bundles。GroupBy 决定按文件、目录或显式组形成 Bundle;依赖分析还会产生 shared Bundle。
10.2 构建产物
XAsset/Build/Bundles 菜单调用:
BuildScript.BuildRules();
BuildScript.BuildAssetBundles();
BuildAssetBundles 执行 Unity BuildPipeline.BuildAssetBundles,再由 BuildVersions:
- 读取 Manifest 中全部 Bundle 与依赖。
- 复制或加密 Bundle 到版本目录。
- 记录 asset、alias、bundle、patch、CRC、长度和首包位置。
- 输出 versions.bundle。
XOutput/{Platform}/{Version}/Bundles/
versions.bundle
*.sb
CopyAssets 会重建 StreamingAssets/Bundles,只复制首包文件和版本表。Jenkins 的 BuildHotfix、BuildPackage 复用同一链路,不应另写一套手工打包流程。
10.3 启动更新
DownloadVersions
-> ReloadVersions
-> DownloadAll(patches4Init) 或 DownloadAll()
-> 校验 CRC/长度
-> 写入 persistentDataPath/Bundles
-> 后续加载优先命中新版本
patches4Init 为空时要结合 updateAll 判断下载策略,不能据此直接判断“没有热更新资源”。
11. 可执行操作手册
11.1 新增运行时 Prefab
- 放入已纳管目录,通常是 Assets/Resources_moved 或 Assets/Maps/Resources_moved。
- 若业务按 ID 加载,更新资源配置并重新生成 CfgAssetsPath 二进制。
- 检查 XAssetSettings 是否收集到资产,alias 与代码传入路径是否一致。
- 根据用途选择正确 ASSETTYPE;角色/特效确认对象池回收路径。
- Editor development 模式运行一次。
- 执行 XAsset/Build/Bundles,检查 versions.bundle 与目标 .sb。
- 用非 development 或目标 Player 验证,不能以 Editor 直读通过代替 Bundle 验收。
- 关闭界面或退出关卡,检查 Request、实例池和纹理是否释放。
11.2 发布资源热更新
- 固定代码、配置、资源与地图数据版本。
- BuildRules,确认资源和依赖分组。
- BuildAssetBundles,生成版本目录与 versions.bundle。
- 检查首包 patch 与远端 patch 边界,避免登录必需资源未预下载。
- 上传 Bundle,最后切换 versions.bundle,避免客户端先读到未上传完整的版本。
- 冷启动验证 DownloadVersions、下载进度、CRC 和落盘路径。
- 断网、弱网、覆盖安装各测一次。
- 进图并切关,用资源诊断窗口或 Profiler 检查引用和内存回落。
12. 常见故障与定位顺序
| 现象 | 首查 | 常见原因 |
|---|---|---|
| Editor 能加载,Player 为 null | versions.bundle、AssetToBundles、目标 .sb | 只通过 AssetDatabase 验证,资源未进入构建规则 |
| 配置 ID 查不到资源 | CfgAssetsPath 二进制与生成时间 | ID 索引未生成或版本不一致 |
| 路径存在但 alias 不命中 | XAssetSettings.alias、searchPaths、大小写 | 代码用短路径,版本表只有完整路径 |
| 首次进地图缺资源,重进正常 | patch 下载完成时机、请求竞态 | 不在首包且进图早于下载完成 |
| 真机加载旧资源 | persistentDataPath/Bundles、版本号 | 缓存优先、版本表未切换或覆盖安装残留 |
| Bundle 加载失败 | BundleToChildren、依赖文件、CRC | shared Bundle 漏上传或平台产物混用 |
| GetAssetAsync 卡主线程 | AMF Resources 分支实际调用 AddressablesX.Load | 名字异步但核心同步 |
| UI 反复打开后内存上涨 | AssetsHolder.Release、AddressablesX.Unload | Request Retain/Release 不对称 |
| 角色 Destroy 后内存不降 | Despawn、ClearPool、底层 Request | 只销毁实例,Prefab/Bundle 仍被引用 |
| 切关后旧光照残留 | LightmapSettings、UnloadLightmapTextures | 普通清缓存不会清静态 Lightmap 数组 |
| Release 报 refCount < 0 | 同一 owner 重复释放 | Release 多于 Load/Retain |
建议记录:
inputPath, resolvedPath, assetType, alias
bundleName, dependencies, loadState, error
request.refCount, AMF AssetUnit status/refcount
development, platform, versions.ver
basePath, updatePath, downloadURL
poolName, spawnedInstanceCount
13. 当前实现的风险与改进优先级
- AddressablesX 命名会误导维护者:当前实际是 XAsset。
- AMF Unit 与 XAsset Request 各有引用计数,排查必须同时看;长期可收敛为明确的 Handle 所有权。
- AMF 异步名不代表真正异步,大资源可能制造主线程尖峰。
- AssetsHolder 异步判断顺序可疑,应先等待完成,再检查 asset/error。
- 同一路径以不同 ASSETTYPE 请求会冲突,可让生成的资源句柄携带类型。
- ClearAssetUnits 粒度较粗,建议按 Global、Lobby、Battle、Level 建立 Scope。
- 必须增加非 development、Bundle 缺失、依赖缺失、版本回滚和低内存测试。
14. 建议的学习顺序
- 从 Assets/Init/Init.unity 记录 development、searchPaths 与更新开关。
- 跟 InitXAsset -> Assets.Initialize -> ReloadVersions,画出三张映射表。
- 在 CfgAssetsPath 选一个 Hero ID,确认它怎样变成路径。
- 断点跟 AMF.AssetManager.GetAssetAsync -> AddressablesX.Load -> Assets.LoadAsset。
- 对照 Editor development 与 Player Bundle 两种请求类型。
- 打开同一 UI 两次,观察 Request.refCount,再关闭并确认 AssetsHolder.Release。
- 生成一次 Bundle,检查 XOutput 平台版本目录、versions.bundle 与目标 .sb。
- 进入并退出地图,观察 ResourceDestroyer、池对象、Lightmap 和内存变化。
- 最后再评估是否重构 AMF/XAsset;没有理解所有权前不要直接替换资源框架。