环境隔离
部分 .NET 实现不支持多实例,也不支持热重启(如 CoreCLR)。因此,Z# 使用 .NET 内置的 AssemblyLoadContext (ALC)
实现环境隔离。
ALC 是 .NET 主推的实现环境隔离的方式。 一个 ALC 表示一个相对隔离的运行环境,每个 ALC 都会加载各自的程序集,多个 ALC 可以各自加载同一个程序集的不同版本而不会相互冲突; 在解析类型时,每个 ALC 都只会在自己的范围内查找,不会去其他 ALC 中查找类型; 对于同一个程序集,每个 ALC 有各自的静态字段,这意味着每个 ALC 有自己的独立全局环境。
Default ALC
这是 .NET 本身就存在的概念,这里只是简单介绍一下。 在 .NET 启动时,会自动创建一个 ALC,这就是 Default ALC。 如果没有显式指定,那么所有程序集都会加载到 Default ALC 中,所有代码也都是运行在 Default ALC 上的。 因此,即使你对 ALC 没有任何概念,也不影响你使用 .NET 进行开发。
Default ALC 并不像名字一样只是一个默认创建的 ALC 而已。实际上,Default ALC 是一个特殊的 ALC,它与其他 ALC 有一个关键的区别——
它作为一个 共享环境
存在。就是说,加载到 Default ALC 中的程序集就像被加载到了所有 ALC 中一样,其他 ALC 在解析类型时也会查看 Default ALC。
因此,.NET 的标准库和 Z# 的核心组件都会加载到 Default ALC,所有代码共享一份实例(当然也包括全局状态)。
Default ALC 无法卸载,这意味着所有加载到 Default ALC 中的代码都无法实现热重载。 这是开发者需要注意的,因为 Z# 的一些核心组件也在 Default ALC 中,并且开发者也可能需要将一些自己的组件加载到 Default ALC 中。 如果开发者修改了这部分代码,则需要完全重启引擎才能生效。
Master ALC
这是 Z# 为了实现热重载而引入的概念。 热重载对于一个脚本引擎而言非常重要,能显著提升开发效率,值得纳入架构设计的考量之中。
Master ALC 的定位和 Default ALC 几乎没什么不同,唯一的区别就是可以热重载。 如果没有热重载的需求,Z# 很可能不会引入 Master ALC。
Z# 将开发期会频繁变动的代码加载到 Master ALC 中,以支持热重载。这些代码主要包括引擎的胶水代码,项目的胶水代码以及项目的脚本代码。
Master ALC 拥有 共轭映射
和 ZCall
两个核心互操作功能,几乎所有与引擎的复杂交互都发生在 Master ALC 上。
此外,Master ALC 没有共享环境的特性,这可能是一个附带的好处,因为这里是应用层代码,绝大多数情况下我们不会希望其他 ALC 访问到。
生命周期
Master ALC 的首次创建稍晚于 Default ALC 的创建,但几乎可以认为是同时的。 一个关键的时序关系是,Master ALC 的创建早于 UObject 系统的初始化,因为只有这样,Master ALC 才能正确处理所有 UObject 的共轭映射。
有多种策略控制 Master ALC 的热重载。默认情况下,开关 PIE 会触发热重载。你也可以使用控制台命令 z#.reloadmasteralc
来显式触发热重载。
用户代码可以假定 Master ALC 在任意时刻都是存在的,因为 Master ALC 创建的时间非常早,几乎比所有用户代码都要早; 同时 Master ALC 热重载期间没有抛出任何事件,这意味着不会有用户代码插入到热重载的流程之中。
虽然有办法能在进程结束前强行卸载 Master ALC,但不建议这样做。 Master ALC 作为脚本引擎的核心,可能在任意时刻被引擎调用,保证它的全程可用是一件非常重要的事。 如果某一个时刻 Master ALC 不可用,那么引擎的调用将无法产生符合预期的结果。
Slim ALC
这是 Z# 为了方便开发者在虚幻引擎中执行一些相对独立的托管代码而引入的概念。 对于一些简单且独立的任务而言,Master ALC 有些过重了。 而且,把太多程序集加载进 Master ALC 也会污染主要环境。 为此,Z# 引入了一种用完即弃的 ALC 来执行简单代码。
一般通过控制台命令 z#.run
来在 Slim ALC 上执行代码。
这条命令会在主线程创建一个 Slim ALC,加载参数指定的程序集并调用它的入口方法。
当入口方法返回后,这个 Slim ALC 就会被卸载。
Z# 的构建工具就是运行在 Slim ALC 上的。