ILRuntime官方Demo分析

一.简介

  ILRuntime是一个纯C#的热更新框架,能够使不支持JIT的运行环境(如IOS)能够实现代码热更新。

  项目Github地址:Ourpalm/ILRuntime: Pure C# IL Intepreter Runtime, which is fast and reliable for scripting requirement on enviorments, where jitting isn't possible. (github.com)

  官方文档地址:介绍 — 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.暂未完成

原文地址:https://www.cnblogs.com/movin2333/p/14827508.html