HybridCLR 热更新技术浅谈

Unity IL2CPP 热更新解决方案完整指南

📖一、HybridCLR 概述

1.1 什么是 HybridCLR

HybridCLR(原 Huatuo)是 Unity IL2CPP 的扩展方案,在保留 IL2CPP 性能优势的同时,增加了代码热更新能力。

1.2 能否热更新 AOT 中的逻辑?

答案:不能直接热更新 AOT 代码
代码类型 能否热更新 说明
AOT 代码 ❌ 不能 已编译成机器码,嵌入游戏包
热更新代码 ✅ 可以 IL 格式,可动态加载替换
解决方案:将需要热更新的逻辑放入热更新程序集(如 HotUpdate.dll)

⚙️二、核心原理

2.1 架构图

Unity IL2CPP 运行时
AOT 代码
  • IL → C++ → 机器码
  • 原生执行
热更新代码
  • IL 字节码
  • HybridCLR 解释器执行
HybridCLR 运行时
元数据系统
Metadata
IL 解释器
Interpreter

2.2 核心组件

IL 解释器

热更新 DLL (IL字节码)
IL 解释器
HybridCLR 的核心(C++ 实现)
逐条解释执行 IL 指令
执行结果

元数据注册

将热更新程序集的类型信息注册到 IL2CPP 的元数据系统,使 AOT 代码可以通过反射等方式调用热更新代码。

2.3 执行流程

1
游戏启动
2
加载 AOT 程序集(原生执行)
3
初始化 HybridCLR 运行时
4
加载补充元数据
5
加载热更新 DLL
6
执行热更新代码
• 热更新代码 → IL 解释器执行
• AOT 代码 → 原生代码执行

2.4 与其他方案对比

特性 HybridCLR ILRuntime Lua/xLua
语言 C# C# Lua
执行方式 IL 解释 IL 解释 Lua 虚拟机
性能 较高 中等 中等
开发体验 原生 C# 需要适配 需要学 Lua
与 Unity 兼容 完全兼容 部分限制 需要绑定

📦三、DLL 基础知识

3.1 什么是 DLL

DLL = Dynamic Link Library(动态链接库)

DLL 是把代码打包成一个文件,供其他程序调用使用的"代码包"。

3.2 两种 DLL 类型

原生 DLL (Native DLL)

  • 包含:机器码
  • 直接被 CPU 执行
  • 语言:C / C++ / Rust / Go

托管 DLL (.NET DLL)

  • 包含:IL 字节码
  • 需要 .NET 运行时
  • 语言:C# / VB.NET / F#

3.3 原生 DLL 效率对比

语言 效率 原因
C ⭐⭐⭐⭐⭐ 零开销抽象,直接映射硬件
C++ ⭐⭐⭐⭐⭐ 同 C,编译器优化成熟
Rust ⭐⭐⭐⭐⭐ 零成本抽象,LLVM 优化
Go ⭐⭐⭐⭐ 有 GC、运行时开销

为什么都是机器码,效率还有差异?

  1. 编译器生成的指令数量不同
  2. 语言特性强制的额外代码(如边界检查、GC)
  3. 运行时库的开销
托管 DLL:C#、VB.NET、F# 生成的托管 DLL 效率基本相同,因为最终都是 IL 字节码,由同一个运行时执行。

🔧四、补充元数据

4.1 为什么需要补充元数据

问题:AOT 泛型限制

IL2CPP 在 AOT 编译时,只会为"用到的"泛型组合生成代码:

AOT 代码中有:
├─ List<int> ✅ 生成代码
├─ List<string> ✅ 生成代码
└─ List<Player> ✅ 生成代码
但 AOT 代码中没有:
└─ List<HotClass> ❌ 没有生成代码!
热更新代码想用 List<HotClass> 就会出问题!

4.2 补充元数据是什么

补充元数据 = AOT 程序集的原始 DLL(IL 格式)
运行阶段
AOT 机器码
  • 只有 List<int>
  • 只有 List<string>
  • 没有 List<HotClass>
+
补充元数据
  • mscorlib.dll (原始)
  • 包含 List<T> 完整 IL 定义
