HybridCLR 补充元数据笔记:原理、裁剪、AB 和热更边界

Unity 开发高级/资深 热更新 资源管理

2026-10-23,一次 HybridCLR 补充元数据问答的整理。场景是 Unity IL2CPP + HybridCLR,代码混淆开着,补充元数据走 YooAsset。下面按问题顺序记,后半是容易混的点。

旧笔记 Unity 资源热更概念与分包地图 里有一句「热更代码新用到一个 AOT 泛型组合,就必须更新补充元数据」。对过一次真实工程后,这句话要收窄,见文末「和旧笔记的差别」。

补充元数据是什么

补充元数据是一份已经打进主包的 AOT 程序集的裁剪后 DLL。运行时再加载一次,是为了让热更代码能补上主包里没生成的 AOT 泛型实例。它不替换主包里的 native 代码,也不等于热更 DLL。

IL2CPP 是全 AOT。泛型只有在打包时被 AOT 代码用到的那些封闭实例,才会生成 native 函数。热更程序集由解释器执行,它可以调用 AOT 程序集。一旦调用的封闭实例主包里没有,native 函数不存在。

LoadMetadataForAOTAssembly 把裁剪后的 AOT DLL 挂到已经存在的那个 AOT 程序集上。某个泛型实例的 native 函数存在时继续走 native;不存在时,用这份 DLL 里的 IL 解释执行。进程里仍然只有一个 mscorlib。多出来的是它的同源镜像(homologous image)。

启动进游戏前加载,模式用的是 HomologousImageMode.SuperSet。注释的意思是:加载后自动 hook,AOT 泛型的 native 函数不存在时走解释器。这个 hook 不是改 libil2cpp 里已有函数的机器码,而是查找封闭实例失败时落到镜像里的 IL。

三个词

打完 IL2CPP,一个 AOT 程序集同时留下三份东西。

产物 里面有什么 谁用
libil2cpp 里的函数 打包时用到过的每个封闭泛型实例的机器码 CPU 直接调用
global-metadata.dat Unity 自己的类型表、MethodInfo、函数指针索引。IL 已经丢掉 libil2cpp 运行时
补充元数据 DLL 裁剪后仍保留的 CLI 表,以及泛型方法的 IL HybridCLR 解释器

global-metadata.dat 解决不了缺口。IL 转换时已经丢了,表里也只登记打包时真正实例化过的封闭泛型。解释器要执行一条任意的新实例,只能读回一份仍带 IL 的托管 DLL。

CLI 元数据表

CLI 是 Common Language Infrastructure,公共语言基础设施,标准编号 ECMA-335。DLL 里除了文件头,还有一块登记「有哪些类型、方法、字段」的数据。它不负责干活,所以叫元数据。文件里是一张张表:

