从零开始

源码目录复制到Unity工程的Assets目录:

  • LitJson
  • Mono.Cecil
  • ILRuntime

需要注意的是,需要删除这些目录里面的bin、obj、Properties子目录,以及.csproj文件。此外,由于ILRuntime使用了unsafe代码来优化执行效率,所以你需要在Unity中开启unsafe模式

以及symbols里的2个AssemblyInfo文件

热更域AppDomain: Hotfix.dll

正常项目中应该是自行从其他地方下载dll,或者打包在AssetBundle中读取.

  • HotFix.dll
  • HotFix.pdb
    • PDB文件是调试数据库,如需要在日志中显示报错的行号,则必须提供PDB文件,不过由于会额外耗用内存,正式发布时请将PDB去掉.

读取这2个文件转换成内存流,

byte[] dll = ....bytes;
byte[] pdb = ....bytes;
fs = new MemoryStream(dll);
p = new MemoryStream(pdb);

第一步加载热更域中的程序集

AppDomain是ILRuntime的入口

// 正式版 将p改为null
appdomain.LoadAssembly(fs, p, new ILRuntime.Mono.Cecil.Pdb.PdbReaderProvider());

第二步初始化ILRuntime

进行一些ILRuntime的注册

调用热更域中程序集里的方法

appdomain.Invoke("HotFix_Project.InstanceClass", "StaticFunTest", null, null);

主工程调用热更工程中的方法

appdomain.Invoke

委托

非跨域调用委托

主工程Invoke -> 热更工程方法A -> 使用热更工程内定义的委托.

完全在热更dll内部使用的委托,直接可用, 不需要做任何处理.

跨域调用委托

主工程Invoke -> 热更工程方法B -> 在热更工程中引用了主工程,将hotfix的委托赋值给Model层的委托实例.

如果需要跨域调用委托(将热更DLL里面的委托实例传到Unity主工程用), 就需要注册适配器,不然就会像下面这样

原因是: 这是因为iOS的IL2CPP模式下,不能动态生成类型,为了避免出现不可预知的问题,我们没有通过反射的方式创建委托实例,因此需要手动进行一些注册,首先需要注册委托适配器,刚刚的报错的错误提示中,有提示需要的注册代码;

// 下面这些注册代码,正式使用的时候,
// 应该写在InitializeILRuntime()中

//TestDelegateMethod, 这个委托类型为有个参数为int的方法,注册仅需要注册不同的参数搭配即可
appdomain.DelegateManager.RegisterMethodDelegate<int>();
//带返回值的委托的话需要用RegisterFunctionDelegate,返回类型为最后一个
appdomain.DelegateManager.RegisterFunctionDelegate<int, string>();
//Action<string> 的参数为一个string
appdomain.DelegateManager.RegisterMethodDelegate<string>();

注册完毕后再次运行会发现这次会报另外的错误:

原因是: ILRuntime内部是用ActionFunc这两个系统内置的委托类型来创建实例的,所以其他的委托类型都需要写转换器,将Action或者Func转换成目标委托类型

// 定义的委托
public delegate void TestDelegateMethod(int a);
public delegate string TestDelegateFunction(int a);

public static TestDelegateMethod TestMethodDelegate;
public static TestDelegateFunction TestFunctionDelegate;
public static System.Action<string> TestActionDelegate;

//
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);
    });
});

再举一个例子UGUI经常遇到的一个委托,例如UnityAction

appdomain.DelegateManager.RegisterDelegateConvertor<UnityEngine.Events.UnityAction<float>>((action) =>
{
      return new UnityEngine.Events.UnityAction<float>((a) =>
      {
          ((System.Action<float>)action)(a);
      });
});

委托总结

  • 跨域调用委托: 将热更DLL里面的委托实例传到主工程用.
  • 委托适配器: 不同参数类型的委托注册.(如果不是内置委托,需要写转换器)
  • 委托转换器: 转换成Action或者Func.

Action或者Func当作委托类型的话,可以避免写转换器,所以项目中在不必要的情况下尽量只用ActionFunc,另外应该尽量减少不必要的跨域委托调用,如果委托只在热更DLL中用,是不需要进行任何注册的.

关于跨域交互:

Model -> Hotfix 使用事件,

Hotfix -> Moedel 使用引用。

继承

跨域继承

主工程中创建hotfix.dll中的继承主工程中基类的子类. 需要注册适配器.

  • TestClassBase: 主工程中的基类.
    • TestInheritance: 热更工程中继承主工程基类的子类.

appdomain.Instantiate<TestClassBase>("HotFix_Project.TestInheritance");

报错了,因为跨域继承必须要注册适配器

>>>>注册需要被跨域继承的主工程中基类的适配器例子