HybridCLR 解释器可以为 List<HotClass> 解释执行 IL

4.3 加载补充元数据代码示例

private static readonly string[] AOTAssemblies = new string[]
{
    "mscorlib.dll",
    "System.dll",
    "System.Core.dll",
    "UnityEngine.CoreModule.dll",
    // ... 其他需要的程序集
};

void LoadMetadataForAOTAssemblies()
{
    foreach (var aotDll in AOTAssemblies)
    {
        byte[] dllBytes = LoadDllBytes(aotDll);
        
        LoadImageErrorCode err = RuntimeApi.LoadMetadataForAOTAssembly(
            dllBytes, 
            HomologousImageMode.SuperSet
        );
        
        Debug.Log($"LoadMetadataForAOTAssembly {aotDll}: {err}");
    }
}

4.4 补充元数据的特性

特性 说明
一次补充,永久适用 泛型定义是 List<T>,T 可以是任何类型
可分批加载 只要在使用前加载即可
值类型也支持 IL 是参数化的,运行时确定类型大小
执行方式 HybridCLR 解释执行 IL(比机器码慢 3-10 倍)

4.5 需要补充哪些程序集

常见需要补充的:
├─ mscorlib.dll (List<T>, Dictionary<K,V>...)
├─ System.Core.dll (Linq, Action<T>, Func<T>...)
├─ System.dll (各种系统类型)
├─ UnityEngine.CoreModule.dll
└─ 其他你用到泛型的 AOT 程序集
使用 HybridCLR 工具自动分析:HybridCLR → Generate → AOTGenericReferences

📋五、link.xml 代码保留

5.1 为什么需要 link.xml

Unity IL2CPP 构建时会进行代码裁剪(Stripping),移除"未使用"的代码以减小包体。

但有些代码是动态调用的,编译器检测不到:

// 这些调用方式,编译器检测不到!
Type type = Type.GetType("MyNamespace.MyClass");  // 反射
JsonUtility.FromJson<MyClass>(jsonStr);           // 序列化

5.2 link.xml 的作用

link.xml = 告诉 Unity "这些代码不要裁剪!"

5.3 语法示例

<linker>
    <!-- 保留整个程序集的所有内容 -->
    <assembly fullname="mscorlib" preserve="all"/>
    
    <!-- 保留程序集中的特定类型 -->
    <assembly fullname="UnityEngine.CoreModule">
        <type fullname="UnityEngine.Vector3" preserve="all"/>
        <type fullname="UnityEngine.Quaternion" preserve="all"/>
    </assembly>
    
    <!-- 保留命名空间下的所有类型 -->
    <assembly fullname="Assembly-CSharp">
        <namespace fullname="MyGame.Data" preserve="all"/>
    </assembly>
</linker>

⚖️六、补充元数据 vs link.xml

6.1 本质区别

特性 link.xml 补充元数据
保留的是 机器码(IL 被转换丢弃) IL 字节码
执行方式 CPU 直接执行 HybridCLR 解释执行
解决的问题 防止代码被删除 提供泛型 IL 定义
作用时机 构建时 运行时

6.2 图解

link.xml

原始 DLL (IL)
↓ link.xml 说"保留"
IL2CPP 不删除,但转换
C++ → 机器码
(IL 没了)

补充元数据

原始 DLL (IL)
↓ 直接加载
IL 字节码
(保持 IL 格式)
协作关系:两者是互补的,解决不同层面的问题:
  • link.xml:确保代码"存在"(不被删除)
  • 补充元数据:确保泛型定义"可用"(能被解释执行)

🔗七、热更新与 AOT 互调用

7.1 热更新调用 AOT

热更新代码可以直接调用 AOT 代码,就像普通 C# 代码一样:

// 热更新代码中
using UnityEngine;          // Unity API (AOT)
using GameCore;             // 游戏核心框架 (AOT)

namespace HotUpdate
{
    public class HotLogic
    {
        public void Start()
        {
            // ✅ 调用 Unity API(AOT 机器码)
            Debug.Log("Hello from HotUpdate!");
            
            // ✅ 调用游戏核心框架(AOT)
            GameManager.DoSomething();
            
            // ✅ 使用 .NET 类型(AOT)
            var list = new List<string>();
        }
    }
}

