前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >ILRuntime热更新

ILRuntime热更新

作者头像
[Sugar]
发布2022-09-21 11:19:06
2.3K0
发布2022-09-21 11:19:06
举报
文章被收录于专栏:U3D技术分享

  • IL热更优点: 1、无缝访问C#工程的现成代码,无需额外抽象脚本API 2、直接使用VS2015进行开发,ILRuntime的解译引擎支持.Net 4.6编译的DLL 3、执行效率是L#的10-20倍| 4、选择性的CLR绑定使跨域调用更快速,绑定后跨域调用的性能能达到slua的2倍左右(从脚本调用GameObject之类的接口) 5、支持跨域继承 6、完整的泛型支持 7、拥有Visual Studio的调试插件,可以实现真机源码级调试。支持Visual Studio 2015 Update3 以及Visual Studio 2017和Visual Studio 2019 8、最新的2.0版引入的寄存器模式将数学运算性能进行了大幅优化
  • 工程使用U3D核心模板,版本号:2020.3.27f1(Personal)
  • ILRuntime版本:2.1.0-preview
  • 2020/7/29补充:ILRuntime框架允许直接使用Unity的组件,具体可以直接在B站直接搜索ILRuntime,找到视频时长为2小时左右的那个教程(B站基本上都是那一个教程),那个教程有详细框架搭建和指引,算是目前免费的里面比较好的教程视频了。在ILRuntime热更框架中你可以直接使用Unity组件,直接用Find等方式找到GameObject然后编写代码逻辑。付费课程我看Siki学院有个全套的,但太贵了我就没买(<-暗示)

我建议ILRuntime的官方手册作者罚抄《CLR via C#》100遍,看看人家怎么写教程的。

目录

前置知识

  • CLR:Common Language Runtime,可由多重编程语言使用的“运行时(即Runtime)”。
  • lib,dll,pdb文件:传送门。dll动态链接库,pdb程序数据二进制文件,pdb文件保存着调试和项
  • 目状态信息,主要作用是调试。
  • ref、out:传送门
  • GC(generational garbage collector):基于代的垃圾回收器
  • appdomain:CLR COM服务器初始化时会创建一个AppDomain。AppDomain是一组程序集的逻
  • 辑容器。CLR初始化时创建的第一个AppDomain称为“默认AppDomain”,这个默认的AppDomain
  • 只有在Windows进程终止时才会被销毁。除了默认 AppDomain,正在使用非托管COM接口方法
  • 或托管类型方法的宿主还可要求CLR创建额外的 AppDomain。AppDomain是为了提供隔离而设计
  • 的。
  • 反射:编译时对一个类型一无所知的情况下,如何在运行时发现类型的信息、创建类型的实例以及
  • 访问类型的成员。反射造成编译时无法保证类型安全性。同时反射有较大的开销。What’s GC.Alloc?
  • GC.Alloc means that during the run time your code (or something in the API) allocates this
  • much of the managed memory.
  • This can cause problems later (that’s why it has the row in the profiler), because when the
  • Garbage Collector runs, it tends to slow down or even hang your game.
  • ILRuntime原理图表
image 100 - ILRuntime热更新
image 100 - ILRuntime热更新

手册部分

环境

  • Windows10
  • Unity:2020.3.27f1(Personal)
  • ILRuntime版本:2.1.0-preview
  • IDE:Visual Studio 2022
  • JDK:Java SE 8(8u202 and earlier)
  • SDK Tools:24.4.1
  • 模拟器:雷电模拟器9.0.5

基础

原理

  • ILRuntime借助Mono.Cecil库来读取DLL的PE信息,以及当中类型的所有信息,最终得到方法的IL汇编码,然后通过内置的IL解译执行虚拟机来执行DLL中的代码。 ILRuntime目标是读取热更新的dll,编译成IL,然后使用自己的JIT Compiler来执行热更dll中的代码,达到热更的目的。
  • 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
  • 最新的2.0版引入的寄存器模式将数学运算性能进行了大幅优化

