注意

尽量不要在热更工程中使用继承MonoBehavior的类,MonoBehaviour需要适配大量的方法,而有些方法可能需要其他关于Unity底层的适配,非常复杂。

本节只展示基本的和可能会常用到的MonoBehaviour方法适配,如Awake、Update等

MonoBehaviour适配器

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
using ILRuntime.CLR.Method;
using ILRuntime.Runtime.Enviorment;
using ILRuntime.Runtime.Intepreter;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MonoBehaviourAdaptor : CrossBindingAdaptor
{
public override System.Type BaseCLRType => typeof(MonoBehaviour);

public override System.Type AdaptorType => typeof(Adaptor);

public override object CreateCLRInstance(AppDomain appdomain, ILTypeInstance instance)
{
return new Adaptor(appdomain, instance);
}
public class Adaptor : MonoBehaviour, CrossBindingAdaptorType
{
AppDomain m_AppDomain;
ILTypeInstance m_Instance;
IMethod m_AwakeMethod;
IMethod m_StartMethod;
IMethod m_UpdateMethod;
IMethod m_ToStringMethod;
public Adaptor() { }
public Adaptor(AppDomain appdomain, ILTypeInstance instance)
{
m_AppDomain = appdomain;
m_Instance = instance;
}

public ILTypeInstance ILInstance
{
get
{
return m_Instance;
}
set//可能会更改实例,因为热更工程中Mono的子类可能会创建和销毁
{
m_Instance = value;
//每次创建时要把函数重置,因为适配器是一直存在的,不会跟随Mono子类销毁
m_AwakeMethod = null;
m_StartMethod = null;
m_UpdateMethod = null;
m_ToStringMethod = null;
}
}
public AppDomain AppDomain
{
get { return m_AppDomain; }
set { m_AppDomain = value; }
}

public void Awake()
{
if(m_Instance != null)//有可能出现Awake调用时,ILRuntime还没有Mono子类的实例的情况,所以要判空。
{
if (m_AwakeMethod == null)
{
m_AwakeMethod = m_Instance.Type.GetMethod("Awake",0);
}
if (m_AwakeMethod != null)
{
m_AppDomain.Invoke(m_AwakeMethod,m_Instance,null);
}
}
}
public void Start()
{
if (m_StartMethod == null)
{
m_StartMethod = m_Instance.Type.GetMethod("Start", 0);
}
if (m_StartMethod != null)
{
m_AppDomain.Invoke(m_StartMethod, m_Instance, null);
}
}
public void Update()
{
if (m_UpdateMethod == null)
{
m_UpdateMethod = m_Instance.Type.GetMethod("Update", 0);
}
if (m_UpdateMethod != null)
{
m_AppDomain.Invoke(m_UpdateMethod, m_Instance, null);
}
}
public override string ToString()
{
if (m_ToStringMethod == null)
{
m_ToStringMethod = m_AppDomain.ObjectType.GetMethod("ToString", 0);//基础Object通过这种方式获取方法
}
IMethod m = m_Instance.Type.GetVirtualMethod(m_ToStringMethod);
if (m != null || m is ILMethod)//检测ToString方法是否被热更类重载过
{
return m_Instance.ToString();
}
else
{
return m_Instance.Type.FullName;
}

}
}
}

ILRuntimeCLRBindingILRuntimeManager中注册适配器

1
AppDomain.RegisterCrossBindingAdaptor(new MonoBehaviourAdaptor());

注册完后生成一下CLR绑定的代码

AddComponent重定向

热更代码中写的Mono脚本想要发挥作用,就需要在运行时将自己挂载上去。此时就需要调用AddComponent方法,但是热更工程里面的类主工程并没有,这时就需要CLR重定向,重定向AddComponent方法,在添加热更工程的类时,添加我们上面的适配器。