7.2 泛型共享机制

引用类型的泛型可以共享机器码:

List<string> ─┐
List<object> ─┼─► 共享同一份机器码(都是指针)
List<HotClass> ─┘
这就是为什么热更新类(引用类型)作为泛型参数时通常不会报错。

7.3 注意事项

  1. 确保 AOT 类型没被裁剪(link.xml)
  2. 泛型方法需要补充元数据
  3. 热更新项目需要引用 AOT 程序集才能编译

📁八、程序集引用配置

8.1 命名空间 vs 程序集

命名空间 (namespace) ≠ 程序集 (assembly)
  • 命名空间:只是代码的逻辑分组,用于避免命名冲突
  • 程序集:编译后的 DLL 文件,是实际的代码单元
跨程序集访问需要:程序集引用 + public 修饰符

8.2 Assembly-CSharp 的特殊性

8.3 热更新程序集配置

关键设置:autoReferenced: false
// HotUpdate.asmdef
{
    "name": "HotUpdate",
    "references": [
        "GameCore"
    ],
    "autoReferenced": false,
    "includePlatforms": [],
    "excludePlatforms": []
}

autoReferenced: false 禁止被 Assembly-CSharp 自动引用

8.4 推荐项目架构

GameCore.asmdef
AOT 共享层
autoReferenced: true
引用
引用
Assembly-CSharp
AOT
  • 启动入口
  • HybridCLR 初始化
autoReferenced: true (默认)
HotUpdate
热更新
  • 业务逻辑
  • UI 界面
  • 玩法逻辑
autoReferenced: false ← 关键!

九、最佳实践

9.1 项目结构

Assets/
├─ GameCore/ ← AOT 共享层
├─ GameCore.asmdef
├─ Managers/
├─ Interfaces/
└─ Utils/
├─ AOT/ ← AOT 启动入口 (Assembly-CSharp)
└─ InitGame.cs
├─ HotUpdate/ ← 热更新程序集
├─ HotUpdate.asmdef
├─ UI/
├─ Logic/
└─ Systems/
└─ StreamingAssets/ ← 补充元数据 & 热更新 DLL
├─ mscorlib.dll.bytes
├─ System.Core.dll.bytes
└─ HotUpdate.dll.bytes

9.2 补充元数据策略

// 推荐:启动时加载常用的 AOT 程序集
void LoadMetadata()
{
    // 必须加载
    LoadMetadataForAOTAssembly("mscorlib.dll");
    LoadMetadataForAOTAssembly("System.Core.dll");
    
    // 按需加载
    LoadMetadataForAOTAssembly("UnityEngine.CoreModule.dll");
    LoadMetadataForAOTAssembly("UnityEngine.PhysicsModule.dll");
    LoadMetadataForAOTAssembly("UnityEngine.UI.dll");
}

9.3 link.xml 策略

宁可多保留,也不要漏掉。多保留只是增加包体,漏掉会导致运行时崩溃。

9.4 性能考虑

代码位置 执行方式 性能 建议
AOT 机器码 ⚡ 最快 性能关键代码
热更新(已有泛型实例) 机器码共享 ⚡ 快 大部分代码
热更新(新泛型组合) IL 解释 🐢 慢 3-10 倍 非性能关键代码

附录:常见问题

Q1: 热更新代码能调用 AOT 代码吗?

A: 可以,直接引用即可,像普通 C# 代码一样。

Q2: AOT 代码能调用热更新代码吗?

A: 不能直接调用(编译时热更新代码不存在),但可以通过接口/委托/反射间接调用。

Q3: 补充元数据可以热更新吗?

A: 技术上可以,但通常不需要,因为它们随 Unity 版本固定,基本不会变。

Q4: 值类型泛型也能热更吗?

A: 可以,补充元数据提供的 IL 是参数化的,HybridCLR 解释器会在运行时处理任意值类型。

Q5: 解释执行的性能如何?

A: 比 AOT 机器码慢 3-10 倍,但对于业务逻辑通常可以接受。