// 注册适配器, 应该在InitializeILRuntime()完成
appdomain.RegisterCrossBindingAdaptor(new InheritanceAdapter());

继承总结

  • 跨域继承: 继承一个主工程里的类,或者实现一个主工程里的接口.
  • 继承适配器: 在主工程中实现一个继承适配器。

反射

但是在脚本中使用反射其实是一个非常困难的事情。因为这需要把ILRuntime中的类型转换成一个真实的C#运行时类型,并把它们映射起来

默认情况下,System.Reflection命名空间中的方法,并不可能得知ILRuntime中定义的类型,因此无法通过Type.GetType等接口取得热更DLL里面的类型。而且ILRuntime里的类型也并不是一个System.Type

为了解决这个问题,ILRuntime额外实现了几个用于反射的辅助类:ILRuntimeTypeILRuntimeMethodInfoILRuntimeFieldInfo等,来模拟系统的类型来提供部分反射功能.

// LoadedTypes返回的是IType类型,但是我们需要获得对应的System.Type才能继续使用反射接口
var it = appdomain.LoadedTypes["HotFix_Project.InstanceClass"];
Debug.Log("");
// 取得Type之后就可以按照我们熟悉的方式来反射调用了
var type = it.ReflectionType;
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));

>>>>Hotfix中的InstanceClass类

反射总结

主工程 中不能通过new T()的方式来创建hotfix热更工程中的类型实例, 要用appdomain.Instantiate().

在Hotfix.dll热更域中:

//-------------------获取类型---------------------
//在热更DLL中,以下两种方式均可以
Type t = typeof(TypeName);
Type t2 = Type.GetType("TypeName");

//-------------------创建实例---------------------
//以下两种方式均可以创建实例
object instance = Activator.CreateInstance(t);
object instance = Activator.CreateInstance<TypeName>();

//-------------------调用方法---------------------
// 通过反射调用方法跟通常C#用法没有任何区别
Type type = typeof(TypeName);
object instance = Activator.CreateInstance(type);
MethodInfo mi = type.GetMethod("foo");
mi.Invoke(instance, null);

在Model主工程中,

//-------------------获取类型---------------------
// 无法通过Type.GetType来取得热更DLL内部定义的类,
// 而只能通过以下方式得到System.Type实例:
IType type = appdomain.LoadedTypes["TypeName"];
Type t = type.ReflectedType;

//-------------------创建实例---------------------
// 无法通过Activator来创建热更DLL内类型的实例,必须通过AppDomain来创建实例
// 主工程中不能通过new T()的方式来创建热更工程中的类型实例
object instance = appdomain.Instantiate("TypeName");

//-------------------调用方法---------------------
// 可以通过C#通常用法来调用,也可以通过ILRuntime自己的接口来调用,
// 两个方式是等效的:
IType t    = appdomain.LoadedTypes["TypeName"];
Type  type = t.ReflectedType;

object instance = appdomain.Instantiate("TypeName");

//系统反射接口
MethodInfo mi = type.GetMethod("foo");
mi.Invoke(instance, null);

//ILRuntime的接口
IMethod m = t.GetMethod("foo", 0);
appdomain.Invoke(m, instance, null);

以下方式没有主工程和热更域使用的区别.

//----------------获取和设置Field的值------------------
// 在热更DLL和Unity主工程中获取和设置Field的值跟通常C#用法没有区别
Type t;
FieldInfo fi = t.GetField("field");
object val = fi.GetValue(instance);
fi.SetValue(instance, val);

//----------------获取Attribute标注------------------
// 在热更DLL和Unity主工程中获取Attribute标注跟通常C#用法没有区别
Type t;
FieldInfo fi = t.GetField("field");
object[] attributeArr = fi.GetCustomAttributes(typeof(SomeAttribute), false);

CLR重定向

没办法直接运行的是主工程中通过new T()创建热更DLL内类型的实例。

Activator.CreateInstance();这个明显内部是new T();的接口通过了CLR重定向来解决, IL解释器发现调用某个指定的方法时, 将实际调用重定向到另外一个一个方法进行挟持.

>>>>编写重定向的例子

CLR绑定

如果要从热更DLL中调用主工程接口,是需要通过反射接口来调用的.

ILRuntime通过CLR方法绑定机制,可以选择性的对经常使用的CLR接口进行直接调用,从而尽可能的消除反射调用开销以及额外的GC Alloc.

CLR绑定借助了ILRuntime的CLR重定向机制来实现,因为实质上也是将对CLR方法的反射调用重定向到我们自己定义的方法里面来

ILRuntime提供了一个代码生成工具来自动生成CLR绑定代码。

