ILRuntime项目为基于C#的平台(例如Unity)提供了一个纯C#实现
,快速
、方便
且可靠
的IL运行时,使得能够在不支持JIT的硬件环境(如iOS)能够实现代码的热更新
同市面上的其他热更方案相比,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版引入的寄存器模式将数学运算性能进行了大幅优化
目前市面上主流的热更方案,主要分为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利用基本构成简单介绍,通过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)); //参数类型列表 ListparamList = 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"); //参数类型列表 ListparamList = 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号位的值 Listlst = 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文件替换,参考百度或视频