草庐IT

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

作孽就得先起床 2023-10-10 原文

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();
           
        }

如何指定参数类型:

先指定参数类型,再组件参数集合<ILRuntime.CLR.TypeSystem.IType>,然后将类型添加进集合中。

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

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

        Debug.Log("指定参数类型来获得IMethod");
        IType intType = appdomain.GetType(typeof(int));
        //参数类型列表
        List<IType> paramList = new List<ILRuntime.CLR.TypeSystem.IType>();
        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<IType> paramList = new List<ILRuntime.CLR.TypeSystem.IType>();
        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<int> lst = ctx.ReadObject<List<int>>(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 ILRuntime热更新基础入门(ILRuntime手记一)的更多相关文章

  1. ruby-on-rails - 如何验证 update_all 是否实际在 Rails 中更新 - 2

    给定这段代码defcreate@upgrades=User.update_all(["role=?","upgraded"],:id=>params[:upgrade])redirect_toadmin_upgrades_path,:notice=>"Successfullyupgradeduser."end我如何在该操作中实际验证它们是否已保存或未重定向到适当的页面和消息? 最佳答案 在Rails3中,update_all不返回任何有意义的信息,除了已更新的记录数(这可能取决于您的DBMS是否返回该信息)。http://ar.ru

  2. ruby-on-rails - 使用 rails 4 设计而不更新用户 - 2

    我将应用程序升级到Rails4,一切正常。我可以登录并转到我的编辑页面。也更新了观点。使用标准View时,用户会更新。但是当我添加例如字段:name时,它​​不会在表单中更新。使用devise3.1.1和gem'protected_attributes'我需要在设备或数据库上运行某种更新命令吗?我也搜索过这个地方,找到了许多不同的解决方案,但没有一个会更新我的用户字段。我没有添加任何自定义字段。 最佳答案 如果您想允许额外的参数,您可以在ApplicationController中使用beforefilter,因为Rails4将参数

  3. postman接口测试工具-基础使用教程 - 2

    1.postman介绍Postman一款非常流行的API调试工具。其实,开发人员用的更多。因为测试人员做接口测试会有更多选择,例如Jmeter、soapUI等。不过,对于开发过程中去调试接口,Postman确实足够的简单方便,而且功能强大。2.下载安装官网地址:https://www.postman.com/下载完成后双击安装吧,安装过程极其简单,无需任何操作3.使用教程这里以百度为例,工具使用简单,填写URL地址即可发送请求,在下方查看响应结果和响应状态码常用方法都有支持请求方法:getpostputdeleteGet、Post、Put与Delete的作用get:请求方法一般是用于数据查询,

  4. 软件测试基础 - 2

    Ⅰ软件测试基础一、软件测试基础理论1、软件测试的必要性所有的产品或者服务上线都需要测试2、测试的发展过程3、什么是软件测试找bug,发现缺陷4、测试的定义使用人工或自动的手段来运行或者测试某个系统的过程。目的在于检测它是否满足规定的需求。弄清预期结果和实际结果的差别。5、测试的目的以最小的人力、物力和时间找出软件中潜在的错误和缺陷6、测试的原则28原则:20%的主要功能要重点测(eg:支付宝的支付功能,其他功能都是次要的)80%的错误存在于20%的代码中7、测试标准8、测试的基本要求功能测试性能测试安全性测试兼容性测试易用性测试外观界面测试可靠性测试二、质量模型衡量一个优秀软件的维度①功能性功

  5. LC滤波器设计学习笔记(一)滤波电路入门 - 2

    目录前言滤波电路科普主要分类实际情况单位的概念常用评价参数函数型滤波器简单分析滤波电路构成低通滤波器RC低通滤波器RL低通滤波器高通滤波器RC高通滤波器RL高通滤波器部分摘自《LC滤波器设计与制作》,侵权删。前言最近需要学习放大电路和滤波电路,但是由于只在之前做音乐频谱分析仪的时候简单了解过一点点运放,所以也是相当从零开始学习了。滤波电路科普主要分类滤波器:主要是从不同频率的成分中提取出特定频率的信号。有源滤波器:由RC元件与运算放大器组成的滤波器。可滤除某一次或多次谐波,最普通易于采用的无源滤波器结构是将电感与电容串联,可对主要次谐波(3、5、7)构成低阻抗旁路。无源滤波器:无源滤波器,又称

  6. 微信小程序开发入门与实战(Behaviors使用) - 2

    @作者:SYFStrive @博客首页:HomePage📜:微信小程序📌:个人社区(欢迎大佬们加入)👉:社区链接🔗📌:觉得文章不错可以点点关注👉:专栏连接🔗💃:感谢支持,学累了可以先看小段由小胖给大家带来的街舞👉微信小程序(🔥)目录自定义组件-behaviors    1、什么是behaviors    2、behaviors的工作方式    3、创建behavior    4、导入并使用behavior    5、behavior中所有可用的节点    6、同名字段的覆盖和组合规则总结最后自定义组件-behaviors    1、什么是behaviorsbehaviors是小程序中,用于实现

  7. 【Java入门】使用Java实现文件夹的遍历 - 2

    遍历文件夹我们通常是使用递归进行操作,这种方式比较简单,也比较容易理解。本文为大家介绍另一种不使用递归的方式,由于没有使用递归,只用到了循环和集合,所以效率更高一些!一、使用递归遍历文件夹整体思路1、使用File封装初始目录,2、打印这个目录3、获取这个目录下所有的子文件和子目录的数组。4、遍历这个数组,取出每个File对象4-1、如果File是否是一个文件,打印4-2、否则就是一个目录,递归调用代码实现publicclassSearchFile{publicstaticvoidmain(String[]args){//初始目录Filedir=newFile("d:/Dev");Datebeg

  8. ES基础入门 - 2

    ES一、简介1、ElasticStackES技术栈:ElasticSearch:存数据+搜索;QL;Kibana:Web可视化平台,分析。LogStash:日志收集,Log4j:产生日志;log.info(xxx)。。。。使用场景:metrics:指标监控…2、基本概念Index(索引)动词:保存(插入)名词:类似MySQL数据库,给数据Type(类型)已废弃,以前类似MySQL的表现在用索引对数据分类Document(文档)真正要保存的一个JSON数据{name:"tcx"}二、入门实战{"name":"DESKTOP-1TSVGKG","cluster_name":"elasticsear

  9. objective-c - 在设置 Cocoa Pods 和安装 Ruby 更新时出错 - 2

    我正在尝试为我的iOS应用程序设置cocoapods但是当我执行命令时:sudogemupdate--system我收到错误消息:当前已安装最新版本。中止。当我进入cocoapods的下一步时:sudogeminstallcocoapods我在MacOS10.8.5上遇到错误:ERROR:Errorinstallingcocoapods:cocoapods-trunkrequiresRubyversion>=2.0.0.我在MacOS10.9.4上尝试了同样的操作,但出现错误:ERROR:Couldnotfindavalidgem'cocoapods'(>=0),hereiswhy:U

  10. ruby-on-rails - Rails Associations 的更新方法是什么? - 2

    这太简单了,太荒谬了,我在任何地方都找不到关于它的任何信息,包括API文档和Rails源代码:我有一个:belongs_to关联,我开始理解当您没有关联时您在Controller中调用的正常模型方法与您有关联时调用的方法略有不同。例如,我的关联在创建Controller操作时运行良好:@user=current_user@building=Building.new(params[:building])respond_todo|format|if@user.buildings.create(params[:building])#etcetera但我找不到关于更新如何工作的文档:@user

随机推荐