# HF 框架微信小游戏 CDN 资源接入方案 ## 一、目标与边界 本方案只设计 HF/StrayFog 框架自身的微信小游戏资源能力,不与任何具体业务工程绑定。 最终使用方式必须保持简单: 1. 业务继续选择现有 `CDNAssets` 加载模式。 2. 编译时启用 `WeChat` 宏,框架自动使用微信 CDN 实现。 3. 未启用 `WeChat` 宏,框架继续使用原有 CDN 实现。 4. 不新增 `isWeChat` 参数、项目启动代码、项目名路径或业务层初始化接口。 本阶段不处理微信登录、支付、广告、分享,也不负责安装、升级或转换 WXSDK。 ## 二、微信官方约束 ### 2.1 SDK 与宏 微信官方文档建议 Unity 2019 及以后版本通过 Package Manager 安装 WXSDK,并说明同时导出普通 WebGL 时应使用编译宏隔离微信专用接口。 框架采用已有的 `WeChat` 宏作为唯一开关: ```csharp #if WeChat // 微信小游戏实现 #else // 原有实现 #endif ``` `WeChat` 宏启用时,使用方必须已经导入提供 `WeChatWASM` 和运行时程序集 `Wx` 的微信插件。宏未启用时,框架不得编译或调用任何微信 API。 ### 2.2 AssetBundle 微信官方资源文档明确要求: - 非首包资源放在 CDN,通过异步接口按需加载。 - 小游戏不能沿用普通 APP 的本地路径 AssetBundle 加载方式。 - Bundle 应通过 CDN URL 使用 `UnityWebRequestAssetBundle` 或 `DownloadHandlerAssetBundle` 异步加载。 - 微信转换插件在底层负责 Bundle 缓存和更新;业务层仍按远程请求编写。 - 不应自行将普通 Bundle 写入 `WX.env.USER_DATA_PATH` 后再使用 `LoadFromMemoryAsync`。 - 禁止使用 `WWW`、`WWW.LoadFromCacheOrDownload` 和 Unity 旧 Cache API。 - Bundle 文件名应包含内容 Hash;推荐 LZ4,并在不需要跨 Unity 版本时关闭 TypeTree。 - 不再使用的 Bundle 应及时卸载。 因此,框架的微信 CDN 后端负责“创建远程 Bundle 请求和管理生命周期”,不实现第二套 AssetBundle 磁盘缓存。 ## 三、框架公开行为 ### 3.1 选择矩阵 | 加载模式 | `WeChat` 宏 | 框架行为 | |---|---:|---| | `CDNAssets` | 开启 | 使用微信 CDN AssetBundle 后端 | | `CDNAssets` | 关闭 | 使用现有 CDN 后端 | | 非 `CDNAssets` | 开启或关闭 | 保持现有 Editor、PersistentData、StreamingAssets 等逻辑 | 只有同时满足“当前为 `CDNAssets`”和“编译启用 `WeChat`”时才切换实现。业务调用方式、资源 ID、回调和 `HFFunCatalogue.Asset` 入口保持不变。 ### 3.2 唯一开关 框架内部选择逻辑应等价于: ```csharp if (loadModel == enHFAbLoadModel.CDNAssets) { #if WeChat LoadAssetBundleByWeChatCdn(); #else LoadAssetBundleByDefaultCdn(); #endif return; } LoadByExistingMode(); ``` 不得再叠加以下开关: - `HF_WECHAT_MINIGAME` - `UNITY_WEIXINMINIGAME` - `useWechatCdn` - `isMiniGame` - 项目配置表中的微信布尔值 Unity 目标平台仍由原逻辑识别为 WebGL,资源平台目录继续使用 `ab_webgl`;`WeChat` 只决定 CDN 实现,不新增 `ab_wechat`。 ## 四、框架内部设计 ### 4.1 编译期分流 微信实现直接封装在现有框架资源调用链中,不新增业务初始化入口: ```text HFAssetLoader -> CDNAssets -> UnityWebRequestAssetBundle(仅 WeChat 宏编译) -> 原有下载与本地缓存流程(未启用 WeChat 宏) HFABMgr Manifest -> CDNAssets -> UnityWebRequestAssetBundle(仅 WeChat 宏编译) -> 原有 Manifest 下载流程(未启用 WeChat 宏) SFSubPackageRuntimeV2 -> Bootstrap 与状态文件 -> WeChatMiniGame / WXFileSystemManager(仅 WeChat 宏编译) -> System.IO(未启用 WeChat 宏) -> 普通 AssetBundle -> UnityWebRequestAssetBundle 预热,由微信插件缓存 ``` 微信桥接负责两项能力: - 创建微信环境下的远程 AssetBundle 请求。 - 为 V2 状态和 Bootstrap 原始文件提供微信文件存储。 普通 AssetBundle 缓存仍由微信插件底层负责,不进入 `HF Byte Storage`。 ### 4.2 无注册、无第二开关 不建立 `HFResourcePlatform.Register(...)` 一类注册流程,也不要求项目启动代码识别微信平台。现有 CDN 根地址、版本、渠道和平台参数仍使用框架原配置;`WeChat` 只在编译时替换 `CDNAssets` 的底层实现。 微信文件根目录由 `HFAssetBundleHelper.GetCdnSaveRootPath()` 在 `#if WeChat` 中直接返回 `WX.env.USER_DATA_PATH` 下的框架目录。V2 在第一次读取状态前等待下载配置与存储回调全部设置完成,避免先访问 `persistentDataPath` 再切换存储。 ### 4.3 程序集边界 - 微信 API 只出现在 `#if WeChat` 包围的框架代码中。 - `WeChatMiniGame` 使用插件提供的 `WeChatWASM` 文件系统类型。 - Loader、Manifest 和 V2 普通 Bundle 分支只依赖 Unity 标准 `UnityWebRequestAssetBundle`。 - 不新增项目程序集引用、项目名、业务配置或微信布尔字段。 - 未启用 `WeChat` 宏时,原有 CDN 代码保持编译和运行。 - 启用 `WeChat` 宏但没有导入微信插件时,`WeChatMiniGame` 会产生明确的编译错误,而不是静默使用普通 WebGL 文件系统。 ## 五、CDN AssetBundle 实现 ### 5.1 默认微信后端 第一阶段使用微信官方常规异步方案: ```csharp #if WeChat UnityWebRequest request = UnityWebRequestAssetBundle.GetAssetBundle(remoteUrl); yield return request.SendWebRequest(); if (request.result == UnityWebRequest.Result.Success) { AssetBundle bundle = DownloadHandlerAssetBundle.GetContent(request); // 交给 HF Loader 生命周期管理。 } else { // 进入框架原有候选 CDN、重试和失败回调。 } request.Dispose(); #endif ``` 实现规则: - 始终传入完整 CDN URL;热启动同样走该请求,由微信插件判断缓存命中。 - 不调用 `ExistLocalFile`、`ReadLocalFile`、`File.Exists`、`FileInfo`、`File.ReadAllBytes`、`AssetBundle.LoadFromFileAsync` 或 `AssetBundle.LoadFromMemoryAsync` 加载微信普通 Bundle。 - 不对插件 Bundle 缓存做业务层物理文件检查、删除和大小统计。 - 不使用 `WWW`、旧 Cache API 或 IndexedDB 模拟缓存。 - 请求进度通过 `UnityWebRequest.downloadProgress` 回传给现有 HF 回调。 - 资源大小优先使用 Manifest 的 `Size`;没有可靠值时保持 `Size = 0`,不影响加载结果。 - 相同 URL 的并发请求继续由框架合并。 - 请求失败继续使用框架现有候选 CDN 地址与重试策略。 ### 5.2 `WXAssetBundle` 扩展后端 微信插件还提供更节省内存的 `WXAssetBundle`。官方同时说明它可能增加磁盘读取成本,因此第一阶段不默认启用。 后续如果真机数据证明必须使用,可以在框架现有微信 CDN 请求方法内部整体替换实现,不改变业务接口和宏: ```csharp WXAssetBundle.GetAssetBundle(remoteUrl) DownloadHandlerWXAssetBundle.assetBundle bundle.WXUnload(false) ``` 这仍然只由 `WeChat` 宏选择。不得为它再增加第二个业务开关;是否升级为该实现属于框架版本决策。 ### 5.3 Hash 与更新 微信底层缓存依赖 URL 和 Bundle Hash。框架构建工具必须保证: - WebGL Bundle 物理文件名包含内容 Hash。 - V2 Manifest 保存 CDN 上真实的 Hash 文件名。 - 相同内容 URL 稳定,不同内容生成新 URL。 - 使用 `BuildAssetBundleOptions.ChunkBasedCompression`。 - 无跨 Unity 版本兼容要求时使用 `DisableWriteTypeTree`。 - 如果现有构建器已经拼接内容 Hash,不再重复使用另一套 Hash 命名。 微信端不读取 Bundle 本地字节做 MD5 校验。V2 中的 MD5 保留用于构建审计、差异展示和非 Bundle 原始文件校验。 ## 六、V2 状态与 Bootstrap 存储 ### 6.1 存储范围 微信文件存储只处理: - `subpackage_state.json` - 最近一次有效的 `subpackage.json` - AOT DLL 原始字节 - HotUpdate DLL 原始字节 - 框架未来明确声明为原始文件的内容 普通 AssetBundle、场景 Bundle、UI、模型、贴图和音频 Bundle 不写入此存储。 ### 6.2 框架通用路径 微信小游戏自身已经按应用隔离用户目录,因此框架路径不包含任何项目名: ```text WX.env.USER_DATA_PATH/hf_resource/v2/ab_webgl ``` 路径由 `WeChatMiniGame.MiniGameSaveRootPath()` 内部生成。业务不能传入产品名或项目目录。 处理规则: - 使用 `WXFileSystemManager` 读写。 - 路径统一为 `/`,禁止 `..` 和越过框架根目录。 - 不使用 `FileInfo` 解析微信路径或统计大小。 - 已持有完整原始字节时可以记录 `LongLength`,否则大小保持 `0`。 - 读取为 `null`、空字节或 MD5 不一致时,当前流程重新下载并覆盖。 - 写入或删除失败不影响下一次重新读取和恢复。 ### 6.3 V2 初始化 V2 初始化由资源框架内部完成: 1. 平台桥接已经自动注册。 2. `SetLocalStorage` 只保存回调,不自行调用 `LoadState()`。 3. CDN、版本、平台和 V2 Local Root 全部设置完成后,只调用一次 `LoadState()`。 4. `WeChat` 宏下使用微信字节存储;`#else` 使用 `System.IO`。 5. 禁止先访问 `Application.persistentDataPath`,再切换到微信存储。 V2 的 Bundle 历史下载状态不能作为微信插件缓存仍存在的证明。每次进程首次使用某个 Bundle 时仍创建远程请求,由插件决定缓存或网络;Package 的微信状态只表示本次进程请求成功。 ## 七、卸载与失败恢复 ### 7.1 卸载 第一阶段使用 `UnityWebRequestAssetBundle`: ```csharp assetBundle.Unload(false); ``` 卸载顺序: 1. 业务释放场景、窗口、音频或实例引用。 2. HF Loader 清理资源引用。 3. Bundle 执行 `Unload(false)`。 4. 请求和 DownloadHandler 被释放。 如果框架以后内部切换为 `WXAssetBundle`,对应请求方法必须改用 `WXUnload(false)`。这一差异仍封装在框架内部,不暴露给业务。 `Resources.UnloadUnusedAssets` 只在场景切换或批量窗口关闭后的安全节点调用,不在每次资源卸载时执行,也不主动调用 `GC.Collect`。 ### 7.2 失败恢复 - 请求失败按候选 CDN 和框架重试次数重新请求。 - 新 Bundle 通过新 Hash URL 绕过旧版本缓存。 - 不依赖微信插件缓存的物理路径,不主动删除插件缓存。 - 请求成功但 `AssetBundle == null` 时最多重试一次,禁止无限循环。 - V2 状态或 Bootstrap 原始文件损坏时重新下载覆盖。 - 日志包含逻辑资源名、完整 URL、候选地址序号、重试次数和请求错误。 ## 八、框架代码修改边界 | 内容 | 框架源位置 | |---|---| | CDN 地址、本地根目录与宏选择 | `Assets/StrayFog/Editor/CopyToX/CopyToGeneral/AssetBundle/ESF_AssetBundleHelper.cs` | | 微信字节存储实现 | `Assets/StrayFog/Editor/CopyToX/CopyToGeneral/MinGameModel/WeChatMiniGame.cs` | | Loader 请求、进度、重试和卸载 | `Assets/StrayFog/Editor/CopyToX/CopyToHF/AssetBundle/Class/` | | CDN Manifest 请求 | `Assets/StrayFog/Editor/CopyToX/CopyToHF/AssetBundle/ESF_ABMgr_Manifest.cs` | | V2 状态与 Bootstrap | `Assets/StrayFog/Core/SFSubPackageV2.cs` | | WebGL Bundle 构建参数与 Hash Manifest | `Assets/StrayFog/Editor/Tools/AssestBundle/` | 禁止把实现放入项目的 Game、Hotfix、启动入口、LoadingWindow 或其他业务管理器中。 修改 ESF 源后执行 CopyToX,并验证生成结果;不能只修改 CopyToHF、CopyToGeneral 等生成目录。 ## 九、验收标准 ### 9.1 公共行为 - 业务只设置 `CDNAssets`,不调用微信初始化 API。 - 打开 `WeChat` 宏后自动走微信 CDN 后端。 - 关闭 `WeChat` 宏后自动走原 CDN 后端。 - 非 CDN 模式行为不变。 - 框架代码中没有项目名、项目路径或业务程序集依赖。 ### 9.2 微信 CDN - Bundle 使用完整 CDN URL 异步加载。 - 相同 Hash URL 的后续请求由插件复用缓存,框架代码路径不变。 - 框架不生成自定义微信 Bundle 缓存文件。 - Bundle Loader 不持有用于 `LoadFromMemoryAsync` 的完整缓存字节。 - 请求失败、重试、进度和完成回调符合现有公开 API。 - Bundle 卸载后 Loader 和 Bundle 内存下降。 ### 9.3 V2 原始文件 - V2 状态与 Bootstrap 写入框架通用微信目录。 - 空文件、损坏文件和 MD5 不一致能够重新下载。 - 微信路径不进入 `System.IO` 和 `FileInfo`。 - V2 初始化只读取一次正确存储中的状态。 ### 9.4 回归 - Editor、Android、iOS 和普通 WebGL 的原 CDN 行为不变。 - 未启用 `WeChat` 宏时不编译微信 API 调用。 - `WeChat` 宏启用但插件缺失时给出明确编译或初始化错误。 ## 十、当前实施结果 截至 2026-07-22,框架第一阶段已经完成: 1. `CDNAssets + WeChat` 直接使用 `UnityWebRequestAssetBundle` 加载主 Bundle,普通 Bundle 不再走自建文件缓存和 `LoadFromMemoryAsync`。 2. CDN Manifest 使用同一 AssetBundle 请求方式,并支持按现有候选 CDN 根地址回退。 3. 请求成功但 Bundle 为空时只重试一次;网络失败继续尝试候选 CDN,失败后正确结束 Loader 状态。 4. 微信普通 Bundle 的 V2 下载改为请求预热,只记录本进程成功状态,不持久化伪缓存命中状态。 5. V2 状态和 Bootstrap 原始字节写入 `WX.env.USER_DATA_PATH/hf_resource/v2/`,微信路径不再进入 `FileInfo`。 6. 微信读取返回 `null` 或 MD5 不一致时沿用 V2 原流程重新下载;微信写入结果会向 V2 报告成功或失败。 7. `SetDownloadConfig` 和 `SetLocalStorage` 只更新配置,状态在首次实际操作前延迟读取一次。 8. ESF 源文件已按 CopyToX 规则同步到 HF 生成目录,并校验生成内容一致。 9. 普通分支及临时追加 `WeChat` 宏的 `StrayFogCore`、`GameGeneral`、`GameHF`、`SFEditor` 均完成 0 error 编译验证。 仍需真机阶段验证微信插件缓存复用、弱网切换、内存峰值和 Bundle 卸载效果。真机数据不足前不启用 `WXAssetBundle`,也不增加第二个开关。 ## 十一、外部依据 - [微信 SDK 安装与使用](https://developers.weixin.qq.com/minigame/dev/guide/game-engine/unity-webgl-transform/Design/SDKInstaller.html) - [微信资源按需加载概述](https://developers.weixin.qq.com/minigame/dev/guide/game-engine/unity-webgl-transform/Design/ResourcesLoading.html) - [微信使用 AssetBundle 进行资源按需加载](https://developers.weixin.qq.com/minigame/dev/guide/game-engine/unity-webgl-transform/Design/UsingAssetBundle.html) - [微信 FileSystemManager.readFile](https://developers.weixin.qq.com/minigame/dev/api/file/FileSystemManager.readFile.html) ## 十二、文档关联 | 文档 | 关联原因 | |---|---| | [资源管理](README.md) | 当前资源框架职责和实现边界 | | [资源管理使用指南](使用指南.md) | 现有加载、卸载和资源更新公开 API | | [Hotfix 启动](../Hotfix启动.md) | Bootstrap AOT 与 HotUpdate 启动顺序 | | [工作流清单](../../../rules/40-工作流清单.md) | 实施、生成、验证和文档同步流程 |