ILRuntimeManager中,添加AddComponent方法

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
private unsafe StackObject* AddComponent(ILIntepreter __intp, StackObject* __esp, IList<object> __mStack, CLRMethod __method, bool isNewObj)
{
ILRuntime.Runtime.Enviorment.AppDomain __domain = __intp.AppDomain;//ILIntepreter即ILRuntime解释器
//因为GameObject.AddComponent<T>();在挟持了AddComponent方法的栈指针__esp之后,__esp - 1就是前面GameObject实例的指针
//这里使用ILIntepreter.Minus方法来实现(__esp - 1)的操作。
StackObject* ptr = ILIntepreter.Minus(__esp, 1);
//得到GameObject的实例,就是主工程内要添加Component的GameObject
GameObject instance = StackObject.ToObject(ptr,__domain, __mStack) as GameObject;
if (instance == null)
{
throw new System.NullReferenceException();
}
__intp.Free(ptr);//释放指针
var genericArguments = __method.GenericArguments;//获取AddComponent<T>具体添加的T类型
if (genericArguments != null && genericArguments.Length == 1)
{
var type = genericArguments[0];
object res;
if (type is CLRType)//如果这是Unity主工程的Mono脚本类
{
res = instance.AddComponent(type.TypeForCLR);
}
else
{
var ilInstance = new ILTypeInstance(type as ILType,false);//Unity不允许new一个MonoBehaviour,所以第二个参数是false
var clrInstance = instance.AddComponent<MonoBehaviourAdaptor.Adaptor>();//在Unity主工程内,使用Adaptor适配器来代替IL中的脚本类
clrInstance.ILInstance = ilInstance;
clrInstance.AppDomain = __domain;
//ilInstance默认创建的CLRInstance不是AddComponent出来的有效实例,而是属于HotFix的,所以要替换
ilInstance.CLRInstance = clrInstance;
res = clrInstance.ILInstance;
//在clrInstance = instance.AddComponent<MonoBehaviourAdaptor.Adaptor>()里面会自动调用Awake,但是当时还没有正确的ilInstance实例
//所以在这里重新调用Awake
clrInstance.Awake();
}
return ILIntepreter.PushObject(ptr, __mStack, res);//从调用栈中放回,修改之后放进去
}
return __esp;
}

GetComponent重定向

基本上和AddComponent同理,但是GetComponent在调用时不创建新的ILTypeInstance,而是需要遍历GameObject,找到符合要求的MonoBehaviourAdaptor

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
private unsafe StackObject* GetComponent(ILIntepreter __intp, StackObject* __esp, IList<object> __mStack, CLRMethod __method, bool isNewObj)
{
ILRuntime.Runtime.Enviorment.AppDomain __domain = __intp.AppDomain;
StackObject* ptr = ILIntepreter.Minus(__esp, 1);
GameObject instance = StackObject.ToObject(ptr,__domain,__mStack) as GameObject;
if (instance == null)
{
throw new System.NullReferenceException();
}
__intp.Free(ptr);
var genericArguments = __method.GenericArguments;
if (genericArguments != null && genericArguments.Length == 1)
{
var type = genericArguments[0];
object res = null;
if(type is CLRType)
{
res = instance.GetComponent(type.TypeForCLR);
}
else
{
var clrInstances = instance.GetComponents<MonoBehaviourAdaptor.Adaptor>();
foreach (var clrInstance in clrInstances)
{
if (clrInstance.ILInstance != null)
{
if (clrInstance.ILInstance.Type == type)
{
res = clrInstance.ILInstance; break;
}
}
}
}
return ILIntepreter.PushObject(ptr, __mStack, res);
}
return __esp;
}

重定向准备

在游戏开始前,需要在ILRuntime初始化时进行方法的重定向。

ILRuntimeManager里面添加SetUpCLRRedirection方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
unsafe void SetUpCLRRedirection()
{
var arr = typeof(GameObject).GetMethods();
foreach (var method in arr)
{
if (method.Name == "AddComponent" && method.GetGenericArguments().Length == 1)
{
m_AppDomain.RegisterCLRMethodRedirection(method, AddComponent);
}
if (method.Name == "GetComponent" && method.GetGenericArguments().Length == 1)
{
m_AppDomain.RegisterCLRMethodRedirection(method, GetComponent);
}
}
}

然后再在ILRuntimeManagerInitializeILRuntime方法内调用

1
2
3
4
5
6
7
8
void InitializeILRuntime()
{
//...
//CLR重定向,必须先重定向,才能再进行CLR绑定
SetUpCLRRedirection();
//CLR绑定初始化
ILRuntime.Runtime.Generated.CLRBindings.Initialize(m_AppDomain);
}

测试

在HotFix工程新建TestMono文件。

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
using UnityEngine;

namespace HotFix
{
public class TestMono
{
public static void RunTest(GameObject go)
{
go.AddComponent<MonoTest>();//测试AddComponent重定向
MonoTest monoTest = go.GetComponent<MonoTest>();//测试GetComponent重定向
monoTest.Test();
}

public class MonoTest : MonoBehaviour
{
private float m_CurTime = 0;

void Awake()
{
Debug.Log("MonoTest Awake!");
}
void Start()
{
Debug.Log("MonoTest Start");
}
void Update()
{
if (m_CurTime < 0.2f)
{
m_CurTime += Time.deltaTime;
Debug.Log("MonoTest Update");
}
}
public void Test()
{
Debug.Log("MonoTest TestMethod");
}
}
}
}

生成dll库,并在ILRuntimeManager中更新热更文件。最后运行便可查看