Unity ILRuntime热更新基础入门(ILRuntime手记一)

2023.3.20简介

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

ILRuntime的优势

同市面上的其他热更方案相比,ILRuntime主要有以下优点:

  • 无缝访问C#工程的现成代码,无需额外抽象脚本API

  • 直接使用VS2015进行开发,ILRuntime的解译引擎支持.Net 4.6编译的DLL

  • 执行效率是L#的10-20倍

  • 选择性的CLR绑定使跨域调用更快速,绑定后跨域调用的性能能达到slua的2倍左右(从脚本调用GameObject之类的接口)

  • 支持跨域继承

  • 完整的泛型支持

  • 拥有Visual Studio的调试插件,可以实现真机源码级调试。支持Visual Studio 2015 Update3、Visual Studio 2017、Visual Studio 2019和Visual Studio 2022

  • 支持VS Code源码级调试,支持Mac OSX

  • 最新的2.0版引入的寄存器模式将数学运算性能进行了大幅优化

C# vs Lua

目前市面上主流的热更方案,主要分为Lua的实现和用C#的实现,两种实现方式各有各的优缺点。

Lua是一个已经非常成熟的解决方案,但是对于Unity项目而言,也有非常明显的缺点。就是如果使用Lua来进行逻辑开发,就势必要求团队当中的人员需要同时对Lua和C#都特别熟悉,或者将团队中的人员分成C#小组和Lua小组。不管哪一种方案,对于中小型团队都是非常痛苦的一件事情。

用C#来作为热更语言最大的优势就是项目可以用同一个语言来进行开发,对Unity项目而言,这种方式肯定是开发效率最高的。

Lua的优势在于解决方案足够成熟,之前的C++团队可能比起C#,更加习惯使用Lua来进行逻辑开发。此外借助luajit,在某些情况下的执行效率会非常不错,但是luajit现在维护情况也不容乐观,官方还是推荐使用公版Lua来开发。

如果需要测试ILRuntime对比Lua的性能Benchmark,需要确认以下几点:

  • ILRuntime加载的dll文件是Release模式编译的

  • dll中对外部API的调用都进行了CLR绑定

  • 确保没有勾选Development Build的情况下发布成正式真机运行包,而不是在Editor中直接运行

  • 可以直接使用Demo工程中提供的性能测试进行对比

ILRuntime设计上为了在开发时提供更多的调试支持,在Unity Editor中运行会有很多额外的性能开销, 因此在Unity Editor中直接测试并不能代表ILRuntime的实际运行性能。

最新2.0版本的ILRuntime,加入了寄存器模式,在10多项测试用例当中的性能,均已超过lua53版xlua,详细测试代码可参见ILRuntime的U3D Demo工程以及视频教程

建议版本控制大于:2021.3.2 教程来源Unity

安装:

在 unity 中的: manifest.json下的第一行粘贴官方提供的地址

"scopedRegistries": [
  {
    "name": "ILRuntime",
    "url": "https://registry.npmjs.org",
    "scopes": [
      "com.ourpalm"
    ]
  }
],

接着回到unity,到window->PacketageManager中找到ILRuntime

安装即可

接下来是设置:排除报错

开启:不安全代码选项:Allow 'unsafe' Code

安装完成!(以下步骤为官方描述)


然后通过Unity的Window->Package Manager菜单,打开Package Manager,将上部标签页选项选择为All Packages,Advanced里勾上Show Preview Packages,等待Unity加载完包信息,应该就能在左侧列表中找到ILRuntime,点击安装即可

部分Unity版本可以无法直接在列表中刷出ILRuntime,如果左边列表找不着,那就在项目的manifest.json中的dependencies段的开头,增加如下代码手动将ILRuntime添加进项目

"com.ourpalm.ilruntime": "1.6.0",

ILRuntime包安装完毕后,在Package Manager中选中ILRuntime, 右边详细页面中有Samples,点击右方的Import to project可以将ILRuntime的示例Demo直接导入当前工程。

示例导入工程后有可能因为没开启unsafe导致编译报错,可以在PlayerSettings中勾选Allow unsafe code解决编译问题。