C# vs Lua

  • 用Lua热更需要对Lua和C#都比较熟悉,Lua优势在于足够成熟。
  • C#直接使用更加便捷

教程

从零开始

  • 在Packages/manifest.json中,添加ILRuntime的源信息:
image 85 - ILRuntime热更新
image 85 - ILRuntime热更新
代码语言:javascript
复制
"scopedRegistries": [
  {
    "name": "ILRuntime",
    "url": "https://registry.npmjs.org",
    "scopes": [
      "com.ourpalm"
    ]
  }
],
  • Unity编辑器弹出:
image 86 - ILRuntime热更新
image 86 - ILRuntime热更新
  • Window->Package Manager菜单中找到ILRuntime如下图所示,然后点击右下角安装
image 87 1024x384 - ILRuntime热更新
image 87 1024x384 - ILRuntime热更新
  • 点击如下两个位置导入sample
image 88 - ILRuntime热更新
image 88 - ILRuntime热更新
  • 报错:Unsafe code may only appear if compiling with /unsafe. Enable “Allow ‘unsafe’ code” in Player Settings to fix this error.
  • 解决方法:允许编译Unsafe code。在build settings里选player settings选palyer里面有一项Allow Unsafe Code改为选中状态。
image 89 - ILRuntime热更新
image 89 - ILRuntime热更新
  • 找到下图文件夹并打开导入sln,并进行release编译。(ILRuntime加载的dll文件是Release模式编译的)
image 90 - ILRuntime热更新
image 90 - ILRuntime热更新
  • 报错:必须添加对程序集“netstandard, Version=2.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51”的引用
  • 解决方案:我的默认工程文件框架为4.6.0,更改到更高的版本再进行编译即可生成dll
image 91 1024x516 - ILRuntime热更新
image 91 1024x516 - ILRuntime热更新
  • 顺利生成dll文件
yh4EZQuhvl 1024x739 - ILRuntime热更新
yh4EZQuhvl 1024x739 - ILRuntime热更新

安装调试器

image 94 - ILRuntime热更新
image 94 - ILRuntime热更新
  • 报错:安装无法将此扩展安装到所有选定的产品
  • 尝试解决问题-未允许扩展开发:打开log日志,其中有一个log状态“找到安装程序实例 4e5d7448,但其处于不可启用状态。”,可能原因为在visual stdio中并未安装扩展开发组件,需要在installer中安装。
image 96 1024x576 - ILRuntime热更新
image 96 1024x576 - ILRuntime热更新
  • 安装完成后仍报错,解决方案2,直接查看vsix中的签署文件extension.vsixmanifest。发现安装版本前置条件设置为12.0版本,故直接更改到我目前的vs大版本17.0。
  • 再次点击,顺利完成安装。
image 97 - ILRuntime热更新
image 97 - ILRuntime热更新
image 98 1024x524 - ILRuntime热更新
image 98 1024x524 - ILRuntime热更新

寄存器模式

(对应Examples 03)

  • 委托适配器(DelegateAdapter):将委托实例传出给ILRuntime外部使用,将其转换成CLR委托实例。
  • 由于IL2CPP之类的AOT编译技术无法在运行时生成新的类型,所以在创建委托实例的时候ILRuntime选择了显式注册的方式,以保证问题不被隐藏到上线后才发现。
代码语言:javascript
复制
//同一参数组合只需要注册一次
delegate void SomeDelegate(int a, float b);
Action<int, float> act;
//注册,不带返回值,最多支持五个参数传入
appDomain.DelegateManager.RegisterMethodDelegate<int, float>();

//注册,带参数返回值,最后一个参数为返回值,最多支持四个参数传入
delegate bool SomeFunction(int a, float b);
Func<int, float, bool> act;
  • 委托转换器:需要将一个不是Action或者Func类型的委托实例传到ILRuntime外部使用,需要写委托适配器和委托转换器。委托转换器将Action和Func转换成你真正需要的那个委托类型
