热更新

热更新是指在不需要重新编译打包游戏的情况下,在线更新游戏中的一些非核心代码和资源,比如活动运营和打补丁。热更新分为资源热更新代码热更新两种,代码热更新实际上也是把代码当成资源的一种热更新,但通常所说的热更新一般是指代码热更新。

  • 资源热更新主要通过AssetBundle来实现,在Unity编辑器内为游戏中所用到的资源指定AB包的名称和后缀,然后进行打包并上传服务器,待游戏运行时动态加载服务器上的AB资源包。
  • 代码热更新主要包括Lua热更新、C#直接反射热更新ILRuntime热更新等。

一、Lua热更新

Lua热更原理:

Lua热更新解决方案是通过一个Lua热更新插件(如ULua、ToLua、XLua等)来提供一个Lua的运行环境以及和C#进行交互,这样可以使用C#和Lua共同开发。由于Lua不需要编译,Lua代码可以直接在Lua虚拟机里运行,因此几乎可以在任何操作系统和平台上运行,那么可以将项目底层代码使用C#进行编写,而游戏逻辑等需要热更新的部分使用Lua编写,版本更新时更新迭代Lua代码即可,从而保证游戏正常运营。

XLua:

而热更新插件中,性能最好的当属XLua对应的热更新解决方案,并且稳定性和可持续性较强。XLua就是为Unity、.Net、Mono等C#环境提供一个Lua虚拟机,使这些环境里也可以运行Lua代码,从而为它们增加Lua脚本编程的能力。借助xLua,这些Lua代码就可以方便的和C#相互调用。 可以形容XLua为轻量化的胶水,打通了Lua和C#的框架

XLua的另一特色:热补丁(注入[Hotfix]标记)
热补丁原理:

xLua进行IL注入时会为打上[Hotfix]标签的类的所有函数创建一个变量a,同时添加对应的判断条件。如果Lua脚本中添加了对应的热更新函数,变量a就不为空,并将变量a中的方法指向xlua.hotfix中的具体function。这时由于变量a不为空,所以C#中的函数就会执行Lua脚本中对应的热更新函数逻辑。但如果没有定义对应的热更新函数,或对应的热更新函数为nil,变量a就为空,则C#中的函数依然执行原有的函数逻辑。就是在运行时用Lua函数替换对应的C#函数。使用Lua语言修复c#bug

其他标记:

与xLua热更新相关的标签还包括:[LuaCallCSharp]、[ReflectionUse]和[CSharpCallLua],这三个标签都需要生成适配代码,但不需要IL注入。
比如:[LuaCallCSharp]标签表示如果一个C#类型添加了该标签,xLua会生成这个类型的适配代码,否则将会尝试用性能较低的反射方式来访问。比如,Lua脚本中想调用某个C#函数,就可以在该C#函数上添加[LuaCallCSharp]标签,这时Lua就会去寻找该函数的适配代码,然后进行调用。


二、C# 直接反射热更新(IOS 禁止)

热更原理:

编译成的DLL打成资源文件, 通过C#反射加载程序集的方式可以动态的从资源包里加载脚本到工程中。
由于Android支持JIT即时编译(动态编译)的模式,即可以边运行边编译,支持在运行时动态生成代码和类型。比如,C#中的虚函数和反射都是在程序运行时才确定对应的重载方法和类。因此,Android平台可以不借助任何第三方热更新方案,直接使用C#反射执行DLL文件。实际开发时通过System.Reflection.Assembly类加载程序集DLL文件,然后再利用System.Type类获取程序集中某个类的信息,还可以通过Activator类来动态创建实例对象。

IOS禁止原因:

而IOS平台采用AOT预先编译(静态编译)的模式,不支持JIT编译模式,即程序运行前就将代码编译成机器码存储在本地,然后运行时直接执行即可,因此AOT不能在运行时动态生成代码和类型。
目前Mono在IOS平台上采用Full AOT模式运行,如果想要使用C#反射执行DLL文件,就会触发Mono的JIT编译器,而Full AOT模式下又不允许JIT,于是Mono就会报错。因此,IOS平台上不允许直接使用C#反射执行DLL文件来实现热更新。


三、ILRuntime热更新

ILRuntime项目为基于C#的平台(例如Unity)提供了一个纯C#实现,快速、方便且可靠的IL运行时,使得能够在不支持JIT的硬件环境(如iOS)能够实现代码的热更新。

热更原理:

官方解释:ILRuntime借助Mono.Cecil库来读取DLL的PE信息,以及当中类型的所有信息,最终得到方法的IL汇编码,然后通过内置的IL解译执行虚拟机来执行DLL中的代码。

直译解释:就是先用VS把需要热更新的C#代码封装成DLL(动态链接库)文件,然后通过Mono.Cecil库读取DLL信息并得到对应的IL中间代码,最后再用内置的IL解译执行虚拟机来执行DLL文件中的IL代码。

IL中间语言

IL是.NET框架中 中间语言(Intermediate Language)的缩写。使用.NET框架提供的编译器可以直接将源程序编译为.exe或.dll文件,但此时编译出来的程序代码并不是CPU能直接执行的机器代码,而是一种中间语言IL的代码。 使用中间语言的优点有两点,一是可以实现平台无关性,既与特定CPU无关;二是只要把.NET框架某种语言编译成IL代码,就实现.NET框架中语言之间的交互操作。

IL中间语言具体应用

像C#这样遵循CLI规范的高级语言,被先被各自的编译器编译成中间语言:IL(CIL),等到需要真正执行的时候,这些IL会被加载到运行时库,也就是VM中,由VM动态的编译成汇编代码(JIT)然后在执行。

ILRuntime项目:

在使用ILRuntime项目时,由于ILRuntime项目是使用C#来完成热更新,因此很多时候会用到反射来实现某些功能。但Unity的主工程的数据类型和热更工程的数据类型是不直接互通的。要想实现互通的话,就需要实现一些跨域功能

跨域功能:

  • 跨域委托:需要额外添加适配器或者转换器。例如,主工程中有一些方法,在热更工程中通过一个代理去调用主工程的方法,需要实现跨域委托。
  • 跨域继承:如果想在热更工程里继承主工程的类/接口,则需要在Unity主工程中实现一个继承适配器并注册。这样热更工程就可以调用继承适配器进行继承。
  • 反射转换:热更工程中的数据类型和C#数据类型是不通用的,所以需要类型映射后使用。
  • CLR重定向:ILRuntime之所以能达到高性能,是因为它会劫持热更工程中可能出现GC的方法调用,它可以进行重定向。我们就有机会将可能产生GC的功能替换成我们自己的实现,从而实现高效能。
  • CLR绑定:借助了ILRuntime的CLR重定向机制来实现,因为实质上也是将对CLR方法的反射调用重定向到我们自己定义的方法里面来。

本文章使用limfx的vscode插件快速发布