ET ILRuntime
官网:http://ourpalm.github.io/ILRuntime/public/v1/guide/index.html
原理
代码热更需要将热更域编译过后的程序集,以资源的方式打包到热更资源服务器中,用户在下载热更程序集后利用IL反射使用该程序集中方法运行游戏
安装
- 下载Unity3D实例工程(切勿下载原工程,修改起来太麻烦)http://ourpalm.github.io/ILRuntime/public/v1/guide/tutorial.html
- 下载VS调试插件(上一步页面最下方有链接)
- 使用时需要在你的Unity工程里添加
appdomain.DebugService.StartDebugService(56000);
该代码,其中的appdomain
指向你的热更程序集 - 然后启动Unity,不要启动VS附加
- 如果安装插件成功可以在VS 的
调试
选项下面看见Attach to ILRuntime
,点击后即可附加到Unity上 - 貌似使用该插件时,如果断点打到异步方法(ETVoid与ETTask)内程序会报错,且无法执行,取消断点啥事没有
- 使用时需要在你的Unity工程里添加
- 将以下几个文件夹复制到项目中:CLR、Other、Plugins、Reflection、Runtime
代码结构
BuildHotfixEditor
编辑器类,将热更层程序集从项目根目录拷贝到项目中,方便后续的资源热更
ILRuntimeCLRBinding
编辑器类,用于CLR绑定
CLRBindings
用于绑定生成的CLR代码
ILHelper
用于注册委托适配器和跨域继承适配器
-
IAsyncStateMachineAdaptor
异步状态机适配器 -
IDisposableAdaptor
Disposable适配器 -
IMessage
IMessage适配器
ActionHelper
委托转换器
Hotfix
热更层管理类
ILStaticMethod
一个待执行的IL静态方法
编辑器类
-
BuildHotfixEditor
用于每次Unity编译后自动将unity项目根目录下
Library/ScriptAssemblies
文件夹里编译过后的的热更程序集复制到unityAssets/Res/Code
文件夹中 -
ILRuntimeCLRBinding
用于生成CLR绑定脚本
- 获取热更程序集,调用
ILHelper.InitILRuntime(domain);
对程序集进行适配器注册 - 调用
ILRuntime.Runtime.CLRBinding.BindingCodeGenerator.GenerateBindingCode(domain, "Assets/Model/ILBinding");
来生成CLR绑定脚本,到指定目录下
- 获取热更程序集,调用
热更流程
-
在下载完热更资源(AB包)后,调用
Game.Hotfix.LoadHotfixAssembly();
方法加载热更程序集- 从
ResourcesComponent
加载之前创建的预制体,并获取程序集文件 - 将程序集文件转换成内存流
- 创建
ILRuntime.Runtime.Enviorment.AppDomain
,调用它的LoadAssembly
方法,并将上一步获取到的内存流作为参数传入 - 传入类名和要执行的方法名,new一个
ILStaticMethod
等待执行静态方法(该方法指向热更层的Init类中的Start方法) - 最后保存程序集中类型
- 从
-
调用
Game.Hotfix.GotoHotfix();
启动热更层Init类的Start方法- 调用
ILHelper.InitILRuntime
方法进行适配器注册- 调用
appdomain.DelegateManager.RegisterMethodDelegate<T>();
来进行不同类型的委托适配器注册 - 从Model程序集中获取标识了
ILAdapterAttribute
适配器标识类的类型,实例化该类型,然后调用appdomain.RegisterCrossBindingAdaptor(adaptor);
来注册该跨域继承适配器
- 调用
- 调用之前创建的IL静态方法的
Run
函数,执行热更程序集中对应方法
- 调用
-
关于委托转换器,参考
ActionHelper
类ILRuntime内部是使用Action,以及Func这两个系统自带委托类型来生成的委托实例,所以如果你需要将一个不是Action或者Func类型的委托实例传到ILRuntime外部使用的话,除了委托适配器,还需要额外写一个转换器,将Action和Func转换成你真正需要的那个委托类型。例如热更层给Button添加事件方法。
跨域继承适配器Adaptor
当我们在热更程序集中继承其他程序集的类或接口称之为跨域继承,由于热更域的特殊性,需要对继承类编写适配器,来达到热更
下列是ET已经写好的适配器:
-
IAsyncStateMachineAdaptor
异步状态机适配器 -
IDisposableAdaptor
Disposable适配器 -
IMessage
IMessage适配器
适配器编写规则,拿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);
总结
- 在热更层
Hotfix
文件夹中新建Unity程序集定义Assembly Definition File
- 建立
BuildHotfixEditor
编辑器脚本,复制热更程序集 - 创建预制体,引用程序集,并将该程序集设置为AB包,方便资源更新
- 创建
ILRuntimeCLRBinding
编辑器脚本,用于创建CLR绑定脚本 - 为跨域继承的类写跨域继承适配器
CrossBindingAdaptor
- 为非Action、Func类型的委托,写委托转换器
- 为不同参数类型委托写委托适配器
- 在调用热更代码前进行热更程序集的加载(参考Game.Hotfix.LoadHotfixAssembly();)
- 进行热更重定向注册委托适配器和跨域继承适配器(参考ILHelper.InitILRuntime(this.appDomain);)
补充
-
多线程
为了防止多线程产生未知错误,可以在执行完适配器注册后进行预热
appdomain.Prewarm("多线程相关类的类名");
与ASYNC异步宏的冲突问题,在打上异步宏后需要执行下CLR绑定
-
为什么需要clr绑定?
两个作用:防止热更层用到的框架层代码被裁减, 以及加速热更代码的执行。
为什么会被裁减呢?因为Unity打包的时候真的不把这个热更dll看做dll,因为这个热更dll是脱离unity框架层的。自然在unity打包的时候,为了包体大小会把认为没有使用的代码全部过滤掉。这种情况下ILRuntime解释执行的时候,去反射调用框架层代码就会被视为错误,因为框架层不存在这些被调用的代码。
加速热更代码执行其实是ILRuntime解释每条il指令的时候,都会去现有缓存中查找当前指令是否为重定向函数,如果为重定向函数,则直接调用,如果不是重定向函数,则会反射调用,反射这就是效率的隐患。重定向函数有自己的函数签名格式,类似lua的LuaCsFunction。
-
为什么需要委托适配器?
因为ilruntime把热更内部的delegate都看作是action/func的形式,但是框架层可能是自定义的delegate形式,这就需要一层转换。
-
为什么需要适配器?
因为热更层与框架层脱离了关系,至少在Unity看来脱离了关系,那么此时Unity就会开始自己的strip优化,框架层中一些仅仅被热更层继承使用的接口,类等就可能被优化掉。所以第一个原因就是:防裁剪。
因为脱离了关系,那么如何在框架层中驱动的时候,可以同步驱动到热更层,这就成了一个问题。这就需要框架层引用热更层的相关instance去驱动 ,那么如何引用?这就是适配器的作用。适配器工作在框架层,其显式强调了需要引用驱动的类型实例,然后重写相关函数体内容,去实质调用 热更类型实例 的方法。具体参考MonobahaviourAdaptor即可理解。