ILRuntime中的编程2

思考并回答以下问题:

本章涵盖:

学习过程中一定要多看ILRuntime的文档和Demo,多动手尝试。

类的跨域继承

跨域继承是什么意思呢?就是基类和子类不在一个“域”中,不同时在Unity主工程或热更dll工程中。

比如基类或者接口是在Unity主工程代码中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public abstract class TestClassBase
{
public virtual int Value
{
get
{
return 0;
}
}

public virtual void TestVirtual(string str)
{
Debug.Log("!! TestClassBase.TestVirtual, str = " + str);
}

public abstract void TestAbstract(int gg);
}

但是子类是在热更的dll中,这时候如果直接创建热更dll里的类的实例,就会报错:

System.TypeLoadException: Cannot find Adaptor for:TestClassBase
这是应为在ILRuntime中,跨域继承必须要注册适配器。

如果你想在热更DLL项目当中继承一个Unity主工程里的类,或者实现一个主工程里的接口,你需要在Unity主工程中实现一个继承适配器。 如果是热更DLL里面继承热更里面的类型,不需要任何注册。

注册适配器的方法如下:

1
appdomain.RegisterCrossBindingAdaptor(new InheritanceAdapter());

这个InheritanceAdapter中的内容是什么呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
using System;
using System.Collections;
using System.Collections.Generic;
using ILRuntime.CLR.Method;
using ILRuntime.Runtime.Enviorment;
using ILRuntime.Runtime.Intepreter;

public class InheritanceAdapter : CrossBindingAdaptor
{
public override Type BaseCLRType
{
get
{
return typeof(TestClassBase);//这是你想继承的那个类
}
}

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

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

//实际的适配器类需要继承你想继承的那个类,并且实现CrossBindingAdaptorType接口
class Adaptor : TestClassBase, CrossBindingAdaptorType
{
ILTypeInstance instance;
ILRuntime.Runtime.Enviorment.AppDomain appdomain;
IMethod mTestAbstract;
bool mTestAbstractGot;
IMethod mTestVirtual;
bool mTestVirtualGot;
bool isTestVirtualInvoking = false;
IMethod mGetValue;
bool mGetValueGot;
bool isGetValueInvoking = false;
//缓存这个数组来避免调用时的GC Alloc
object[] param1 = new object[1];

public Adaptor()
{

}

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

public ILTypeInstance ILInstance { get { return instance; } }

//你需要重写所有你希望在热更脚本里面重写的方法,并且将控制权转到脚本里去
public override void TestAbstract(int ab)
{
if (!mTestAbstractGot)
{
mTestAbstract = instance.Type.GetMethod("TestAbstract", 1);
mTestAbstractGot = true;
}
if (mTestAbstract != null)
{
param1[0] = ab;
appdomain.Invoke(mTestAbstract, instance, param1);//没有参数建议显式传递null为参数列表,否则会自动new object[0]导致GC Alloc
}
}

public override void TestVirtual(string str)
{
if (!mTestVirtualGot)
{
mTestVirtual = instance.Type.GetMethod("TestVirtual", 1);
mTestVirtualGot = true;
}
//对于虚函数而言,必须设定一个标识位来确定是否当前已经在调用中,否则如果脚本类中调用base.TestVirtual()就会造成无限循环,最终导致爆栈
if (mTestVirtual != null && !isTestVirtualInvoking)
{
isTestVirtualInvoking = true;
param1[0] = str;
appdomain.Invoke(mTestVirtual, instance, param1);
isTestVirtualInvoking = false;
}
else
base.TestVirtual(str);
}

public override int Value
{
get
{
if (!mGetValueGot)
{
//属性的Getter编译后会以get_XXX存在,如果不确定的话可以打开Reflector等反编译软件看一下函数名称
mGetValue = instance.Type.GetMethod("get_Value", 1);
mGetValueGot = true;
}
//对于虚函数而言,必须设定一个标识位来确定是否当前已经在调用中,否则如果脚本类中调用base.Value就会造成无限循环,最终导致爆栈
if (mGetValue != null && !isGetValueInvoking)
{
isGetValueInvoking = true;
var res = (int)appdomain.Invoke(mGetValue, instance, null);
isGetValueInvoking = false;
return res;
}
else
return base.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;
}
}
}

虽然上面的代码看起来很长,但是里面的套路是类似的,主要是用来重写所有你希望在热更脚本里面重写的方法,并且将控制权转到脚本里去。

这里面有两个需要注意的地方:

适配器需要继承CrossBindingAdaptor并实现其中的方法

实际的适配器类需要继承你想继承的那个类,并且实现CrossBindingAdaptorType接口

ILRuntime中的反射

用C#开发项目,很多时候会需要使用反射来实现某些功能。但是在脚本中使用反射其实是一个非常困难的事情。因为这需要把ILRuntime中的类型转换成一个真实的C#运行时类型,并把它们映射起来。

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

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

在热更DLL中使用反射跟原生C#没有任何区别,那如何在主工程中反射热更DLL中的类型呢?

假设我们要通过反射创建HotFix_Project.InstanceClass的实例,通过Activator或者Type.GetType(“HotFix_Project.InstanceClass”)是无法取到类型信息的,热更DLL中的类型我们均需要通过AppDomain取得,也就是这样:

1
var it = appdomain.LoadedTypes["HotFix_Project.InstanceClass"];

LoadedTypes返回的是IType类型,但是我们需要获得对应的System.Type才能继续使用反射接口,这时候可以用下面的接口:

1
var type = it.ReflectionType;

取得Type之后就可以按照我们熟悉的方式来反射调用了:

1
2
3
4
5
6
7
8
9
10
var ctor = type.GetConstructor(new System.Type[0]);
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));

ILRuntime与LitJson

LitJson我们之前学到过,可以用来序列化和反序列化JSON。

ILRuntime也集成了LitJson,可以更好地在热更的dll中使用JSON,由于涉及到跨域,在反序列化时直接使用会有问题。

在使用LitJson前,需要对LitJson进行注册,注册方法很简单,只需要在ILRuntime初始化阶段,在注册CLR绑定之前,执行下面这行代码即可:

1
2
3
4
5
void InitializeILRuntime()
{
//这里做一些ILRuntime的注册,这里我们对LitJson进行注册
LitJson.JsonMapper.RegisterILRuntimeCLRRedirection(appdomain);
}

注册过后,就可以在热更的dll工程中使用LitJson来处理JSON了。

总结

ILRuntime中常用的编程方法除了这两天讲的,还有一些高级的技术,比如CLR绑定和CLR重定向,这些高级的技术理解起来非常困难,暂时就不给你增加负担了。如果你在实际项目中使用了ILRuntime,当你遇到问题时,可以再回头去学这些高级技术。别忘了去ILRuntime的文档寻找答案。

0%