返回

ETILRuntime

官网:http://ourpalm.github.io/ILRuntime/public/v1/guide/index.html

原理

代码热更需要将热更域编译过后的程序集,以资源的方式打包到热更资源服务器中,用户在下载热更程序集后,调用ILRuntime解析热更程序集中的IL指令,然后桥接调用Model层程序集中IL方法运行游戏。

安装

  1. 下载Unity3D实例工程(切勿下载原工程,修改起来太麻烦)http://ourpalm.github.io/ILRuntime/public/v1/guide/tutorial.html
  2. 下载VS调试插件(上一步页面最下方有链接)
    • 使用时需要在你的Unity工程里添加appdomain.DebugService.StartDebugService(56000);该代码,其中的appdomain指向你的热更程序集
    • 然后启动Unity,不要启动VS附加
    • 如果安装插件成功可以在VS 的调试选项下面看见Attach to ILRuntime,点击后即可附加到Unity上
    • 貌似使用该插件时,如果断点打到异步方法(ETVoid与ETTask)内程序会报错,且无法执行,取消断点啥事没有
  3. 将以下几个文件夹复制到项目中:CLR、Other、Plugins、Reflection、Runtime

代码结构

BuildHotfixEditor编辑器类,将热更层程序集从项目根目录拷贝到项目中,方便后续的资源热更

ILRuntimeCLRBinding编辑器类,用于CLR绑定

CLRBindings用于绑定生成的CLR代码

ILHelper用于注册委托适配器和跨域继承适配器

  • IAsyncStateMachineAdaptor异步状态机适配器
  • IDisposableAdaptorDisposable适配器
  • IMessageIMessage适配器

ActionHelper委托转换器

Hotfix热更层管理类

ILStaticMethod一个待执行的IL静态方法

编辑器类

  • BuildHotfixEditor

    用于每次Unity编译后自动将unity项目根目录下Library/ScriptAssemblies文件夹里编译过后的的热更程序集复制到unityAssets/Res/Code文件夹中

  • ILRuntimeCLRBinding

    用于生成CLR绑定脚本

    1. 获取热更程序集,调用ILHelper.InitILRuntime(domain);对程序集进行适配器注册
    2. 调用ILRuntime.Runtime.CLRBinding.BindingCodeGenerator.GenerateBindingCode(domain, "Assets/Model/ILBinding");来生成CLR绑定脚本,到指定目录下

热更流程

  1. 在下载完热更资源(AB包)后,调用Game.Hotfix.LoadHotfixAssembly();方法加载热更程序集

    • ResourcesComponent加载之前创建的预制体,并获取程序集文件
    • 将程序集文件转换成内存流
    • 创建ILRuntime.Runtime.Enviorment.AppDomain,调用它的LoadAssembly方法,并将上一步获取到的内存流作为参数传入
    • 传入类名和要执行的方法名,new一个ILStaticMethod等待执行静态方法(该方法指向热更层的Init类中的Start方法)
    • 最后保存程序集中类型
  2. 调用 Game.Hotfix.GotoHotfix();启动热更层Init类的Start方法

    • 调用ILHelper.InitILRuntime方法进行适配器注册
      • 调用appdomain.DelegateManager.RegisterMethodDelegate<T>();来进行不同类型的委托适配器注册
      • 从Model程序集中获取标识了ILAdapterAttribute适配器标识类的类型,实例化该类型,然后调用appdomain.RegisterCrossBindingAdaptor(adaptor);来注册该跨域继承适配器
    • 调用之前创建的IL静态方法的Run函数,执行热更程序集中对应方法
  3. 关于委托转换器,参考ActionHelper

    ILRuntime内部是使用Action,以及Func这两个系统自带委托类型来生成的委托实例,所以如果你需要将一个不是Action或者Func类型的委托实例传到ILRuntime外部使用的话,除了委托适配器,还需要额外写一个转换器,将Action和Func转换成你真正需要的那个委托类型。例如热更层给Button添加事件方法。

跨域继承适配器Adaptor

当我们在热更程序集中继承其他程序集的类或接口称之为跨域继承,由于热更域的特殊性,需要对继承类编写适配器,来达到热更

下列是ET已经写好的适配器:

  • IAsyncStateMachineAdaptor异步状态机适配器
  • IDisposableAdaptorDisposable适配器
  • IMessageIMessage适配器

适配器编写规则,拿IDisposableAdaptor举例:

using System;
using ILRuntime.CLR.Method;
using ILRuntime.Runtime.Enviorment;
using ILRuntime.Runtime.Intepreter;

namespace ETModel
{
    /// <summary>
    /// Disposable适配器封装类
    /// </summary>
    [ILAdapter]//该特性用于ET在程序集中快速获取适配器类,然后进行注册
    public class IDisposableClassInheritanceAdaptor : CrossBindingAdaptor
    {
        public override Type BaseCLRType
        {
            get
            {
                return typeof(IDisposable);//这是你想继承的那个类

            }
        }

