前言
做游戏离不开热更新,目前市面上热更新方案用的比较多的是Lua(XLua,ToLua),最近又出现了基于C#的热更新 huatuo(已改名HybridCLR又叫wolong)。来不及学习了,以后用到了再去了解吧。
笔者入行做的第一个项目是利用ILRuntime进行热更新的,当时也是用的稀里糊涂的,一些坑点都是项目主程去解决的。这里做一个简单的回顾。
一、ILRuntime是什么?
1.官方简介
ILRuntime项目为基于C#的平台(例如Unity)提供了一个纯C#实现,快速、方便且可靠的IL运行时,使得能够在不支持JIT的硬件环境(如iOS)能够实现代码的热更新。
2.实现原理
ILRuntime借助Mono.Cecil库来读取DLL的PE信息,以及当中类型的所有信息,最终得到方法的IL汇编码,然后通过内置的IL解译执行虚拟机来执行DLL中的代码来实现热更新功能。
查看ILRuntime源码你会发现,内部有一个很大的switch/case结构,就是针对基本上每一条IL指令码进行解释,同时维护一个Stackframe用于模拟cpu的函数调用的基本操作进行辅助解释。
ILRuntime中解释热更dll中的自定义类实例,在框架层这边都是对应的同一个warper,即ILTypeInstance。
ILTypeInstance会知道最终被调用方法的il指令内容,如果调用,则就是switch逐句去解析这个方法的IL代码。
这样一来就没有什么执行权限的问题,简单理解为读取一个普通文件,然后解析文件内容。
如果是反射处理这种情形,那就是真实的构建出一个新的类型,然后调用新类型的方法,这倒是会涉及到内存权限问题。
二、ILRuntime使用
当时使用的时候只记得有以下限制:
1.不能手动挂载热更Mono脚本,只能通过代码AddComponent
2.不能使用非System.Action/Fun类型的委托,需要手动注册委托类型转换
3.需要将类的成员初始化赋值删除,改为在方法内初始化
4.不允许使用ref和out
5.将热更脚本放在特定文件夹。通过定义宏,在日常开发中在Assembly-CSharp编译调用,打包时将热更脚本单独打DLL。
当时每天都在赶UI,没有去细究为什么这么做。
由于项目已经挂掉了,这里就不去纠结了。下面记录一下自己学习ILRuntime的历程。
1.跨域委托
只在热更新的DLL项目中使用的委托,是不需要任何额外操作的,就跟在通常的C#里那样使用即可。
如果你需要将委托实例传给ILRuntime外部使用,那则根据情况,你需要额外添加适配器或者转换器。
示例:同一个参数组合的委托,只需要注册一次即可
Action,以及Func委托需要在主工程注册适配器
// 无返回值委托
appDomain.DelegateManager.RegisterMethodDelegate<int, float>();
// 带返回值委托
appDomain.DelegateManager.RegisterFunctionDelegate<int, float, bool>();
自定义委托需要额外添加转换器DelegateConvertor
// 自定义委托
delegate bool SomeFunction(int a, float b);
app.DelegateManager.RegisterDelegateConvertor<SomeFunction>((action) =>
{
return new SomeFunction((a, b) =>
{
return ((Func<int, float, bool>)action)(a, b);
});
});
官方建议:
尽量避免不必要的跨域委托调用。
尽量使用Action以及Func这两个系统内置万用委托类型。
2.跨域继承
如果你想在热更DLL项目当中继承一个Unity主工程里的类,或者实现一个主工程里的接口,你需要在Unity主工程中实现一个继承适配器。
为什么需要适配器?
1)防止热更层用到的框架层代码被裁减。
为什么会被裁减呢?因为Unity打包的时候真的不把这个热更dll看做dll,因为这个热更dll是脱离unity框架层的。自然在unity打包的时候,为了包体大小会把认为没有使用的代码全部过滤掉。这种情况下ILRuntime解释执行的时候,去反射调用框架层代码就会被视为错误,因为框架层不存在这些被调用的代码。
因为脱离了关系,那么如何在框架层中驱动的时候,可以同步驱动到热更层,这就成了一个问题。这就需要框架层引用热更层的相关instance去驱动 ,那么如何引用?这就是适配器的作用。适配器工作在框架层,其显式强调了需要引用驱动的类型实例,然后重写相关函数体内容,去实质调用 热更类型实例 的方法。具体参考MonoBehaviourAdapter即可理解。
ILRuntime提供了一个代码生成工具来自动生成跨域继承的适配器代码。
示例:
void OnHotFixLoaded()
{
Debug.Log("首先我们来创建热更里的类实例");
TestClassBase obj;
Debug.Log("现在我们来注册适配器, 该适配器由ILRuntime/Generate Cross Binding Adapter菜单命令自动生成");
appdomain.RegisterCrossBindingAdaptor(new TestClassBaseAdapter());
Debug.Log("现在再来尝试创建一个实例");
obj = appdomain.Instantiate<TestClassBase>("HotFix_Project.TestInheritance");
Debug.Log("现在来调用成员方法");
obj.TestAbstract(123);
obj.TestVirtual("Hello");
obj.Value = 233;
Debug.LogFormat("obj.Value={0}", obj.Value);
Debug.Log("现在换个方式创建实例");
obj = appdomain.Invoke("HotFix_Project.TestInheritance", "NewObject", null, null) as TestClassBase;
obj.TestAbstract(456);
obj.TestVirtual("Foobar");
obj.Value = 2333333;
Debug.LogFormat("obj.Value={0}", obj.Value);
}
3.CLR绑定与重定向
为什么需要绑定与重定向机制?
1)防止热更层用到的框架层代码被裁减。
2)加速热更代码的执行。
加速热更代码执行其实是ILRuntime解释每条il指令的时候,都会去现有缓存中查找当前指令是否为重定向函数,如果为重定向函数,则直接调用,如果不是重定向函数,则会反射调用。通过反射来调用接口调用效率会比直接调用低很多,反射传递函数参数时需要使用object[]数组,这样不可避免的每次调用都会产生不少GC Alloc。众所周知GC Alloc高意味着在Unity中执行会存在较大的性能问题。
ILRuntime提供了一个代码生成工具来自动生成CLR绑定代码。
生成代码示例:
namespace ILRuntime.Runtime.Generated
{
unsafe class HelloWorld_Binding
{
public static void Register(ILRuntime.Runtime.Enviorment.AppDomain app)
{
BindingFlags flag = BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly;
MethodBase method;
Type[] args;
Type type = typeof(global::HelloWorld);
args = new Type[]{};
method = type.GetMethod("TestHotfixInvokeMain", flag, null, args, null);
app.RegisterCLRMethodRedirection(method, TestHotfixInvokeMain_0);
args = new Type[]{};
method = type.GetConstructor(flag, null, args, null);
app.RegisterCLRMethodRedirection(method, Ctor_0);
}
static StackObject* TestHotfixInvokeMain_0(ILIntepreter __intp, StackObject* __esp, IList<object> __mStack, CLRMethod __method, bool isNewObj)
{
ILRuntime.Runtime.Enviorment.AppDomain __domain = __intp.AppDomain;
StackObject* ptr_of_this_method;
StackObject* __ret = ILIntepreter.Minus(__esp, 1);
ptr_of_this_method = ILIntepreter.Minus(__esp, 1);
global::HelloWorld instance_of_this_method = (global::HelloWorld)typeof(global::HelloWorld).CheckCLRTypes(StackObject.ToObject(ptr_of_this_method, __domain, __mStack), (CLR.Utils.Extensions.TypeFlags)0);
__intp.Free(ptr_of_this_method);
instance_of_this_method.TestHotfixInvokeMain();
return __ret;
}
static StackObject* Ctor_0(ILIntepreter __intp, StackObject* __esp, IList<object> __mStack, CLRMethod __method, bool isNewObj)
{
ILRuntime.Runtime.Enviorment.AppDomain __domain = __intp.AppDomain;
StackObject* __ret = ILIntepreter.Minus(__esp, 0);
var result_of_this_method = new global::HelloWorld();
return ILIntepreter.PushObject(__ret, __mStack, result_of_this_method);
}
}
}
先学到这,持续更新中。。。文章来源:https://www.toymoban.com/news/detail-404295.html
参考链接:
github仓库地址:https://github.com/Ourpalm/ILRuntime
中文文档:https://ourpalm.github.io/ILRuntime/public/v1/guide/index.html
使用ILRuntime遇到的一些问题
王王王渣渣ILRuntime系列文章来源地址https://www.toymoban.com/news/detail-404295.html
到了这里,关于Unity 热更新方案之——ILRuntime的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!