一.简介
ILRuntime是一个纯C#的热更新框架,能够使不支持JIT的运行环境(如IOS)能够实现代码热更新。
官方文档地址:介绍 — ILRuntime (ourpalm.github.io)。
参考资料:ILRuntime入门笔记 - 赵青青 - 博客园 (cnblogs.com)。
二.导入ILRuntime框架
这篇博客使用Unity版本为2020.3.9,在Unity2018以上版本中可以直接使用Package Manager导入:
1)打开Unity项目,并打开Package Manager,搜索找到ILRuntime。
2)导入后如果在Console中发现大量CS0227报错,那么在Player设置中打开允许不安全代码选项(ILRuntime中使用了指针等不安全代码):
3)勾选后,正常情况下Console中就没有报错信息了。接下来在Project窗口中就可以打开刚才导入的Demo的各种场景,如下图所示:
三.Demo场景及脚本分析
运行Demo场景:打开任意一个Demo的场景,直接运行都会有404报错,这是因为我们还没有生成热更新资源。热更新资源位置如下图所示:
在Demo目录下有一个HotFix_Project~文件夹,这个文件夹以~结尾,Unity会忽略掉它。在Unity中打开Visio Studio,在解决方案上右键添加现有项目,如下图:
在弹出的窗口中找到HotFix_Project~文件夹下以.csproj结尾的的文件并添加这个文件,就可以在VS的解决方案中看到这个项目了:
接下来在项目上右键点击生成就可以生成热更资源,注意可能在生成过程中有报错,需要解决所有报错才能成功生成:
成功生成文件后可以在Project窗口中看到StreamingAssets文件夹和下面的两个热更新文件:
这时再次打开Demo中的场景运行就可以看到相应的打印信息了:
1.Demo场景一:HelloWorld:
场景一的运行结果如上图所示,打开相应的脚本文件后,发现其中的注释是中文的(ILRuntime是由国人写的)。简单做如下分析:
1)脚本在Start函数中开启了协程运行ILRuntime脚本,21-30行是创建AppDomain对象及相应的注释,这个类提供了运行热更新的dll文件中的函数的各种方式:
2)31-63行代码是使用WWW加载热更新的资源,案例中加载了热更新的方法所在的dll文件和调试数据库pdb文件:
3)接下来调用了InitializeILRuntime方法,顾名思义,这个方法会初始化AppDomain对象。
4)在加载好了dll文件及初始化了AppDomain对象后,就可以开始运行dll中的方法了:
总结:ILRuntime热更的一个基本的流程是将项目代码打包为dll,这个dll可以直接放在项目文件中(DEBUG阶段),也可以打包到AB包中并从远端下载后加载(RELEASE阶段)。在工程中,使用AppDomain调用dll文件中的相应方法即可实现热更新(可以提供一个固定的开始热更新方法并调用这个方法,然后热更新部分再在这个方法中调用其他热更新资源)。
2.Demo场景二:Invocation:
场景二对应的脚本文件和场景一的脚本基本相同,只是在OnHotFixLoaded函数中提供了演示了各种方法的调用方式(静态方法调用、成员方法调用、对象创建、泛型方法调用、获取类的抽象对象、获取方法的抽象对象再调用等),脚本中的注释已经比较详细了,这里不再赘述。
3.Demo场景三:DelegateDemo:
场景三主要演示了委托的调用。如果委托类型定义在Unity主工程代码(非热更代码)中或热更代码中或者是使用C#或Unity定义好的委托(Action、Func等),委托声明在热更新代码中,然后在热更新代码或Unity主工程代码中执行委托,这样的委托执行和其他方法执行方式相同,不需要特殊操作。但是委托经常被用于解耦合,在解耦合中经常出现这样一种情况:在主工程中定义委托和调用委托,但是在热更新工程代码中为委托添加方法,这种委托定义和方法添加(委托实例化)所在工程不同的调用方式称为跨域。由于ILRuntime中已经定义好了Action和Func委托,所以如果跨域委托是Action或Func类型,那么调用不会出现问题;但是如果跨域的委托是自定义的委托,这时委托的实例化和多播委托等操作就不会成功,好在ILRuntime框架提供了适配器来解决这个问题。委托适配器首先要注册委托,然后注册委托适配器(将委托强转为Action或Func类型进行调用),在场景三脚本中的InitializeILRuntime方法中示例了委托适配器的定义方式:
值得注意的是,Unity提供的委托类型UnityAction在ILRuntime中不支持,也需要适配器转换委托类型。ILRuntime的官方文档中明确了虽然ILRuntime支持委托跨域,但是尽量少使用,一般地,如果在工程中使用的特定框架中定义了不是Action或Func类型的委托时再使用适配器,自己定义委托时都尽量使用Action或Func类型的委托。
4.场景四:Inheritance:
场景四主要演示了跨域继承的问题。对于父子类均在主工程或者热更工程的继承关系,正常使用即可;但是对于父类和子类一个在主工程中一个在热更工程中的情况,我们可以称之为跨域继承。跨域继承同样需要提供一个适配器进行转换,这个适配器不像委托适配器,跨域继承的适配器需要在主工程中专门写一个类进行继承。下面是Demo中的跨域继承类,我在上面添加了相应的注释:
namespace ILRuntimeDemo { public class TestClassBaseAdapter : CrossBindingAdaptor //适配器必须继承CrossBindingAdaptor { static CrossBindingFunctionInfo<System.Int32> mget_Value_0 = new CrossBindingFunctionInfo<System.Int32>("get_Value"); static CrossBindingMethodInfo<System.Int32> mset_Value_1 = new CrossBindingMethodInfo<System.Int32>("set_Value"); static CrossBindingMethodInfo<System.String> mTestVirtual_2 = new CrossBindingMethodInfo<System.String>("TestVirtual"); static CrossBindingMethodInfo<System.Int32> mTestAbstract_3 = new CrossBindingMethodInfo<System.Int32>("TestAbstract"); public override Type BaseCLRType { get { return typeof(global::TestClassBase); //这里是父类类型或接口类型。跨域继承只能有一个适配器,所以如果同时实现多个接口,可能会造成不可预期的问题,尽量避免使用 } } public override Type AdaptorType { get { return typeof(Adapter); //这里是适配器类型 } } public override object CreateCLRInstance(ILRuntime.Runtime.Enviorment.AppDomain appdomain, ILTypeInstance instance) { return new Adapter(appdomain, instance); //返回适配器类 } /// <summary> /// 实际的适配器类,需要继承父类或实现子类要实现的接口(这里是TestClassBase) /// </summary> public class Adapter : global::TestClassBase, CrossBindingAdaptorType { ILTypeInstance instance; ILRuntime.Runtime.Enviorment.AppDomain appdomain; /// <summary> /// 无参构造 /// </summary> public Adapter() { } /// <summary> /// 有参构造 /// </summary> /// <param name="appdomain"></param> /// <param name="instance"></param> public Adapter(ILRuntime.Runtime.Enviorment.AppDomain appdomain, ILTypeInstance instance) { this.appdomain = appdomain; this.instance = instance; } public ILTypeInstance ILInstance { get { return instance; } } /// <summary> /// 下面的一系列方法是重写所有需要在热更脚本中重写的方法,在这些方法中将控制权转移到脚本中 /// 转移脚本可以采用以下方法进行:获取热更代码中重写的方法,如果获取到了方法就使用Invoke调用方法 /// </summary> /// <param name="str"></param> public override void TestVirtual(System.String str) { if (mTestVirtual_2.CheckShouldInvokeBase(this.instance)) base.TestVirtual(str); else mTestVirtual_2.Invoke(this.instance, str); } public override void TestAbstract(System.Int32 gg) { mTestAbstract_3.Invoke(this.instance, gg); } public override System.Int32 Value { get { if (mget_Value_0.CheckShouldInvokeBase(this.instance)) return base.Value; else return mget_Value_0.Invoke(this.instance); } set { if (mset_Value_1.CheckShouldInvokeBase(this.instance)) base.Value = value; else mset_Value_1.Invoke(this.instance, value); } } public override string ToString() { IMethod m = appdomain.ObjectType.GetMethod("ToString", 0); m = instance.Type.GetVirtualMethod(m); if (m == null || m is ILMethod) { return instance.ToString(); } else return instance.Type.FullName; } } } }
5.场景九:Reflection
在分析场景五之前先分析场景九反射。在场景九对应脚本中,结构和之前的脚本结构是相同的,对反射使用的演示在OnHotFixLoaded函数中。在热更工程中使用反射和C#工程使用反射的方式相同,只是在主工程中使用反射得到热更dll中的类或方法的方式有所不同:在主工程中获取热更dll中的类无法使用Activator或GetType方法进行,但是ILRuntime封装的类的抽象IType中的属性ReflectionType就是反射的Type类型,之后就可以通过Type获取其中的方法、属性等。
6.场景五:CLRRedirection:
场景五是CLR重定向。在热更工程中虽然能够使用主工程中的API,但是需要引用相应的dll文件,实际上热更工程会通过反射调用主工程中的API,这会增加性能消耗。重定向是指本来通过反射调用的API,通过挟持原方法的方式达到不使用反射的效果,从而消除反射调用造成的额外性能开销和频繁的GC。重定向的方法写法比较复杂,需要对ILRuntime的底层原理有较深入了解,我也暂时没有弄明白,所以暂时不作深入,下图是场景五脚本中提供的重定向方法:
7.场景六:CLRBinding:
由于重定向方法书写比较麻烦,而且一个工程中往往有很多需要重定向的内容,所以ILRuntime提供了CLR绑定的方法简化重定向,实际可以理解为ILRuntime提供了内置的通用重定向方法,我们只需要给定重定向的类名即可实现重定向,甚至ILRuntime还提供了自动分析dll引用生成CLR绑定的方式:
8.场景七:Coroutine:
在热更工程中是可以实现协程的。ILRuntime自动实现了IEnumerator接口的实现类,但是由于是跨域继承,所以需要适配器。
9.暂未完成