Demo测试部署:

打开Demo文件夹:Demo->HotFix_Project~->HotFix_Project.sln,使用vs在侧栏右击选择生成即可完成自动部署

然后选择Demo中的第一个场景,有输出即为测试成功!

ILRuntime开始:

ILRuntime利用基本构成简单介绍,通过c#.Net建立dll文件,将他隐藏在unity工程文件中,通过www/UnityWebRequest进行流加载到unityMon中进行调用完成热更新。

基于c#断点特性实现断点方式.

通过建立AppDomain调取ILRuntime,利用携程方式进行加载读取内容。

读取完后在进程中完成进程ID属性对齐,告诉ILRuntime主线程的线程ID才能正确将函数运行耗时报告给Profiler,完成正式运行

接着在方法中进行正常调用即可。这样就完成了初始化,在unity调取到了热更文件。

[unity文件夹隐藏方式:("名称"+"~")完成隐藏]

其他调取方式:

在unity中调取c#热更文件的方法

注意:IType type = appdomain.LoadedTypes["HotFix_Project.InstanceClass"];是可以复用的,为了分开书写,便于阅读故多次出现。具体可结合视频以及Demo

基础方法:

//类名 方法名 null null (进行调用)

第一个 null ,如果对象为静态static方法的话为null

如果不是则传:对象

第二个 null 传递的是这个方法的参数。如果存在多个参数,直接在后面添加","直接添加参数即可

示例如下:

        Debug.Log("调用无参数静态方法");
        //调用无参数静态方法,appdomain.Invoke("类名", "方法名", 对象引用, 参数列表);
        appdomain.Invoke("HotFix_Project.InstanceClass", "StaticFunTest", null, null);
        //调用带参数的静态方法
        Debug.Log("调用带参数的静态方法");
        appdomain.Invoke("HotFix_Project.InstanceClass", "StaticFunTest2", null, 123);

存在的问题:

性能比较差,每次需要通过字符串寻找这个类的对象,寻找方法

比基础方法高效一点的:

通过ILRuntime的接口拿取方法。

通过appdomain应用程序域拿取,在LoadedTypes中保存了所有的已经加载ILRuntime的类型

再通过type.GetMethod反射拿到对应的方法 ,这里存在几个方式:

1、存在的方式的数量,该参数方式的数量

2、如果存在的数量一样,但类型不一致。则通过重载传递一个list,里面则包含具体参数的类型,还有泛型、返回值都可进行指定

         Debug.Log("通过IMethod调用方法");
        //预先获得IMethod,可以减低每次调用查找方法耗用的时间
        IType type = appdomain.LoadedTypes["HotFix_Project.InstanceClass"];
        //根据方法名称和参数个数获取方法
        IMethod method = type.GetMethod("StaticFunTest2", 1);
        appdomain.Invoke(method, null, 123);

问题:

appdomain.Invoke(method, null, 123);在这里可能会产生了频繁的装箱拆箱,导致出现GC垃圾

解决上方无GC的方案:

通过ILRuntime中的BeginInvoke进行参数传递.

通过栈来进行数值的一次性传递,完成无GC消耗

如果要传递成员对象还需要先将自己这个对象压栈进入(示例中的: ctx),再通过Invoke调用

        //先获取与上方一致
        IType type = appdomain.LoadedTypes["HotFix_Project.InstanceClass"];
        //根据方法名称和参数个数获取方法
        IMethod method = type.GetMethod("StaticFunTest2", 1);
        //通过无GC Alloc方式调用方法
        using (var ctx = appdomain.BeginInvoke(method))
        {
            //将参数在此一个个压栈
            ctx.PushInteger(123);
            //.....
             //Invoke调用触发
            ctx.Invoke();
           
        }

如何指定参数类型:

先指定参数类型,再组件参数集合,然后将类型添加进集合中。

IType是ILRuntime中的,因此想要获取须通过appdomain.GetType(typeof(int))进行。

