生态-YooAsset+HybridCLR热更新方案
篇章:09-生态篇-集成与协同 状态:已完成 阅读时间:约 25 分钟
一、引言
1.1 本章在系列中的定位
在 YooAsset 单独使用时,它能解决资源热更新(美术、UI、关卡、配置等),但无法解决代码热更新(C# 脚本)。而对于商业级 Unity 项目来说,代码热更 + 资源热更是完整热更新能力的两翼:
- 资源热更:解决"看不见的内容"(皮肤、关卡、剧情)
- 代码热更:解决"看不见的逻辑"(Bug 修复、玩法调整)
本章介绍的 YooAsset + HybridCLR 组合,是当前 Unity 生态中最成熟、最稳定的代码+资源双热更方案之一。YooAsset 负责资源,HybridCLR 负责代码,两者通过"将 DLL 打包为 YooAsset 的 RawFile 资源"实现无缝集成。
1.2 本章要解决的核心问题
- YooAsset 与 HybridCLR 如何协同工作?
- DLL 如何作为资源参与 YooAsset 的打包和热更?
- HybridCLR 的 AOT + Interpreter 混合模式如何工作?
- 实战中代码热更与资源热更的版本如何对齐?
1.3 阅读前置要求
- 完整阅读 08-核心技术-热更新系统章节
- 了解 .NET 平台 AOT 与 JIT 的差异
- 了解 Unity 中 DLL 与 Assembly 的基本概念
二、代码热更+资源热更协同架构
2.1 协同架构的设计
在 YooAsset + HybridCLR 的协同方案中,整个游戏被分为三个层次:
[游戏包体层次]
│
├── 1. 主包(安装包内)
│ ├── YooAsset Runtime DLL(AOT)
│ ├── HybridCLR Runtime DLL(AOT)
│ ├── 启动代码(主 DLL)
│ ├── 启动场景与 UI
│ └── 初始 YooAsset 清单
│
├── 2. 热更包 1:代码 DLL(HybridCLR)
│ ├── Hotfix.GameLogic.dll
│ ├── Hotfix.UI.dll
│ ├── Hotfix.Network.dll
│ └── ... (业务代码)
│
└── 3. 热更包 2:资源(YooAsset)
├── Prefabs
├── Textures
├── Audio
├── Config
└── Scenes
协同工作流程:
[游戏启动]
│
▼
[加载主包代码] ─── AOT 编译(已编译进可执行文件)
│
▼
[初始化 YooAsset]
│ ├── 加载本地资源清单
│ ├── 检查资源更新
│ └── 下载差异资源
│
▼
[加载热更代码 DLL] ─── HybridCLR 加载
│ ├── 解析 DLL 元数据
│ ├── 注册到 HybridCLR Runtime
│ └── 等待 AOT 桥接
│
▼
[从热更代码创建启动类]
│
▼
[进入游戏主逻辑]
2.2 协同架构的优势
2.2.1 独立更新通道
代码和资源走完全独立的更新通道,互不干扰:
| 维度 | 代码热更 | 资源热更 |
|---|---|---|
| 通道 | HybridCLR | YooAsset |
| 频率 | 低(每月一两次) | 高(每周一两次) |
| 风险 | 高(影响逻辑) | 中(只影响美术) |
| 测试 | 严格 | 较宽松 |
| 灰度 | 强烈建议 | 建议 |
2.2.2 灵活的发布策略
独立通道带来灵活的发布策略:
- 紧急 Bug 修复:可以只发代码包,不动资源
- 新美术资源:可以只发资源包,不动代码
- 大版本更新:代码包+资源包同步发布
2.2.3 风险隔离
代码和资源问题可以独立回滚:
- 资源出问题:回滚到上一资源版本(不影响代码)
- 代码出问题:回滚到上一代码版本(不影响资源)
2.3 协同架构的挑战
2.3.1 版本管理
最关键的问题是代码版本与资源版本的对齐:
| 场景 | 代码版本 | 资源版本 | 兼容性 |
|---|---|---|---|
| 资源更新 | 1.0.5 | 1.0.6 | ✅ 兼容(资源变更不影响代码接口) |
| 代码更新 | 1.0.6 | 1.0.5 | ⚠️ 需谨慎(代码可能引用新资源) |
| 同时更新 | 1.0.6 | 1.0.6 | ✅ 同步发版 |
解决方案:建立版本对应表,明确每次发版的代码版本与资源版本对应关系。
2.3.2 依赖管理
代码与资源存在依赖关系:
- 代码引用了资源(如
LoadAssetAsync<Sprite>("Icon")) - 资源被代码引用(Prefab 上的脚本引用)
依赖管理需要:
- 代码中使用地址常量(如
"Icon:Button_Normal"),避免硬编码路径 - 资源命名要稳定,不轻易改名
- 代码改动要评估对资源的影响
2.3.3 测试复杂度
代码+资源的联合测试是难点:
测试矩阵
─────────────────────────────────────────
代码版本 \ 资源版本 │ 1.0.5 │ 1.0.6 │
─────────────────────────────────────────
1.0.5 │ ✓ │ △ │ (资源升级,需测)
1.0.6 │ ✗ │ ✓ │ (代码升级,旧资源可能缺)
─────────────────────────────────────────
推荐策略:
- 新代码 + 新资源:主测
- 新代码 + 旧资源:边缘测试
- 旧代码 + 新资源:基本不需要(运营会强制升级)
- 旧代码 + 旧资源:N/A
三、DLL 打包为 RawFile
3.1 RawFile 的概念
RawFile(原始文件)是 YooAsset 中的一种特殊资源类型:
| 特性 | 普通资源 | RawFile |
|---|---|---|
| 解析方式 | YooAsset 解析(识别为 Sprite、AudioClip 等) | 原样保留,不解析 |
| 加载方式 | LoadAssetAsync | GetRawFileAsync |
| 用途 | 美术、UI、配置 | DLL、密钥、二进制资源 |
| 压缩 | 默认 LZ4 压缩 | 可选压缩 |
| 加密 | 支持 | 支持 |
3.2 DLL 打包为 RawFile
3.2.1 收集器配置
YooAsset 的 AssetBundleCollector 可以识别 DLL 文件并将它们归类为 RawFile:
public class HybridCLRBuildPipeline
{
public List<PackRule> GetPackRules()
{
return new List<PackRule>
{
// 1. HybridCLR 热更 DLL 单独打包
new PackRule
{
Name = "HybridCLRDll",
Filter = "Assets/Hotfix/**/bin/**/Hotfix*.dll",
PackTo = EBundlePackTarget.HotUpdatePackage,
AssetType = EAssetType.RawFile // 关键:标记为 RawFile
},
// 2. 普通资源
new PackRule
{
Name = "NormalAsset",
Filter = "Assets/**/*",
PackTo = EBundlePackTarget.HotUpdatePackage,
ExcludeFilters = new[] { "Assets/Hotfix/**/*" }
}
};
}
}
3.2.2 YooAsset 中的 DLL 加载
加载 RawFile DLL 的代码示例:
using YooAsset;
public class HybridCLRBridge
{
/// <summary>
/// 从 YooAsset 加载 DLL
/// </summary>
public async Task<byte[]> LoadDllBytes(string dllName)
{
var op = YooAssets.GetRawFileAsync($"Assets/Hotfix/{dllName}.dll");
await op.Task;
if (op.Status != EOperationStatus.Succeed)
{
Debug.LogError($"加载 DLL 失败: {op.Error}");
return null;
}
return op.Result;
}
}
3.2.3 加载流程
[YooAsset 加载 DLL]
│
▼
[GetRawFileAsync]
│
▼
[查询 Bundle 信息]
│
▼
[检查本地是否已下载]
│
├── 是 → [从本地读取]
│
└── 否 → [下载 Bundle]
│
▼
[解压(如果是 LZ4)]
│
▼
[解密(如果加密)]
│
▼
[校验 Hash]
│
▼
[缓存到本地]
│
▼
[读取]
│
▼
[返回 DLL 字节流]
3.3 DLL 的热更新
3.3.1 热更新流程
[代码热更流程]
│
▼
[开发阶段]
│ ├── 修改 C# 脚本
│ ├── 编译 Hotfix DLL
│ └── 输出到 Assets/Hotfix/...
│
▼
[构建阶段]
│ ├── 收集 DLL
│ ├── 打包为 AB
│ ├── 生成 Manifest
│ └── 上传到 CDN
│
▼
[运行阶段]
│ ├── 启动时检查代码版本
│ ├── 下载新 DLL
│ ├── 加载到 HybridCLR
│ └── 重启游戏激活新代码
3.3.2 代码版本管理
代码版本需要单独的版本管理:
public class CodeVersionConfig
{
public string CodeVersion; // 代码版本,如 "1.0.6"
public List<string> DllList; // DLL 文件列表
public string[] ResourceVersions; // 兼容的资源版本列表
public string Hash; // 代码包整体 Hash
}
3.3.3 DLL 回滚机制
DLL 出问题时需要快速回滚:
public class CodeRollback
{
public async Task RollbackToVersion(string targetVersion)
{
// 1. 删除当前版本 DLL
var currentDlls = GetCurrentDlls();
foreach (var dll in currentDlls)
{
YooAssets.RemoveBundle(dll);
}
// 2. 加载目标版本 DLL
var targetDlls = await LoadVersionDlls(targetVersion);
// 3. 重新加载到 HybridCLR
HybridCLRManager.LoadDlls(targetDlls);
// 4. 重启游戏
RestartGame();
}
}
四、HybridCLR 集成
4.1 HybridCLR 简介
HybridCLR(原 Huatuo)是 Unity 平台的完整代码热更新方案。它通过 AOT + Interpreter 混合运行模式,让 Unity 项目具备接近原生 IL2CPP 的性能 + 完整的 C# 运行时能力。
4.1.1 HybridCLR 的工作原理
[传统 IL2CPP 模式]
├── 所有 C# 代码 → AOT 编译 → 静态二进制
└── 运行时无法修改代码
[HybridCLR 模式]
├── 启动 DLL → AOT 编译 → 静态二进制
├── 热更 DLL → IL 字节码 → HybridCLR 解释执行
└── AOT 与 Hotfix 通过"桥接"互通
4.1.2 HybridCLR 的核心优势
| 优势 | 说明 |
|---|---|
| 零运行时开销 | Hotfix 代码与 AOT 代码性能一致 |
| 完整 C# 支持 | 几乎所有 C# 特性都支持 |
| 无需额外胶水代码 | 不需要为每个类写"包装" |
| 兼容性好 | 支持 Unity 2019+ |
| 性能优 | 接近原生性能(解释器经过优化) |
| 成熟稳定 | 已有多款商业项目验证 |
4.2 HybridCLR 与 YooAsset 的集成
4.2.1 整体架构
[启动阶段]
│
▼
[主包代码 AOT 编译]
│ ├── YooAsset Runtime
│ ├── HybridCLR Runtime
│ ├── 启动入口 GameLauncher
│ └── HybridCLR 桥接层
│
▼
[GameLauncher.Launch]
│
▼
[初始化 YooAsset]
│ ├── 加载本地资源清单
│ ├── 检查资源更新
│ └── 下载差异资源
│
▼
[加载 Hotfix DLL]
│ ├── 从 YooAsset 加载 DLL
│ ├── HybridCLR.LoadAssembly
│ └── 注册到 AppDomain
│
▼
[激活 Hotfix 入口]
│ ├── 通过反射调用 Hotfix.Program.Main
│ └── 主程序接管控制权
│
▼
[进入游戏]
4.2.2 启动入口
主包中需要保留的"启动入口":
// 主包代码 - GameLauncher.cs
public class GameLauncher
{
public static async void Launch()
{
// 1. 初始化 YooAsset
var initOp = YooAssets.InitializeAsync();
await initOp.Task;
// 2. 检查并下载资源
await UpdateResources();
// 3. 加载 Hotfix DLL
var hotfixAssembly = await LoadHotfixDll("Hotfix.GameLogic");
// 4. 激活 Hotfix 入口
var entryType = hotfixAssembly.GetType("Hotfix.GameLogic.Program");
var entryMethod = entryType.GetMethod("Main", BindingFlags.Public | BindingFlags.Static);
entryMethod.Invoke(null, null);
}
static async Task<Assembly> LoadHotfixDll(string dllName)
{
// 从 YooAsset 加载 DLL
var op = YooAssets.GetRawFileAsync($"Assets/Hotfix/{dllName}.dll.bytes");
await op.Task;
byte[] dllBytes = op.Result;
// HybridCLR 加载
return HybridCLRHelper.LoadAssembly(dllBytes);
}
}
4.2.3 HybridCLR 桥接层
主包与 Hotfix 之间需要桥接。桥接代码放在主包中:
// 主包代码 - HybridCLRHelper.cs
public static class HybridCLRHelper
{
public static Assembly LoadAssembly(byte[] dllBytes)
{
// HybridCLR 加载 DLL
var assembly = HybridCLR.RuntimeApi.LoadAssembly(dllBytes);
return assembly;
}
public static void LoadMetadataForAOTAssembly(byte[] dllBytes)
{
// 补充 AOT 元数据
HybridCLR.RuntimeApi.LoadMetadataForAOTAssembly(dllBytes, HomologousImageMode.SuperSet);
}
}
4.3 集成配置
4.3.1 YooAsset 配置
// YooAsset 收集器配置
[DisplayName("HybridCLR Dll")]
public class HybridCLRDllCollector : IPackRule
{
public bool Match(AssetInfo info)
{
return info.AssetPath.Contains("/Hotfix/") && info.AssetPath.EndsWith(".dll");
}
public string GetBundleName(AssetInfo info)
{
return "hybridclr_dlls";
}
public EAssetType GetAssetType(AssetInfo info)
{
return EAssetType.RawFile;
}
}
4.3.2 HybridCLR 配置
// HybridCLR 配置文件
public class HybridCLRSettings : ScriptableObject
{
[Header("Hotfix DLLs")]
public string[] HotfixDlls = new[]
{
"Hotfix.GameLogic",
"Hotfix.UI",
"Hotfix.Network"
};
[Header("AOT DLLs")]
public string[] AotDlls = new[]
{
"mscorlib",
"System",
"UnityEngine"
};
}
4.3.3 启动流程
public class StartupFlow
{
public static async Task Bootstrap()
{
// 1. 加载 AOT 元数据(如果是 iOS 等需要补充元数据的平台)
await LoadAOTMetadata();
// 2. 初始化 YooAsset
var initOp = YooAssets.InitializeAsync();
await initOp.Task;
// 3. 检查并更新资源
await CheckAndUpdate();
// 4. 加载 Hotfix DLL
var assemblies = new List<Assembly>();
foreach (var dllName in HybridCLRSettings.HotfixDlls)
{
var op = YooAssets.GetRawFileAsync($"Assets/Hotfix/{dllName}.dll.bytes");
await op.Task;
var asm = HybridCLRHelper.LoadAssembly(op.Result);
assemblies.Add(asm);
}
// 5. 激活 Hotfix 入口
var entryType = assemblies[0].GetType("Hotfix.GameLogic.Program");
var main = entryType.GetMethod("Main");
main.Invoke(null, null);
}
static async Task LoadAOTMetadata()
{
var aotList = HybridCLRSettings.AotDlls;
foreach (var aotName in aotList)
{
var op = YooAssets.GetRawFileAsync($"Assets/AOT/{aotName}.dll.bytes");
await op.Task;
HybridCLRHelper.LoadMetadataForAOTAssembly(op.Result);
}
}
}
五、实战踩坑记录
5.1 常见问题
问题 1:iOS 平台 DLL 加载失败
现象:
- Android 上正常,iOS 上加载 DLL 直接崩
- 错误信息:
AOT module 'X' not found
原因: iOS 平台由于系统限制,AOT 编译时会剥离未使用的元数据。Hotfix DLL 中如果引用了 AOT DLL 中未被显式使用的类型,运行时会找不到。
解决:
使用 HybridCLR 的 LoadMetadataForAOTAssembly 补充元数据:
public static async Task LoadIOSMetadata()
{
#if UNITY_IOS && !UNITY_EDITOR
var aotList = new[] { "mscorlib", "System", "System.Core" };
foreach (var aotName in aotList)
{
var op = YooAssets.GetRawFileAsync($"Assets/AOT/{aotName}.dll.bytes");
await op.Task;
HybridCLRHelper.LoadMetadataForAOTAssembly(op.Result,
HomologousImageMode.SuperSet);
}
#endif
}
问题 2:热更代码引用了主包私有类型
现象:
- 代码编译通过
- 运行时找不到类型
Hotfix cannot find type 'MainPackage.SecretClass'
原因: Hotfix DLL 引用了主包中未公开的类。HybridCLR 模式下,Hotfix 与 AOT 之间的依赖必须显式声明。
解决:
- 用接口隔离:在主包定义接口,Hotfix 实现
- 用桥接类:在主包中暴露的类,避免 Hotfix 直接访问私有类
// 主包 - 公开的接口
public interface IPlayerInfo
{
long GetPlayerId();
string GetPlayerName();
}
// 主包 - 私有类
internal class PlayerInfoImpl : IPlayerInfo
{
public long GetPlayerId() => 12345;
public string GetPlayerName() => "Player";
}
// Hotfix 可以引用 IPlayerInfo,但不能引用 PlayerInfoImpl
问题 3:热更代码改了泛型,运行时崩
现象:
- 修改了
List<T>的具体用法 - iOS 闪退,Android 报
MissingMethodException
原因:
AOT 模式下,泛型会在编译时单态化(针对每个具体类型生成代码)。如果 Hotfix 中使用了一个 AOT 中没出现过的泛型实例化(如 List<MyNewClass>),AOT 中没有对应代码,运行时会崩。
解决:
- 在 AOT 中预热所有可能用到的泛型
- 使用
Link.xml配置保留 - 避免在 Hotfix 中引入新的泛型实例化
// 主包 AOT 中预热
public class GenericPrewarm
{
public static void Prewarm()
{
// 触发 AOT 编译
var _ = new List<MyNewClass>();
var __ = new Dictionary<string, MyNewClass>();
}
}
问题 4:内存泄漏
现象:
- 多次热更后内存持续上涨
- 玩家反馈"游戏越玩越卡"
原因: HybridCLR 加载 DLL 后,旧的 DLL 实例无法完全卸载(.NET Framework 没有 Assembly Unload,iOS 也不支持)。如果每次热更都加载新 DLL,会导致多份 DLL 同时在内存中。
解决:
- 强制重启:每次代码热更后强制重启游戏
- 定时重启:超过 X 天的玩家强制重启
- 限制热更次数:只支持 X 次热更,超出强制升级到新版本
public class HotfixVersionManager
{
public bool NeedReboot()
{
var lastVersion = PlayerPrefs.GetString("LastHotfixVersion", "");
var currentVersion = GetCurrentHotfixVersion();
if (lastVersion != currentVersion)
{
PlayerPrefs.SetString("LastHotfixVersion", currentVersion);
return true; // 需要重启
}
return false;
}
}
5.2 解决方案总结
| 问题 | 触发条件 | 解决方案 |
|---|---|---|
| iOS 元数据缺失 | iOS 平台 | LoadMetadataForAOTAssembly |
| 类型找不到 | 引用了私有类 | 用接口隔离 |
| 泛型崩溃 | 引入新泛型 | AOT 预热 + Link.xml |
| 内存泄漏 | 多次热更 | 强制重启 + 限制次数 |
| DLL 加载失败 | 路径错误 | 校验 + 错误处理 |
| 版本不一致 | 代码资源版本错位 | 强校验 + 灰度 |
5.3 最佳实践
5.3.1 代码分离
将代码严格分为三层:
代码分层
─────────────────────────────────────
主包代码 (AOT)
├── YooAsset Runtime
├── HybridCLR Runtime
├── 启动入口 GameLauncher
├── HybridCLR 桥接层
└── 必要抽象接口
Hotfix DLL 1 - 基础设施
├── 通用工具类
├── 日志
├── 配置加载
└── 网络请求
Hotfix DLL 2 - 业务逻辑
├── 战斗
├── 角色
├── 任务
└── 活动
Hotfix DLL 3 - UI
├── 主城 UI
├── 战斗 UI
├── 商城 UI
└── 活动 UI
─────────────────────────────────────
5.3.2 版本控制
建立版本对应表:
| 总版本 | 代码版本 | 资源版本 | 发布时间 |
|---|---|---|---|
| 1.0.0 | 1.0.0 | 1.0.0 | 2024-01-01 |
| 1.0.1 | 1.0.1 | 1.0.1 | 2024-01-15 |
| 1.1.0 | 1.1.0 | 1.1.0 | 2024-02-01 |
| 1.1.1 | 1.1.0 | 1.1.1 | 2024-02-15 (只更资源) |
5.3.3 灰度发布
代码热更的灰度策略:
5% (1 天) → 20% (1 天) → 50% (2 天) → 100%
每阶段监控:
- 启动崩溃率 < 0.5%
- 关键功能成功率 > 99%
- 内存增长 < 50MB/h
六、性能优化
6.1 DLL 体积优化
| 优化手段 | 效果 |
|---|---|
启用 Link.xml 精确裁剪 | 减少 20-40% 体积 |
启用 il2cpp 高级优化 | 减少 10-20% 体积 |
| 拆分 Hotfix DLL | 按需下载 |
启用 Strip Unused Metadata | 减少 5-10% 体积 |
6.2 启动速度优化
public class StartupOptimization
{
public static async Task OptimizedStartup()
{
// 1. 并行加载多个 DLL
var dllTasks = new List<Task<Assembly>>();
foreach (var dllName in HotfixDlls)
{
dllTasks.Add(LoadDllAsync(dllName));
}
await Task.WhenAll(dllTasks);
// 2. 延迟加载:非关键 DLL 后台加载
LoadCriticalDlls();
_ = Task.Run(LoadNonCriticalDllsAsync);
}
}
七、总结
7.1 本章要点回顾
- 协同架构:代码与资源走独立更新通道,灵活且风险隔离
- DLL 打包:将 Hotfix DLL 打包为 YooAsset 的 RawFile 资源
- HybridCLR 集成:AOT + Interpreter 混合模式,零额外开销
- 踩坑记录:iOS 元数据、泛型、内存泄漏、版本对齐是四大常见问题
7.2 与前后章节的关联
- 前章:08 章解决了"如何热更资源"
- 本章:补充了"如何热更代码",与资源热更形成完整闭环
- 后章:09-02 将介绍小游戏平台适配,解决"如何在小游戏平台热更"
7.3 实践建议
- DLL 拆分要合理:太碎导致加载慢,太大导致下载慢
- iOS 必须做 AOT 元数据补充:否则必定崩
- 灰度一定要做:代码热更的爆炸半径比资源大得多
- 强制重启机制必须有:避免 DLL 内存泄漏累积
- 版本对应表要严格维护:代码和资源版本错位是最常见的坑