表 一行代表 List<T> 的例子
TypeDef 一个类型 名字 List\1`
Field 一个字段 _items 类型 T[],_size 类型 int
MethodDef 一个方法 Add,参数 T,返回 void,再加上 RVA
GenericParam 一个泛型参数 T
Param 一个形参 Add 的 item

签名是参数类型加返回类型。RVA(Relative Virtual Address)是方法体在 DLL 里的位置。Token 是「第几张表的第几行」。Unity 运行时用另一套下标,所以 SuperSet 按名字和签名对齐,不按 token 对齐。

泛型方法的 IL

IL(Intermediate Language,中间语言,也称 CIL)是方法体编译后的字节码。MethodDef 那一行的 RVA 指向这串字节。

List<T>.Add 的 IL 仍按未绑定的 T 写:读 _size,不够就扩容,把参数写入 _items,返回。解释执行时带着 { T = 某个热更类 },读到和 T 有关的指令就把 T 换成具体类型。同一份 IL 可以套很多个实参。

还写着 T 的叫泛型定义,也叫开放定义。填上具体类型之后的 List<某个热更类>.Add 叫封闭实例。用实参去套定义,这个动作叫膨胀。

上一轮里说过的「反向」是另一条路。主包 native 通过委托或虚函数回调热更方法时,执行的是热更程序集里那个方法自己的 IL。补充元数据里的 IL 是 AOT 泛型定义,例如 mscorlib 的 List<T>.Add。全称是 Reverse P/Invoke(反向平台调用)。

native 函数指针

IL2CPP(Intermediate Language To C++)把打包时用到过的封闭实例编译进 libil2cpp。运行时每个方法有一块 MethodInfo,字段 methodPointer 存机器码地址。这个地址就是 native 函数指针。

指针不是 0:解释器把参数交给 MethodBridge.cpp。这座桥按 CPU 的 ABI(Application Binary Interface,参数进哪个寄存器)跳进 libil2cpp。这条路不再读 IL。

指针是 0:没有机器码。解释器用 RVA 从同源镜像取出 List<T>.Add 的 IL,带上这次的类型实参解释执行。

list.Add 怎么分叉

分叉只发生在游戏真正执行 Add 的那一瞬间。程序只问一句:主包里有没有这份 Add 的机器码。有就跳进去,没有就照补充元数据里的做法说明,把 T 换成这次的类型一步步做。

用三个虚构调用看,不要当成某个工程里的字段名:

List<int> scores = new List<int>();
List<double> rates = new List<double>();
List<ItemRow> rows = new List<ItemRow>(); // ItemRow 是类

打首次包。 主包编译只看主包自己的代码。热更程序集不参加这次编译,所以热更里写了 Add,不会让主包多出一份机器码。主包自己用过 List<int>.Add,机器码就在 libil2cpp 里。没用过就没有。同时,裁剪后的 mscorlib 被做成补充元数据放进包,里面留着 List<T>.Add 的做法说明。

玩家第一次打开:先加载热更程序集,再加载 mscorlib 的补充元数据。然后执行 scores.Add(3)。

  • 主包编译过 List<int>.Add:走桥,跳进 libil2cpp。
  • 没编译过:照做法说明执行,T 是 int,往格子里抄 4 字节。

热更包,没有新类型。 只换热更 DLL。主包和补充元数据不变。再执行 Add 时,问题和答案与首次包相同。热更包不会把 List<int>.Add 重新编译进主包。

热更里新增类 ItemRow。 new List<ItemRow>().Add(row) 不可能在首次包里有专属机器码。mscorlib 的补充元数据仍然不用换,做法说明本来就是 List<T>.Add。这次能不能借用别的机器码,看下面的共享。

不能共享的泛型

能共享,就是多份封闭实例共用同一段机器码。不能共享,就是每份的机器码不一样,主包没生成过的那份只能解释。

差别在格子里的东西多大。

  • ItemRow 是类。格子里只放地址,64 位上是 8 字节。List<string>、别的热更类也是 8 字节地址。主包只要为「元素是类」编译过一份 List.Add,新的热更类就可以借用。这叫能共享。借用时靠 rgctx(Runtime Generic Context)记住这次的 T 是谁。
  • int 是 4 字节,double 是 8 字节,数值本身放在格子里。两份机器码不能互换,也借不了「元素是类」的那份。这叫不能共享。

HybridCLR 的扫描器规则是:任何一个泛型参数没有 class 约束,就当成不能共享。List<T> 的 T 没有 where T : class,所以它会被记进 AOTGenericReferences。清单不能保证每一次 Add 都走解释。真正执行时仍然先看 methodPointer。

三次发布里:

调用 结果
scores.Add(3) 找 List<int> 自己的机器码。首次包编译过就走桥,没有就解释。之后的热更包不改变这个结果
rates.Add(1.5) 找另一份 List<double> 的机器码
新增类 有机会借用「元素是类」那份机器码。借到就走桥
新增结构体 大小是新的。没有可借的机器码,解释执行。做法说明仍用原来的补充元数据

哪些文件算补充元数据

名单不是自动生成的。自动生成的是这些文件的内容。

哪几个程序集算补充元数据,手写在一份随主包走的配置里,不是扫热更代码扫出来的。典型会包括 mscorlib、System、UnityEngine.CoreModule,再加工程自己选定的若干 AOT 程序集。名字拼上 bytes 一类后缀,放在热更代码资源目录下的补充元数据子目录。拷贝和启动都只遍历这个数组。

AOTGenericReferences.cs 里的 PatchedAOTAssemblyList 才是扫描热更代码后自动生成的,名单通常更长。启动和拷贝都不读它。HybridCLR Settings 里那份 patchAOTAssemblies 可以是空的,那时它也不决定这个目录。

有的工程会写编辑器代码,把运行时配置同步进 HybridCLR Settings。如果那段被一个没有定义的脚本宏包住,它不会编译,Settings 里的名单就一直是空的。

什么时候要重新生成

只要还是同一个主包,补充元数据的内容就不变。热更只换热更 DLL。

要重新裁剪并重拷补充元数据的情况:

  • 改了会进主包的代码:业务 AOT、框架,或其余随包编译的 AOT 程序集。
  • 改了 link.xml、IL2CPP 裁剪级别、脚本宏。
  • 开着符号混淆打了新包。对齐按混淆后的名字进行,补充 DLL 必须是同一次混淆、同一次裁剪的产物。
  • 换了 Unity 或 HybridCLR 版本。

只改热更程序集,并且新代码用的开放定义已经在现有补充元数据里:不用重出。再增加 List<另一个热更类>,用的仍是 mscorlib 里那份 List<T>.Add。

热更要用一个旧补充 DLL 里根本没有的 AOT 类型或方法:要换一份更全的补充 DLL。它仍必须包含当前主包里已有的全部类型。定义如果在打主包时就被裁掉了,下一包还要先重生成 link.xml。

打包时哪一步生成

完整包会自动生成这些 .bytes。写进资源目录的是「裁完」之后的拷贝。热更包不重新裁剪,只把该主包基线里的原文件拷回去。

完整包里和这件事有关的顺序:

  1. 编译热更 DLL 并拷贝。 只处理热更程序集,写成资源目录里的热更 DLL。不产生补充元数据。
  2. 生成桥、link、裁剪 AOT。 打一次只编译脚本的临时 Player。裁完的 DLL 到 HybridCLRData/AssembliesPostIl2CppStrip/{平台}/。这时资源目录里的补充元数据还没更新。
  3. 按手写名单拷贝。 把 {名字}.dll 覆盖写成资源目录里的 {名字}.bytes。裁剪目录不存在时,这一步会报裁剪后的 AOT DLL 要等 BuildPlayer 才能生成。

热更包从基线的 supplemental/ 把原文件拷回资源目录。只打 AssetBundle、不跑拷贝步骤的流程,不会刷新这些文件。

进主包还是进热更

这些 .bytes 走热更资源,不进主包机器码。完整包的首包里会自带一份。

它们和热更 DLL 放在同一个收集目录下,由 YooAsset 收成「热更新代码」那一组。运行时按资源路径加载。

打完整包时,把整套 AB 拷进 StreamingAssets,所以第一次安装就已经带着补充元数据。之后的热更资源包仍收集整个目录,这个 AB 会再带上,内容还是主包那一份。

哪几个名字要加载,写在 Resources 里的配置里,用 Resources.Load 读取,在主包里。改名单要重新出主包。

同一个主包上,补充元数据不用热更。热更包里如果还带着它们,也是把首包那一份再拷出去。换一份新的,启动加载会对不上当前主包。

为什么和热更 DLL 放在一起

放在同一个收集目录,是为了和热更 DLL 走同一套资源加载,不是因为这份元数据本身要热更。

进游戏场景之前,加载器把这些文件当成 TextAsset 读出来。热更 DLL 已经是这个读法。补充元数据放在子目录,后缀同样是字节文件,地址规则同样是按资源路径。分组只要收集这一个父目录。

YooAsset 的 PackDirectory 用父目录当包名。所以补充元数据子目录打成一个 AB,热更 DLL 因为父目录不同,打成另一个 AB。构建身份文件如果再放一层子目录,还会单独成第三个包。它们只是收集目录在一起。

Consistent 和 SuperSet

当时的启动代码用的是 SuperSet。

  • Consistent:补充 DLL 和主包裁出来的类型、方法集合必须一样。多一个或少一个都失败。
  • SuperSet:补充 DLL 至少包含主包里有的全部类型和方法,允许多出一些。多出来的没有机器码,只能解释执行。两边都有的方法,签名必须仍然一致。主包里已经编译过的方法还是走原来的机器码。

对齐看类型名和方法签名,不是文件字节是否相同。少了主包已有的类型,两种模式都失败。

运行时的执行速度没有区别。模式只影响启动时怎么核对 DLL,不改变之后每次调用走机器码还是解释器。补充元数据和主包是同一次裁剪的结果时,两种模式加载的是同一批字节。SuperSet 只在补充 DLL 真的多出成员时,多付登记和解释这些成员的成本。

AOTGenericReferences 做什么

它是一份扫描笔记。Generate/All 的最后一步扫描热更代码,把不能共享的 AOT 泛型写到 AOTGenericReferences.cs。输出目录由 HybridCLR Settings 指定,常见是 Assets/HybridCLRGenerate/。

文件分三段:

  • PatchedAOTAssemblyList:这些泛型落在哪些 AOT 程序集。
  • 中间注释:泛型类型。
  • RefMethods():全是注释。生成器只写注释,不写真正的 C# 调用。

运行时没有人读这个文件。它不参与补充元数据的生成,也不让 IL2CPP 编出机器码。

想让 List<int>.Add 变成机器码,要在会打进主包的程序集里写真正的调用,例如 new List<int>().Add(0),然后打完整包。热更里才出现的类型,AOT 代码写不出来。类有机会借用「元素是类」的共享机器码。新的结构体仍然解释执行。值得提前写的是调用频繁、又不能共享的那些。热更方法自己仍然是解释执行,只是它调进去的那个 Add 变成机器码。

link.xml 是另一份生成物,告诉 Unity Linker 保住热更引用过的 AOT 类型。preserve="all" 的类型,裁剪后 DLL 里才还保有开放定义和 IL。它不决定运行时加载哪几个补充 DLL,也不会为 List<int> 这种封闭实例生成机器码。

机器码和解释执行差多少

热更程序集整份都是解释执行。补充元数据只决定它调进去的那个 AOT 泛型方法走机器码还是继续解释。

HybridCLR 官方在 OnePlus 9R 上的数值微基准里,AOT 机器码比社区版解释器快大约 4.1 到 90 倍,区间很大,取决于是整数运算、浮点还是更重的指令。官方同时说:社区版除了数值计算和 AOT 差距明显,其余方面差距没那么大,整包游戏帧耗时通常不会按这个上限放大。商业版把数值差距压到大约 1.3 到 12.9 倍。这些数字来自 执行性能,测的是紧循环,不是一次 List.Add。

一次调用要拆成两段成本:

段 走机器码 走解释
热更里的调用方 仍然解释。scores.Add(3) 这行本身在热更方法里 同样解释
List<int>.Add 的方法体 过一次桥进 libil2cpp,方法体是机器码 方法体逐条解释,值类型拷贝最吃亏

桥不是免费的,但通常比把整个 Add(判容量、扩容、按元素大小写入)解释一遍便宜。真正拉开 4 到 90 倍的是热更里自己的数值循环,例如每帧对一大段 int、float 做运算。那种循环即使里面的 Add 走了机器码,循环本身仍在解释器里。

所以:泛型方法体走机器码,省的是这个 AOT 方法。热更函数自己的解释成本还在。

让热更泛型尽量走机器码

能变成机器码的,只有主包 IL2CPP 编译时看得到、并且裁剪后还留着的封闭实例。热更包改不了这件事,要打新的完整包。

按收益做这几件:

  1. 引用类型先别急着补。 List<某个类>、Dictionary<int, 某个类> 的元素或值是指针,经常能借用主包里「元素是类」的那一份共享机器码。热更里新加一个类,往往已经在走机器码。先用真机确认热点,再决定要不要写示例。

  2. 值类型要在 AOT 程序集里写真正的调用。 List<int>、List<float>、Dictionary<int, int> 不能和别的大小共用机器码。在会打进主包的程序集里写会执行到的代码,例如:

static void PreserveGenericAot()
{
    new List<int>().Add(0);
    new List<float>().Add(0f);
    new Dictionary<int, int>().Add(0, 0);
}

AOTGenericReferences.RefMethods() 里的注释不会被编译。link.xml 的 preserve="all" 只保住开放定义,不会生成 List<int>.Add 的机器码。

  1. 这段代码必须活过裁剪。 没人调用的空方法,Unity Linker 会在 IL2CPP 之前删掉,里面的泛型实例一起消失。从确实会进包的启动路径调用它,或用 [Preserve]、link.xml 保住这个方法。保住之后打完整包,下一次热更里的 List<int>.Add 才会看到非 0 的 methodPointer。

  2. 只存在于热更里的结构体,AOT 写不出来。 这种泛型没有可借的机器码,只能解释。它若在热路径上,把结构体挪到 AOT 程序集再按第 2 步写一次调用,或者热路径改成非泛型的专用函数。

  3. 数值大循环不要留在热更里。 把内层循环做成 AOT 方法,热更只调用一次。这样循环本身是机器码,而不只是循环里的某一个泛型调用走机器码。

  4. 只为热点写。 每多一个封闭实例,libil2cpp 更大,打包更慢。调用很少的泛型留着解释即可。

做完用真机看热点函数的耗时,不要用编辑器判断。编辑器里 LoadMetadataForAOTAssembly 直接返回成功,不代表真机上有机器码。

裁剪补充元数据

裁剪就是打主包时 Unity 先把 mscorlib 这类 AOT 程序集删小,再把删小后的那份 DLL 当作补充元数据。删掉的是主包用不到、也没被 link.xml 保住的类型和方法。留下的是还要用的类型表,以及泛型方法的做法说明。

必须用裁过的这份,因为它和主包里实际留下的程序集是同一次裁剪的结果。换一份没裁过的原始 mscorlib,两边成员对不上,加载会失败。

插件里还有进一步删掉普通方法 IL、只留泛型方法的工具。有的工程拷贝时并不调用它。那时这些 .bytes 只经过 Unity 打临时 Player 时的那一次裁剪。

Generate 菜单每个按钮

这些按钮是给打主包准备的。热更不要逐个点。

菜单 解决什么
About、Documents/* 打开官网,不改工程
Installer... 把改过的 libil2cpp 装进工程。第一次使用和升级包之后要先装
Settings... 打开 HybridCLR Settings。热更程序集、link.xml 和 AOTGenericReferences.cs 的输出路径在这里
CompileDll/* 只把热更程序集编译到 HybridCLRData/HotUpdateDlls/{平台}/。不生成桥,不裁剪,不拷进资源目录
Generate/Il2CppDef 写版本头和程序集清单,告诉 IL2CPP 哪些程序集是热更程序集
Generate/LinkXml 扫描热更引用了哪些 AOT 类型,写成 link.xml,裁剪时保住它们
Generate/AOTDlls 打临时 Player,产出裁剪后的 AOT DLL。这是补充元数据的来源。不拷进资源目录
Generate/MethodBridgeAndReversePInvokeWrapper 生成 MethodBridge.cpp 并清 IL2CPP 缓存。解释器调主包、主包回调热更,都靠它。必须先有裁剪 DLL
Generate/AOTGenericReference 写出上面的注释清单
Generate/All 按顺序串起来:编译热更 DLL、程序集清单、link.xml、裁剪 AOT、桥、泛型清单

工程如果开了代码混淆,直接点插件的 Generate/All,会按没混淆的热更 DLL 生成 link.xml 和桥。整包应走混淆插件包过的那条生成,再把热更 DLL 拷进资源目录。这个动作常常没有单独菜单,而是挂在自己的构建步骤上。

工程自己加在 HybridCLR/CompileDll 下的「编译并拷进资源目录」,在 Android 上会按热更基线走。不要拿它当整包生成。

不走构建面板时怎么处理

平台先切到目标平台,例如 Android。

首次整包和更新整包是同一串。

  1. 走混淆后的生成:编译并混淆热更 DLL,生成 link.xml、泛型清单、桥,打临时 Player 裁出 AOT DLL,再把热更 DLL 拷进资源目录。
  2. 按手写名单,把裁剪 DLL 拷成资源目录里的补充元数据。
  3. 用 YooAsset 的 AssetBundle Builder 打资源。版本号要和后面发布用的资源版本一致。
  4. 把 AB 拷进导出工程的 StreamingAssets。这一步往往没有菜单,是构建脚本里的一次目录拷贝。
  5. 导出 Android 工程,再到 Android Studio 打 APK。
  6. 用自己的基线窗口校验最终 APK,并登记基线。

更新整包会把主包机器码、桥、补充元数据、热更 DLL 和资源全部重做。旧 APK 不能接着热更。登记的是一条新基线。

热更先在基线窗口里选中目标 APK,再点「编译并拷进资源目录」。然后打 AssetBundle,再把资源发到服务器。

热更包里变的是热更 DLL 和改过的资源。补充元数据会带在包里,字节还是这个主包的那一份。主包机器码和 APK 不变。

构建面板只是把这些步骤串进同一次基线会话。拆开点时,编译菜单结束就会关掉这次基线会话,后面的打 AB 和发布不再自动带着那次校验。

热更编译具体做了什么

Android 上那个「编译并拷进资源目录」会先进入热更基线,再编译。

  1. 把工作区当前的符号表备份走,换成这个 APK 基线里的那一份。清空 HybridCLRData/AssembliesPostIl2CppStrip/{平台}/,把基线里冻住的 aot/*.dll 拷进去。
  2. 用混淆插件编译现在的热更源码,并用这份旧符号表做混淆。产物先在混淆插件的输出目录。
  3. 把混淆后的热更 DLL 覆盖到资源目录里那份给运行时加载的字节文件。
  4. 用程序集解析器读这份新 DLL。每个类型、方法、字段引用只到刚拷回来的那些 AOT DLL 里解析。解析不到就失败:热更调用了这个 APK 里已经被裁掉、或者签名已经变了的成员。
  5. 重算 IL2CPP 的程序集清单、link.xml、桥和泛型清单。这一步不重新裁剪 AOT,也不打 Player。
  6. 新 MethodBridge.cpp 的 SHA256 和基线里那份比较。不一致就停,提示这份热更需要旧 APK 里没有的桥,要打新包。
  7. 把基线 supplemental/ 里的补充元数据再拷回资源目录。内容不变。

编译用的是当前热更源码。冻住的 AOT 用来锁定混淆名字,并在编译后核对引用。主包机器码没有重编。

失败看 Unity Console 里的异常,不用对文件,也不用看 git。

冻住的 AOT 不是 HybridCLR 自带的

HybridCLR 没有「冻住的 AOT」。它每次 Generate/AOTDlls 都按当前工程重新裁,放到 HybridCLRData/AssembliesPostIl2CppStrip/{平台}/,不记住上一个 APK。

冻住的 AOT 是工程自己在登记基线时,从那一次 APK 的裁剪结果里复制出来的 aot/*.dll。同一份基线还有 supplemental/、符号表和 generated/MethodBridge.cpp。热更时再把 aot/*.dll 拷回裁剪目录。当前 git 里的 AOT 源码即使改过,这次热更核对的仍是那个已发布 APK 里的 AOT。

这是工程自己的编辑器逻辑,不是 HybridCLR 包里的菜单。

桥失败,和用 git 看 AOT 不是一回事

以前看 git 提交有没有改 AOT 代码,管的是另一件事。

桥的哈希不看 AOT 源码有没有提交。它只看新热更 DLL 需要的桥,是不是和这个 APK 里已经编进去的那份相同。热更新增了一种主包没有的参数组合、泛型调用或反向调用,AOT 目录的 git 可以是干净的,这里仍会失败。桥编在主包的 libil2cpp 里,热更资源换不了它。

只改了 AOT 方法体、签名没变时,桥的哈希经常还是一样,这个检查不会拦。旧 APK 里跑的仍是旧方法体。所以 git 里 AOT 实现有没有改,仍然要看。

两件事各管一块:

检查 能发现 发现不了
git 里 AOT 源码变了 主包实现、签名、混淆名可能变了,应该打新整包 热更自己新增的调用形状。AOT 源码没动也会要新桥
MethodBridge.cpp 哈希和基线不一致 这份热更需要旧 APK 里没有的桥 只改方法体、桥文件碰巧相同的 AOT 改动
对照冻住的 AOT 解析热更引用 热更引用了这个 APK 裁剪结果里不存在的类型、方法、字段 方法体变了但签名还在

疑惑点

  • 补充元数据不是热更 DLL,也不是漏掉的机器码。它是裁剪后的 AOT DLL,里面是泛型定义的表和 IL。
  • AOTGenericReferences.RefMethods() 全是注释,不会自动做「多写泛型示例、编成机器码」这件事。
  • 社区版解释器在数值微基准上可以比机器码慢数倍到几十倍。热更函数自己始终解释;要让泛型方法体走机器码,得在 AOT 里写会活过裁剪的真实调用,再打完整包。引用类型经常已经能借用共享机器码。
  • 加载名单是手写的短名单。扫描出来的长名单不决定补充元数据目录。
  • 文件内容在完整包时自动生成。热更不重新裁。
  • 补充元数据和热更 DLL 是两个 AB。放在同一个收集目录,是为了同一套加载。
  • 同一个主包上,补充元数据内容不用热更。热更包里再带一份,字节不变。
  • SuperSet 不比 Consistent 跑得更快。选它,是为了 DLL 多几个成员时仍能加载。
  • 开了混淆时,插件菜单 Generate/All 不是整包该走的那条生成。
  • 热更用的是「编译并拷进资源目录」,不是 Generate 里的按钮。
  • 冻住的 AOT、基线哈希、按旧 AOT 解析热更引用,都是工程自己写的,不是 HybridCLR 自带。

和旧笔记的差别

旧笔记把「热更新用了一个 AOT 泛型组合」写成必须更新补充元数据。更准确的边界是:

  • 开放定义已经在现有补充元数据里,例如又写了一个 List<新的热更类>:不用重出补充元数据,解释器可以把 T 换成这个新类型。
  • 开放定义本身被裁掉了,或者调用形状需要一份旧主包里没有的桥:热更做不成,要打新的整包。
  • 同一个主包上,补充元数据的字节保持那一次裁剪的结果。

评论