09-01-生态-YooAsset+HybridCLR热更新方案

0 阅读11分钟

生态-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 独立更新通道

代码和资源走完全独立的更新通道,互不干扰:

维度代码热更资源热更
通道HybridCLRYooAsset
频率低(每月一两次)高(每周一两次)
风险高(影响逻辑)中(只影响美术)
测试严格较宽松
灰度强烈建议建议
2.2.2 灵活的发布策略

独立通道带来灵活的发布策略:

  • 紧急 Bug 修复:可以只发代码包,不动资源
  • 新美术资源:可以只发资源包,不动代码
  • 大版本更新:代码包+资源包同步发布
2.2.3 风险隔离

代码和资源问题可以独立回滚:

  • 资源出问题:回滚到上一资源版本(不影响代码)
  • 代码出问题:回滚到上一代码版本(不影响资源)

2.3 协同架构的挑战

2.3.1 版本管理

最关键的问题是代码版本与资源版本的对齐:

场景代码版本资源版本兼容性
资源更新1.0.51.0.6✅ 兼容(资源变更不影响代码接口)
代码更新1.0.61.0.5⚠️ 需谨慎(代码可能引用新资源)
同时更新1.0.61.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 等)原样保留,不解析
加载方式LoadAssetAsyncGetRawFileAsync
用途美术、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.01.0.01.0.02024-01-01
1.0.11.0.11.0.12024-01-15
1.1.01.1.01.1.02024-02-01
1.1.11.1.01.1.12024-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 实践建议

  1. DLL 拆分要合理:太碎导致加载慢,太大导致下载慢
  2. iOS 必须做 AOT 元数据补充:否则必定崩
  3. 灰度一定要做:代码热更的爆炸半径比资源大得多
  4. 强制重启机制必须有:避免 DLL 内存泄漏累积
  5. 版本对应表要严格维护:代码和资源版本错位是最常见的坑

上一篇:完整热更新流程实战 下一篇:小游戏平台适配