代码语言:javascript
复制
app.DelegateManager.RegisterDelegateConvertor<SomeFunction>((action) =>
{
    return new SomeFunction((a, b) =>
    {
       return ((Func<int, float, bool>)action)(a, b);
    });
});
  • 为了避免不必要的麻烦,以及后期热更出现问题,建议: 1、尽量避免不必要的跨域委托调用 2、尽量使用Action以及Func委托类型

跨域继承

  • 建议直接看demo中的Inheritance部分。手册中的adpter可以在Unity中自动生成。

反射

  • 反射有损性能,建议不用。大部分教程都推荐使用CLR绑定。

CLR重定向

  • ILRuntime为了解决外部调用内部接口的问题,引入了CLR重定向机制。 原理就是当IL解译器发现需要调用某个指定CLR方法时,将实际调用重定向到另外一个方法进行挟持,再在这个方法中对ILRuntime的反射的用法进行处理
  • 从代码中可以看出重定向的工作是把方法挟持下来后装到ILIntepreter的解释器里面实例化
  • 不带返回值的重定向:
代码语言:javascript
复制
public static StackObject* CreateInstance(ILIntepreter intp, StackObject* esp, List<object> mStack, CLRMethod method, bool isNewObj)
{
    //获取泛型参数<T>的实际类型
    IType[] genericArguments = method.GenericArguments;
    if (genericArguments != null && genericArguments.Length == 1)
    {
        var t = genericArguments[0];
        if (t is ILType)//如果T是热更DLL里的类型
        {
            //通过ILRuntime的接口来创建实例
            return ILIntepreter.PushObject(esp, mStack, ((ILType)t).Instantiate());
        }
        else
            return ILIntepreter.PushObject(esp, mStack, Activator.CreateInstance(t.TypeForCLR));//通过系统反射接口创建实例
    }
    else
        throw new EntryPointNotFoundException();
}
//注册
foreach (var i in typeof(System.Activator).GetMethods())
{
    //找到名字为CreateInstance,并且是泛型方法的方法定义
    if (i.Name == "CreateInstance" && i.IsGenericMethodDefinition)
    {
        //RegisterCLRMethodRedirection:通过redirectMap存储键值对MethodBase-CLRRedirectionDelegate,如果i不为空且redirectMap中没有传入的MethodBase(即下方的i)则存储redirectMap[i] = CreateInstance。所以如此看来注册行为就是把键值对存储到redirectMap的过程
        appdomain.RegisterCLRMethodRedirection(i, CreateInstance);
    }
}
  • 带返回值方法的重定向
代码语言:javascript
复制
public unsafe static StackObject* DLog(ILIntepreter __intp, StackObject* __esp, List<object> __mStack, CLRMethod __method, bool isNewObj)
{
    ILRuntime.Runtime.Enviorment.AppDomain __domain = __intp.AppDomain;
    StackObject* ptr_of_this_method;
    //只有一个参数,所以返回指针就是当前栈指针ESP - 1
    StackObject* __ret = ILIntepreter.Minus(__esp, 1);
    //第一个参数为ESP -1, 第二个参数为ESP - 2,以此类推
    ptr_of_this_method = ILIntepreter.Minus(__esp, 1);
    //获取参数message的值
    object message = StackObject.ToObject(ptr_of_this_method, __domain, __mStack);
    //需要清理堆栈
    __intp.Free(ptr_of_this_method);
    //如果参数类型是基础类型,例如int,可以直接通过int param = ptr_of_this_method->Value获取值,
    //关于具体原理和其他基础类型如何获取,请参考ILRuntime实现原理的文档。
			
    //通过ILRuntime的Debug接口获取调用热更DLL的堆栈
    string stackTrace = __domain.DebugService.GetStackTrance(__intp);
    Debug.Log(string.Format("{0}\n{1}", format, stackTrace));

    return __ret;
}

CLR绑定

  • 通过反射来调用接口调用效率会比直接调用低很多,再加上反射传递函数参数时需要使用object[]数组,这样不可避免的每次调用都会产生不少GC Alloc。众所周知GC Alloc高意味着在Unity中执行会存在较大的性能问题。
  • 最新版本有自动CLR绑定生成