如果具有相同方法但具有不同参数类型可通过此方法进行。

        Debug.Log("指定参数类型来获得IMethod");
        IType intType = appdomain.GetType(typeof(int));
        //参数类型列表
        List paramList = new List();
        paramList.Add(intType);
        //根据方法名称和参数类型列表获取方法
        method = type.GetMethod("StaticFunTest2", paramList, null);
        appdomain.Invoke(method, null, 456);

如何实例化热更里的类:

1、第一种方式

appdomain.Instantiate(“类名”,构造函数)

        Debug.Log("实例化热更里的类");
        object obj = appdomain.Instantiate("HotFix_Project.InstanceClass", new object[] { 233 });

2、第二种方式

需要先获取type ,再进行一次转换才能使用

Instantiate();也可指定参数,无参直接使用即可

        //第二种方式
        IType type = appdomain.LoadedTypes["HotFix_Project.InstanceClass"];
        object obj2 = ((ILType)type).Instantiate();

如何调用成员方法:

IType type = appdomain.LoadedTypes["HotFix_Project.InstanceClass"];
Debug.Log("调用成员方法");
method = type.GetMethod("get_ID", 0);//获取成员属性变量(名称)
 using (var ctx = appdomain.BeginInvoke(method))//采用无GC方法调用
 {
    ctx.PushObject(obj);//先将对象压栈
    ctx.Invoke();//再调用触发
    int id = ctx.ReadInteger();//读取返回值
    Debug.Log("!! HotFix_Project.InstanceClass.ID = " + id);
  }
​

如果方法为泛型方法:

调用:

        Debug.Log("调用泛型方法");
        IType stringType = appdomain.GetType(typeof(string));//指定参数类型
        IType[] genericArguments = new IType[] { stringType };//先生成参数列表
        appdomain.InvokeGenericMethod("HotFix_Project.InstanceClass", "GenericMethod",                genericArguments, null, "TestString");//调用

获取:

        Debug.Log("获取泛型方法的IMethod");
         //参数类型列表
        List paramList = new List();
        paramList.Clear();
        paramList.Add(intType);
        genericArguments = new IType[] { intType };
        method = type.GetMethod("GenericMethod", paramList, genericArguments);
        appdomain.Invoke(method, null, 33333);

热更方法带out,ref的方法

需要进行处理 这里建议直接看视频

       Debug.Log("调用带Ref/Out参数的方法");
       object obj = appdomain.Instantiate("HotFix_Project.InstanceClass", new object[] { 233 });
        method = type.GetMethod("RefOutMethod", 3);
        int initialVal = 500;
        using(var ctx = appdomain.BeginInvoke(method))
        {
            //第一个ref/out参数初始值
            ctx.PushObject(null);
            //第二个ref/out参数初始值
            ctx.PushInteger(initialVal);
            //为何:传递默认的初始值
            
            //压入this  对象开始调用
            ctx.PushObject(obj);
            //压入参数1:addition
            ctx.PushInteger(100);
            //压入参数2: lst,由于是ref/out,需要压引用,这里是引用0号位,也就是第一个PushObject的位置
            ctx.PushReference(0);
            //压入参数3,val,同ref/out
            ctx.PushReference(1);
            ctx.Invoke();//调用
            //读取0号位的值
            List lst = ctx.ReadObject>(0);//读取被热更方法改变的值
            initialVal = ctx.ReadInteger(1);
           //完成调用
            Debug.Log(string.Format("lst[0]={0}, initialVal={1}", lst[0], initialVal));
        }

热更调用主工程:

1、设置引用类库在热更工程文件中设置

(1)引用unity的CSharp

位置:unity项目->library->ScriptAssemblies

(2)引用UnityEngine.CoreModule

(3)引用UnityEngine.UIModule

位置:unity安装目录->Editor->Data\Managed->UnityEngine

然后与在unity中一样直接进行使用即可

默认可能会拷贝一些不需要的文件:

选中引入的类库下面的“引用属性”复制本地改为false即可,再进行生成

也可通过修改路径直接完成dll文件替换,参考百度或视频

你可能感兴趣的:(unity,开发语言,c#,热更新)