        public override Type AdaptorType
        {
            get
            {
                return typeof(IDisposableAdaptor);//这是实际的适配器类类型
            }
        }

        public override object CreateCLRInstance(ILRuntime.Runtime.Enviorment.AppDomain appdomain, ILTypeInstance instance)
        {
            return new IDisposableAdaptor(appdomain, instance);//创建一个新的适配器类实例
        }

        /// <summary>
        /// 适配器,继承你想继承的那个类和CrossBindingAdaptorType
        /// </summary>
        public class IDisposableAdaptor : IDisposable, CrossBindingAdaptorType
        {
            private ILTypeInstance instance;
            private ILRuntime.Runtime.Enviorment.AppDomain appDomain;

            //需要执行的方法
            private IMethod iDisposable;
            //方法要传入的参数
            private readonly object[] param0 = new object[0];

            public IDisposableAdaptor()
            {
            }

            public IDisposableAdaptor(ILRuntime.Runtime.Enviorment.AppDomain appDomain, ILTypeInstance instance)
            {
                this.appDomain = appDomain;
                this.instance = instance;
            }

            public ILTypeInstance ILInstance
            {
                get
                {
                    return instance;
                }
            }

            //以下方法为你需要重写所有你希望在热更脚本里面重写的方法,并且将控制权转到脚本里去
            public void Dispose()
            {
                //由于Dispose可能多次调用,所以将其写成一个全局遍历,避免每次都重复获取
                if (this.iDisposable == null)
                {
                    this.iDisposable = instance.Type.GetMethod("Dispose");
                }
                this.appDomain.Invoke(this.iDisposable, instance, this.param0);
            }

            public override string ToString()
            {
                IMethod m = this.appDomain.ObjectType.GetMethod("ToString", 0);
                m = instance.Type.GetVirtualMethod(m);
                if (m == null || m is ILMethod)
                {
                    return instance.ToString();
                }

                return instance.Type.FullName;
            }


        }
    }
}

在完成适配器编写完成后,还需要再程序启动后进行注册才能使用

ET在ILHelper.InitILRuntime(this.appDomain);方法中完成了对程序集中所有适配器的注册

核心代码 appdomain.RegisterCrossBindingAdaptor(adaptor);

总结

  1. 在热更层Hotfix文件夹中新建Unity程序集定义Assembly Definition File
  2. 建立BuildHotfixEditor编辑器脚本,复制热更程序集
  3. 创建预制体,引用程序集,并将该程序集设置为AB包,方便资源更新
  4. 创建ILRuntimeCLRBinding编辑器脚本,用于创建CLR绑定脚本
  5. 为跨域继承的类写跨域继承适配器CrossBindingAdaptor
  6. 为非Action、Func类型的委托,写委托转换器
  7. 为不同参数类型委托写委托适配器
  8. 在调用热更代码前进行热更程序集的加载(参考Game.Hotfix.LoadHotfixAssembly();)
  9. 进行热更重定向注册委托适配器和跨域继承适配器(参考ILHelper.InitILRuntime(this.appDomain);)

补充

  1. 多线程

    为了防止多线程产生未知错误,可以在执行完适配器注册后进行预热

    appdomain.Prewarm("多线程相关类的类名");

  2. 与ASYNC异步宏的冲突问题,在打上异步宏后需要执行下CLR绑定

  3. 为什么需要clr绑定?

    两个作用:防止热更层用到的框架层代码被裁减, 以及加速热更代码的执行。

    为什么会被裁减呢?因为Unity打包的时候真的不把这个热更dll看做dll,因为这个热更dll是脱离unity框架层的。自然在unity打包的时候,为了包体大小会把认为没有使用的代码全部过滤掉。这种情况下ILRuntime解释执行的时候,去反射调用框架层代码就会被视为错误,因为框架层不存在这些被调用的代码。

    加速热更代码执行其实是ILRuntime解释每条il指令的时候,都会去现有缓存中查找当前指令是否为重定向函数,如果为重定向函数,则直接调用,如果不是重定向函数,则会反射调用,反射这就是效率的隐患。重定向函数有自己的函数签名格式,类似lua的LuaCsFunction。

  4. 为什么需要委托适配器?

    因为ilruntime把热更内部的delegate都看作是action/func的形式,但是框架层可能是自定义的delegate形式,这就需要一层转换。

  5. 为什么需要适配器?

    因为热更层与框架层脱离了关系,至少在Unity看来脱离了关系,那么此时Unity就会开始自己的strip优化,框架层中一些仅仅被热更层继承使用的接口,类等就可能被优化掉。所以第一个原因就是:防裁剪。

    因为脱离了关系,那么如何在框架层中驱动的时候,可以同步驱动到热更层,这就成了一个问题。这就需要框架层引用热更层的相关instance去驱动 ,那么如何引用?这就是适配器的作用。适配器工作在框架层,其显式强调了需要引用驱动的类型实例,然后重写相关函数体内容,去实质调用 热更类型实例 的方法。具体参考MonobahaviourAdaptor即可理解。

Licensed under CC BY-NC-SA 4.0
0