image 101 - ILRuntime热更新
image 101 - ILRuntime热更新

LitJson集成

  • Json序列化是开发中非常经常需要用到的功能,考虑到其通用性,因此ILRuntime对LitJson这个序列化库进行了集成
代码语言:javascript
复制
//对LitJson进行注册,需要在注册CLR绑定之前
LitJson.JsonMapper.RegisterILRuntimeCLRRedirection(appdomain);
//LitJson使用
//将一个对象转换成json字符串
string json = JsonMapper.ToJson(obj);
//json字符串反序列化成对象
JsonTestClass obj = JsonMapper.ToObject<JsonTestClass>(json);

额外补充

  • 原教程代码示例中:Mono.Cecil.Pdb.PdbReaderProvider(),应改为ILRuntime.Mono.Cecil.Pdb.PdbReaderProvider()
image 99 - ILRuntime热更新
image 99 - ILRuntime热更新

其他

ILRuntime的性能优化

  • 博客中提到的性能优化建议:
image 102 1024x602 - ILRuntime热更新
image 102 1024x602 - ILRuntime热更新
  • 值类型优化:使用ILRuntime外部定义的值类型(例如UnityEngine.Vector3)在默认情况下会造成额外的装箱拆箱开销。ILRuntime在1.3.0版中增加了值类型绑定(ValueTypeBinding)机制,通过对这些值类型添加绑定器,可以大幅增加值类型的执行效率,以及避免GC Alloc内存分配。
  • 大规模数值计算:如果在热更内需要进行大规模数值计算,则可以开启ILRuntime在2.0版中加入的寄存器模式来进行优化
  • 避免使用foreach:尽量避免使用foreach,会不可避免地产生GC。而for循环不会。

Examples部分