[MenuItem("ILRuntime/Generate CLR Binding Code")]
static void GenerateCLRBinding()
{
    List<Type> types = new List<Type>();
    //在List中添加你想进行CLR绑定的类型
    types.Add(typeof(int));
    types.Add(typeof(float));
    types.Add(typeof(long));
    types.Add(typeof(object));
    types.Add(typeof(string));
    types.Add(typeof(Console));
    types.Add(typeof(Array));
    types.Add(typeof(Dictionary<string, int>));
    //所有ILRuntime中的类型,实际上在C#运行时中都是ILRuntime.Runtime.Intepreter.ILTypeInstance的实例,
    //因此List<A> List<B>,如果A与B都是ILRuntime中的类型,只需要添加List<ILRuntime.Runtime.Intepreter.ILTypeInstance>即可
    types.Add(typeof(Dictionary<ILRuntime.Runtime.Intepreter.ILTypeInstance, int>));
    //第二个参数为自动生成的代码保存在何处
    ILRuntime.Runtime.CLRBinding.BindingCodeGenerator.GenerateBindingCode(types, "Assets/ILRuntime/Generated");
}


// 在CLR绑定代码生成之后,需要将这些绑定代码注册到AppDomain中才能使CLR绑定生效,
// 但是一定要记得将CLR绑定的注册写在CLR重定向的注册后面,因为同一个方法只能被重定向一次,只有先注册的那个才能生效。
ILRuntime.Runtime.Generated.CLRBindings.Initialize(appdomain);

未绑定注册调用RunTest

  • 5.7M的GC Alloc.
  • 方法执行了409ms

使用自动绑定并且注册调用RunTest

  • 28.3KB的GC Alloc
  • 方法执行了123ms
    • 之所以有20字节的GC Alloc是因为Editor模式ILRuntime会有调试支持,正式发布(关闭Development Build)时这20字节也会随之消失

协程

热更工程中的协程让主工程去执行.

使用Couroutine时,C#编译器会自动生成一个实现了IEnumerator,IEnumerator,IDisposable接口的类,因为这是跨域继承,所以需要写CrossBindAdapter.

值类型绑定

Vector3等Unity常用值类型如果不做任何处理,在ILRuntime中使用会产生较多额外的CPU开销和GC Alloc

我们通过值类型绑定可以解决这个问题,只有主工程的值类型在热更Dll中使用才需要此处理,热更DLL内定义的值类型不需要任何处理.

void InitializeILRuntime()
{
    //这里做一些ILRuntime的注册,这里我们注册值类型Binder,注释和解注下面的代码来对比性能差别
    appdomain.RegisterValueTypeBinder(typeof(Vector3), new Vector3Binder());
    appdomain.RegisterValueTypeBinder(typeof(Quaternion), new QuaternionBinder());
    appdomain.RegisterValueTypeBinder(typeof(Vector2), new Vector2Binder());
}

>>>>值类型绑定

其他

  • 不推荐在热更Dll中使用MonoBehaviour
  • 对LitJson进行注册LitJson.JsonMapper.RegisterILRuntimeCLRRedirection(appdomain);
  • 防止IL2CPP剪裁,在主工程中,建立一个类,然后在里面定义用到的那些泛型实例的public变量

ET中的热更执行顺序

// --------------------主工程---------------------
// 1. 获取热更dll
Game.Hotfix.LoadHotfixAssembly();

// 1.1
Game.Scene.GetComponent<ResourcesComponent>().LoadBundle($"code.unity3d");
GameObject code = (GameObject)Game.Scene.GetComponent<ResourcesComponent>().GetAsset("code.unity3d", "Code");

// 1.2 获取之后创建热更域 并通过反射获取热更域的入口方法
this.start = new ILStaticMethod(this.appDomain, "ETHotfix.Init", "Start", 0);
// 1.3 获取热更域里的类型
this.hotfixTypes = this.appDomain.LoadedTypes.Values.Select(x => x.ReflectionType).ToList();

// 1.4
Game.Scene.GetComponent<ResourcesComponent>().UnloadBundle($"code.unity3d");

// 2.0
Game.Hotfix.GotoHotfix();

// 2.1  初始化热更域
ILHelper.InitILRuntime(this.appDomain);

// 2.2 执行热更域的入口方法,
this.start.Run();

// --------------------热更工程---------------------
// 3.0 热更域的Init类Start方法入口
// 注册热更层回调, 这里用到了跨域委托 但是都是Action类型实例, 不需要注册和转换器
ETModel.Game.Hotfix.Update = () => { Update(); };
ETModel.Game.Hotfix.FixedUpdate = () => { FixedUpdate(); };
ETModel.Game.Hotfix.LateUpdate = () => { LateUpdate(); };
ETModel.Game.Hotfix.OnApplicationQuit = () => { OnApplicationQuit(); };