Unity 热更新方案之——ILRuntime

前言

做游戏离不开热更新,目前市面上热更新方案用的比较多的是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);
        }

    }
}


先学到这,持续更新中。。。

参考链接:
github仓库地址:https://github.com/Ourpalm/ILRuntime
中文文档:https://ourpalm.github.io/ILRuntime/public/v1/guide/index.html
使用ILRuntime遇到的一些问题
王王王渣渣ILRuntime系列

猜你喜欢

转载自blog.csdn.net/qq_41044598/article/details/126286235