01.HelloWorld

  • 加载dll并在逻辑后处理进行简单调用
  • 整个文件流程:创建IEnumerator并运行->用文件流判断并读入dll和pdb->尝试加载程序集dll->(如果加载成功)初始化脚本引擎(InitializeILRuntime())->执行脚本引擎加载后的逻辑处理(OnHotFixLoaded())->程序销毁(在OnDestoy中关闭dll和pdb的文件流)
  • MemoryStream:为系统提供流式读写。MemoryStream类封装一个字节数组,在构造实例时可以使用一个字节数组作为参数,但是数组的长度无法调整。使用默认无参数构造函数创建实例,可以使用Write方法写入,随着字节数据的写入,数组的大小自动调整。 参考博客:传送门
  • appdomain.LoadAssembly:将需要热更的dll加载到解释器中。第一个填入dll以及pdb,这里的pdb应该是dll对应的一些标志符号。 后面的ILRuntime.Mono.Cecil.Pdb.PdbReaderProvider()是动态修改程序集,它的作用是给ILRuntime.Mono.Cecil.Pdb.PdbReaderProvider()里的GetSymbolReader)(传入两个参数,一个是通过转化后的ModuleDefinition.ReadModule(stream(即dll))模块定义,以及原来的symbol(即pdb) GetSymbolReader主要的作用是检测其中的一些符号和标志是否为空,不为空的话就进行读取操作。 (这些内容都是ILRuntime中的文件来完成)
代码语言:javascript
复制
//appdomain.LoadAssembly,他还有一个重载版本,只填入第一个steam,其余会自动补充为null
//第三个空填入new ILRuntime.Mono.Cecil.Pdb.PdbReaderProvider()
public void LoadAssembly(Stream stream, Stream symbol, ISymbolReaderProvider symbolReader)
//ILRuntime.Mono.Cecil.Pdb.PdbReaderProvider()
public ISymbolReader GetSymbolReader(ModuleDefinition module, string fileName);
public ISymbolReader GetSymbolReader(ModuleDefinition module, Stream symbolStream)
  • appdomain.Invoke:可以明显看出第三个参数传入与否没用任何用处,只有在返回时原封不动地重新传入了instance
代码语言:javascript
复制
public object Invoke(string type, string method, object instance, params object[] p)
//方法实现
public object Invoke(string type, string method, object instance, params object[] p)
{
            IType type2 = GetType(type);
            if (type2 == null)
            {
                return null;
            }

            IMethod method2 = type2.GetMethod(method, (p != null) ? p.Length : 0);
            if (method2 != null)
            {
                for (int i = 0; i < method2.ParameterCount; i++)
                {
                    if (p[i] != null && !method2.Parameters[i].TypeForCLR.IsAssignableFrom(p[i].GetType()))
                    {
                        throw new ArgumentException("Parameter type mismatch");
                    }
                }

                return Invoke(method2, instance, p);
            }

            return null;
}
//普通调用
appdomain.Invoke("HotFix_Project.InstanceClass", "StaticFunTest", null, null);
//HotFix_Project.InstanceClass里的静态方法StaticFunTest
public static void StaticFunTest()
{
	UnityEngine.Debug.Log("!!! InstanceClass.StaticFunTest()");
}
//传参调用
appdomain.Invoke("HotFix_Project.InstanceClass", "StaticFunTest2", null, 123);
//HotFix_Project.InstanceClass里的静态方法HotFix_Project.InstanceClass
public static void StaticFunTest2(int a)
{
	UnityEngine.Debug.Log("!!! InstanceClass.StaticFunTest2(), a=" + a);
}
//泛型调用
public static void GenericMethod<T>(T a)
{
	UnityEngine.Debug.Log("!!! InstanceClass.GenericMethod(), a=" + a);
}
//多值调用ref/out
public void RefOutMethod(int addition, out List<int> lst, ref int val)
{
	val = val + addition + id;
	lst = new List<int>();
	lst.Add(id);
}
  • 剩余事项都在示例代码注释中有所提及。

02.Invocation(调用)

  • 对逻辑后处理部分进行了各种方式的调用展示(详细内容请看01)
  • 优化:预先获得IMethod,可以减低每次调用查找方法耗用的时间
代码语言:javascript
复制
Debug.Log("通过IMethod调用方法");
//预先获得IMethod,可以减低每次调用查找方法耗用的时间
IType type = appdomain.LoadedTypes["HotFix_Project.InstanceClass"];
//根据方法名称和参数个数获取方法
IMethod method = type.GetMethod("StaticFunTest2", 1);
appdomain.Invoke(method, null, 123);

03.Delegate(跨域委托)

  • 委托的使用以及热更新注册部分
  • 实现:定义->InitializeILRuntime()添加适配器/转换器->OnHotFixLoaded()调用
代码语言:javascript
复制
void InitializeILRuntime()
{
#if DEBUG && (UNITY_EDITOR || UNITY_ANDROID || UNITY_IPHONE)
        //由于Unity的Profiler接口只允许在主线程使用,为了避免出异常,需要告诉ILRuntime主线程的线程ID才能正确将函数运行耗时报告给Profiler
        appdomain.UnityMainThreadID = System.Threading.Thread.CurrentThread.ManagedThreadId;
#endif
        //这里做一些ILRuntime的注册
        //TestDelegateMethod, 这个委托类型为有个参数为int的方法,注册仅需要注册不同的参数搭配即可
        appdomain.DelegateManager.RegisterMethodDelegate<int>();
        //带返回值的委托的话需要用RegisterFunctionDelegate,返回类型为最后一个
        appdomain.DelegateManager.RegisterFunctionDelegate<int, string>();
        //Action<string> 的参数为一个string
        appdomain.DelegateManager.RegisterMethodDelegate<string>();
        
        //ILRuntime内部是用Action和Func这两个系统内置的委托类型来创建实例的,所以其他的委托类型都需要写转换器
        //将Action或者Func转换成目标委托类型

        appdomain.DelegateManager.RegisterDelegateConvertor<TestDelegateMethod>((action) =>
        {
            //转换器的目的是把Action或者Func转换成正确的类型,这里则是把Action<int>转换成TestDelegateMethod
            return new TestDelegateMethod((a) =>
            {
                //调用委托实例
                ((System.Action<int>)action)(a);
            });
        });
        //对于TestDelegateFunction同理,只是是将Func<int, string>转换成TestDelegateFunction
        appdomain.DelegateManager.RegisterDelegateConvertor<TestDelegateFunction>((action) =>
        {
            return new TestDelegateFunction((a) =>
            {
                return ((System.Func<int, string>)action)(a);
            });
        });

        //下面再举一个这个Demo中没有用到,但是UGUI经常遇到的一个委托,例如UnityAction<float>
        appdomain.DelegateManager.RegisterDelegateConvertor<UnityEngine.Events.UnityAction<float>>((action) =>
        {
            return new UnityEngine.Events.UnityAction<float>((a) =>
            {
                ((System.Action<float>)action)(a);
            });
        });
}
代码语言:javascript
复制
//补充
//从源码中可以看到以下两种注册都是使用Action和Func进行实现的,整合后会转递给:public void RegisterDelegateConvertor<T>(Func<Delegate, Delegate> action)
//RegisterMethodDelegate最多支持五个泛型
public void RegisterMethodDelegate<T1, T2, T3, T4, T5>()
//RegisterFunctionDelegate最多支持五个泛型,最后一个是返回值
public void RegisterFunctionDelegate<TResult>()
public void RegisterFunctionDelegate<T1, T2, T3, T4, TResult>()
  • 如果需要跨域调用委托(将热更DLL里面的委托实例传到Unity主工程用), 就需要注册适配器
  • 应该尽量减少不必要的跨域委托调用,如果委托只在热更DLL中用,是不需要进行任何注册的

04.Inheritance(跨域继承)

  • 创建热更类->注册适配器->创建实例
代码语言:javascript
复制
//热更类
public abstract class TestClassBase
{
    public virtual int Value
    {
        get
        {
            return 0;
        }
        set
        {

        }
    }

    public virtual void TestVirtual(string str)
    {
        Debug.Log("!! TestClassBase.TestVirtual, str = " + str);
    }

    public abstract void TestAbstract(int gg);
}
//加载后处理
void OnHotFixLoaded()
{
        Debug.Log("首先我们来创建热更里的类实例");
        TestClassBase obj;
        Debug.Log("现在我们来注册适配器, 该适配器由ILRuntime/Generate Cross Binding Adapter菜单命令自动生成");
        appdomain.RegisterCrossBindingAdaptor(new TestClassBaseAdapter());
        Debug.Log("现在再来尝试创建一个实例");
    //这里的TestInheritance为public class TestInheritance : TestClassBase
    //appdomain.Instantiate:public T Instantiate<T>(string type, object[] args = null)
        obj = appdomain.Instantiate<TestClassBase>("HotFix_Project.TestInheritance");
        Debug.Log("现在来调用成员方法");
        obj.TestAbstract(123);
        obj.TestVirtual("Hellopublic T Instantiate<T>(string type, object[] args = null)
        obj.Value = 233;//public override int Value { get; set; }
        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);
}

05.CLRRedirection(CLR重定向)

  • CLR重定向:需要挟持原方法实现,添加一些热更DLL中的特殊处理的时使用。
  • 由以下代码可以看到,如果不使用重定向,那么系统会调用反射方法来检测使用log。即可以大致的理解为重定向可以允许跨域的调用使用方法。而当没有重定向时,需要使用反射,去“探索”要调用的方式方法。
代码语言:javascript
复制
unsafe void InitializeILRuntime()
{
	...
	//这里做一些ILRuntime的注册
	var mi = typeof(Debug).GetMethod("Log", new System.Type[] { typeof(object) });
    //Log_11为重定向方法
    appdomain.RegisterCLRMethodRedirection(mi, Log_11);
}
unsafe void OnHotFixLoaded()
{
	Debug.Log("请注释和解除InitializeILRuntime方法里的重定向注册,对比下一行日志的变化");
    //注册时显示:call System.Void UnityEngine.Debug::Log(System.Object)
    /*注释时显示:System.Reflection.MethodBase:Invoke (object,object[])
    ILRuntime.CLR.Method.CLRMethod:Invoke*/
    appdomain.Invoke("HotFix_Project.TestCLRRedirection", "RunTest", null, null);
}

06.CLRBinding(CLR绑定)

  • (首先在菜单栏中点击自动生成绑定文件)
  • 按照提示对比方法执行耗时:ILRuntime.Runtime.Generated.CLRBindings.Initialize(appdomain); 注释时:950ms 解除注释:150ms 基础注释并使用RunTest():170ms
  • 可以明显看出CLR绑定对于方法执行耗时有显著的改善,同时先确定IMethod在Invoke会省去一定的查找时间,但对于性能的影响相对较小。
  • 注释时和解除注释时产生的GC.Alloc
image 103 - ILRuntime热更新
image 103 - ILRuntime热更新
image 104 - ILRuntime热更新
image 104 - ILRuntime热更新
代码语言:javascript
复制
//CLR绑定,放在void InitializeILRuntime()里的末尾部分
ILRuntime.Runtime.Generated.CLRBindings.Initialize(appdomain);

void Update()
{
        if (ilruntimeReady && !executed && Time.realtimeSinceStartup > 3)
        {
            executed = true;
            //这里为了方便看Profiler,代码挪到Update中了
            System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
            var type = appdomain.LoadedTypes["HotFix_Project.TestCLRBinding"];
            var m = type.GetMethod("RunTest", 0);
            Debug.Log("请解除InitializeILRuntime方法中的注释对比有无CLR绑定对运行耗时和GC开销的影响");
            sw.Reset();
            sw.Start();
            //使用直接调用的方法
            Profiler.BeginSample("RunTest");
            appdomain.Invoke("HotFix_Project.TestCLRBinding", "RunTest", null, null);
            /*使用指定位置后调用的方法
            Profiler.BeginSample("RunTest2");
            appdomain.Invoke(m, null, null);*/
            Profiler.EndSample();
            sw.Stop();
            Debug.LogFormat("刚刚的方法执行了:{0} ms", sw.ElapsedMilliseconds);

            Debug.Log("可以看到运行时间和GC Alloc有大量的差别,RunTest2之所以有20字节的GC Alloc是因为Editor模式ILRuntime会有调试支持,正式发布(关闭Development Build)时这20字节也会随之消失");
        }
}

07.Coroutine(协程调用)

  • 在主工程文件中写方法调用协程,之后使用协程直接传递。
  • 跨域继承只能有1个Adapter,因此应该尽量避免一个类同时实现多个外部接口,对于coroutine来说是IEnumerator<object>,IEnumerator和IDisposable,ILRuntime虽然支持,但是一定要小心这种用法,使用不当很容易造成不可预期的问题。
  • 日常开发如果需要实现多个DLL外部接口,请在Unity这边先做一个基类实现那些个接口,然后继承那个基类
代码语言:javascript
复制
void InitializeILRuntime()
{
	...
	//使用Couroutine时,C#编译器会自动生成一个实现了IEnumerator,IEnumerator<object>,IDisposable接口的类,因为这是跨域继承,所以需要写CrossBindAdapter(详细请看04_Inheritance教程),Demo已经直接写好,直接注册即可
    appdomain.RegisterCrossBindingAdaptor(new CoroutineAdapter());
    appdomain.DebugService.StartDebugService(56000);//如果要执行单步debug就加上这句话
}

//Demo协程
public class CoroutineDemo : MonoBehaviour
{
    static CoroutineDemo instance;
    public static CoroutineDemo Instance
    {
        get {return instance;}
    }
}
//HotFix_Project->TestCoroutine代码
public class TestCoroutine
{
	public static void RunTest()
    {
		CoroutineDemo.Instance.DoCoroutine(Coroutine());
    }

	static System.Collections.IEnumerator Coroutine()
    {
		Debug.Log("开始协程,t=" + Time.time);
         yield return new WaitForSeconds(3);
         Debug.Log("等待了3秒,t=" + Time.time);
    }
}

08.MonoBehaviour(热更DLL使用MonoBehaviour->不建议)

image 105 - ILRuntime热更新
image 105 - ILRuntime热更新

09.Reflection(主工程反射DLL类型)

  • 热更DLL中使用反射跟原生C#没有区别
  • Demo主要介绍主工程反射热更DLL中的类型
代码语言:javascript
复制
void OnHotFixLoaded()
{
        Debug.Log("热更DLL中的类型我们均需要通过AppDomain取得");
        var it = appdomain.LoadedTypes["HotFix_Project.InstanceClass"];
        Debug.Log("LoadedTypes返回的是IType类型,但是我们需要获得对应的System.Type才能继续使用反射接口");
        var type = it.ReflectionType;
        Debug.Log("取得Type之后就可以按照我们熟悉的方式来反射调用了");
    //返回为当前 Type 定义的所有公共构造函数。
        var ctor = type.GetConstructor(new System.Type[0]);
        var obj = ctor.Invoke(null);
        Debug.Log("打印一下结果");
        Debug.Log(obj);
        Debug.Log("我们试一下用反射给字段赋值");
        var fi = type.GetField("id", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
        fi.SetValue(obj, 111111);
        Debug.Log("我们用反射调用属性检查刚刚的赋值");
        var pi = type.GetProperty("ID");
        Debug.Log("ID = " + pi.GetValue(obj, null));
}

10.LitJson(LitJson支持)

  • 直接参照手册中的代码即可。实际代码只需要两行即可完成相互转换。

11.ValueTypeBinding(值类型绑定)

  • 需要对应的值类型直接在InitializeILRuntime中绑定即可。
代码语言:javascript
复制
//注册对应Binder
void InitializeILRuntime()
{
	......
        //这里做一些ILRuntime的注册,这里我们注册值类型Binder,注释和解注下面的代码来对比性能差别
        appdomain.RegisterValueTypeBinder(typeof(Vector3), new Vector3Binder());
        appdomain.RegisterValueTypeBinder(typeof(Quaternion), new QuaternionBinder());
        appdomain.RegisterValueTypeBinder(typeof(Vector2), new Vector2Binder());
}
  • 耗时对比:经过约100000数据运算

类型

注册耗时

不注册耗时

Vector3

105ms

2444ms

Quaternion

110ms

1685ms

vector2

107ms

2427ms

12.Performance(真机ILRuntime与Lua对比)

  • 首先打包程序为apk:
  • 遇到报错:JDK not found,解决方案比较麻烦,出现该错误的原因为该版本下的Unity先创建的普通3D模板转Android后会丢失JDK等组件,使用原有组件会报版本不匹配错误。故删除模板直接使用3D Andoid模板进行打包,顺利打包。
  • 安装到模拟器(雷电模拟器),效果演示:
  • Lua需要安装Xlua这里就不进行深入探索了。
image 106 1024x583 - ILRuntime热更新
image 106 1024x583 - ILRuntime热更新
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022年7月25日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前置知识
  • 手册部分
    • 环境
      • 基础
        • 原理
        • 优势
        • C# vs Lua
      • 教程
        • 从零开始
        • 安装调试器
        • 寄存器模式
        • 跨域继承
        • 反射
        • CLR重定向
        • CLR绑定
        • LitJson集成
        • 额外补充
      • 其他
        • ILRuntime的性能优化
    • Examples部分
      • 01.HelloWorld
        • 02.Invocation(调用)
          • 03.Delegate(跨域委托)
            • 04.Inheritance(跨域继承)
              • 05.CLRRedirection(CLR重定向)
                • 06.CLRBinding(CLR绑定)
                  • 07.Coroutine(协程调用)
                    • 08.MonoBehaviour(热更DLL使用MonoBehaviour->不建议)
                      • 09.Reflection(主工程反射DLL类型)
                        • 10.LitJson(LitJson支持)
                          • 11.ValueTypeBinding(值类型绑定)
                            • 12.Performance(真机ILRuntime与Lua对比)
                            相关产品与服务
                            容器服务
                            腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                            领券
                            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档