使用Ilruntime热更新

软件开发大郭
0 评论
/
16 阅读
/
34363 字
21 2022-11
分类:

ILRuntime使用讲解快速入门
ILRuntime的作用
为什么要用到ILRuntime
ILRuntime的实现原理
ILRuntime使用
环境部署
生成Unity.Model.dll文件和Unity.HotFix.dll文件
加载unityHotFix.dll和Unity.HotFix.pdb文件
为什么加载unityHotFix.dll和Unity.HotFix.pdb文件
开始加载
HotFixManager脚本
Load函数
HotFix文件夹Test脚本
现在开始使用
跨域访问函数
跨域加载成员变量
跨域委托
跨域继承适配
CLR重定向
运行各种的结果
大总结
ILRuntime的作用
用于unity游戏的热更新,其语言是由C#写的,所以很受unity工程师的喜爱,这样就不再用xlua脚本进行热更新。
ILRuntime官方讲解

为什么要用到ILRuntime
我们知道他的目的后,它主要是用来进行游戏的更新操作,但是更新的流程是玩家运行游戏查看使用的游戏版本和我们上传到服务器版本是否相同,不同则进行更新操作。所以这里我们就用到了程序之间的跨域使用。而ILRuntime就是充当中介的作用实现跨域操作。

ILRuntime的实现原理
ILRuntime借助Mono.Cecil库来读取DLL的PE信息,以及当中类型的所有信息,最终得到方法的IL汇编码,然后通过内置的IL解译执行虚拟机来执行DLL中的代码。

ILRuntime使用
环境部署
将项目设置更改如下:

.在Assetts中创建HotFix文件夹,Model文件夹两个文件夹,

导入资源(我上传的免费资源)下载下来导入
我们将link文件进行修改

<linker>

<assembly fullname="Unity.Model" preserve="all"/>
<assembly fullname="Unity.ThirdParty" preserve="all"/>
<assembly fullname="unityEngine" preserve="all"/>
<assembly fullname="System" preserve="all"/>

</linker>
1
2
3
4
5
6
将文件中的内容替换成该内容

在Modle文件夹创建Uunity.Model程序集

在HotFix文件夹创建Unity.HotFix程序集
在ThirdParty文件夹创建Unity.ThirdParty程序集…(名字自定义)
接下来对这三个程序集进行修改

Unity.Model程序集

Unity.HotFix程序集

Unity.ThirdParty程序集

很好,现在我们的环境搭建完毕。

生成Unity.Model.dll文件和Unity.HotFix.dll文件
我们在Model文件夹和HotFix文件夹中创建两个init脚本编译一下。

我们就可以在这里查看到我们这两个程序集了

加载unityHotFix.dll和Unity.HotFix.pdb文件
我们HotFix问价夹存放的都是我们需要用到热更新的代码,而Modle使我们初始化打包的脚本文件夹,简而言之,我们所有的代码大部分存放在HotFix,Mondle,ThirdParty文件夹中,除非有些脚本要放在Edtior问价夹中。因为这样我们可以很方便的管理脚本。

为什么加载unityHotFix.dll和Unity.HotFix.pdb文件
因为这是跨域加载,要读取热更新里面的脚本我们就要在主工程项目加载到这两个文件然后才能通过ILRuntime进行访问。

开始加载
我们首先创建一个Edtior文件夹,再创建BuildEdtior文件夹,再创建一个BuildHotFix脚本

脚本内容

using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;

//unity提供的特性 每次编译后都会执行 编辑器模式下
[InitializeOnLoad]
public class BuildHotFixEditor
{

private const string scriptAssembliesDir = "Library/ScriptAssemblies";//加载路径
private const string codeDir = "Assets/Res/Code/";//生成的dll和pdb文件夹存放的位置
private const string hotfixDll="Unity.HotFix.dll";//加载文件夹名称
private const string hotfixPdb = "Unity.HotFix.pdb";//加载文件夹名称
static BuildHotFixEditor()
{
    //编译后将原有的文件覆盖掉
    File.Copy(Path.Combine(scriptAssembliesDir,hotfixDll),
        Path.Combine(codeDir,hotfixDll+".bytes"),true);
    
    File.Copy(Path.Combine(scriptAssembliesDir,hotfixPdb),
        Path.Combine(codeDir,hotfixPdb+".bytes"),true);
    Debug.Log("复制hotfix文件成功");
}

}

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

这样子我们每次编译脚本后他都会自动重新修改刷新这两个文件夹的内容,保证实时性

HotFixManager脚本
下面的脚本很大我们耐心一点,不要一次性全部看完,我们慢慢来一步一步的讲解
在Model文件夹创建HotFixManager脚本,作为全局变量加载HotFix文件夹中脚本内容

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using ILRuntime.CLR.Method;
using ILRuntime.CLR.TypeSystem;
using ILRuntime.Mono.Cecil.Pdb;
using ILRuntime.Runtime.Intepreter;
using ILRuntime.Runtime.Stack;
using Unity.Model;
using UnityEditor;
using UnityEditor.Compilation;
using UnityEngine;
using UnityEngine.Events;
using AppDomain = ILRuntime.Runtime.Enviorment.AppDomain;

public class HotFixManager : MonoBehaviour
{

public GameObject code;//code预制体,它身上带有Unity.HotFix.dll和Unity.HotFix.pdb文件

private MemoryStream dllStream;
private MemoryStream pdbStream;

private AppDomain appDomain;//全局变量,建议使用时用单例模式将HotManager做成单例

private string namespaceName = "HotFix";//我们加载热更新脚本的命名空间名字
private string className = "Test";//加载类的名字
private string name = "";//加载文件总名称
// Start is called before the first frame update
void Start()
{
    name = namespaceName + "." + className;
    Load();
    热更新加载函数();
}

void Load()
{
    //1.获取带两个bytes文件
    CodeReference cr = code.GetComponent<CodeReference>();
    byte[] assBytes = cr.hotFixDll.bytes;
    byte[] pdbBytes = cr.hotFixPdb.bytes;
    
     

if ILRuntime

    //2.在ILRuntime模式下 把这两个文件加载到内存流里面
    dllStream = new MemoryStream(assBytes);
    pdbStream = new MemoryStream(pdbBytes);
    
    //3.构造AppDomain对象 通过它的LoadAssembly来进行加载
    appDomain = new AppDomain();
    appDomain.LoadAssembly(dllStream,pdbStream,new PdbReaderProvider());
    委托适配器();
    跨域继承适配器注册();
    CLR重定向方法();
    Debug.Log("ILRuntime模式加载成功");

else

   Assembly.Load(assBytes,pdbBytes);

endif

    
}

void 跨域继承适配器注册()
{
    appDomain.RegisterCrossBindingAdaptor(new UIBaseAdapter());
}

void 热更新加载函数()
{
    

    #region 1.加载静态无返回函数

    //第一个参数:命名空间.类名
    //第二个参数:方法名
    //第三个参数:类的实例(静态函数不用写实例,动态的要添加实例参数
    //第四个参数:参数类型
    appDomain.Invoke(name, "无参函数", null,null);
    appDomain.Invoke(name,"有一参数函数", null, "皮学渣");
    appDomain.Invoke(name, "有多参函数", null, new string[] {"皮学渣", "学渣皮"});
    appDomain.Invoke(name, "多个不同参数类型无返回函数", null, new object[] {"皮学渣", 211});

    #endregion

    #region 2.加载静态有返回函数
    
    object m1 = appDomain.Invoke(name, "无参函数有返回值", null, null);
    Debug.Log(m1);
    object m2 = appDomain.Invoke(name, "有一参数函数有返回", null, "皮学渣");
    Debug.Log(m2);
    object m3 = appDomain.Invoke(name, "有多参函数有返回", null, new string[]{"皮学渣","学渣皮"});
    Debug.Log(m3);
    object m4 = appDomain.Invoke(name, "多个不同参数类型无返回函数有返回", null, new object[]{"皮学渣",211985});
    Debug.Log(m4);

    #endregion

    #region 3.加载动态无返回函数

    InstantiateTest();

    #endregion
    
    #region 4.加载重载函数

    //1.第一种和上面记载的方法一样,指定方法名,实例,参数个数,具体实现就不再重写,自己照着上面就可以了
    //2.List<IType>泛型加载参数形式找到对应的函数
    IType type1 = appDomain.LoadedTypes[name];//这里将指定类的所有类型都加载出来
    Debug.Log(type1);
    IMethod method1 = type1.GetMethod("Log", 0);
    appDomain.Invoke(method1, null, null);//这个对应的无参Log函数
    
    IType type2 = appDomain.LoadedTypes[name];//这里将指定类的所有类型都加载出来
    List<IType> param2 = new List<IType>();
    param2.Add(appDomain.GetType(typeof(string)));//这里对应的函数参数什么类型,有几个都这样添加进来
    IMethod method2 = type2.GetMethod("Log", param2, null);
    appDomain.Invoke(method2, null, "皮学渣");
    
    IType type3 = appDomain.LoadedTypes[name];//这里将指定类的所有类型都加载出来
    List<IType> param3 = new List<IType>();
    param3.Add(appDomain.GetType(typeof(string)));
    param3.Add(appDomain.GetType(typeof(int)));
    IMethod method3 = type2.GetMethod("Log", param3, null);
    appDomain.Invoke(method3, null, new object[]{"皮学渣",211985});
    
    
    //该形式也可和前面的函数类型进行调用,只是那样直接调用的比较方便

endregion

    #region 5.加载成员变量

    调用变量成员();

    #endregion

    #region 6.加载泛型函数

    调用泛型();

    #endregion

    #region 7.加载委托

    调用委托();

    #endregion

    #region 跨域继承的调用

    调用继承类函数();

    #endregion

    #region CLR重定向
    
    #endregion
}

/// <summary>
/// 实例化类,从而达到调用动态函数
/// </summary>
void InstantiateTest()
{
    ILTypeInstance test= appDomain.Instantiate(name, null);
    appDomain.Invoke(name, "动态无参", test, null);
    appDomain.Invoke(name, "动态有一参函数", test, "皮学渣");
    object x1=appDomain.Invoke(name, "动态无参有返回值", test, null);
    Debug.Log(x1);
    object x2=appDomain.Invoke(name, "动态有一参函数有返回值", test, "皮学渣");
    Debug.Log(x2) ;
}

/// <summary>
/// 这个方法是调用热更新dll文件中的类的成员
/// 注意这里的调用热更新文件变量必须是属性
/// 采用get_Name获取和set_Name设置赋值
/// </summary>
void 调用变量成员()
{
    //通过实例化,我们去访问成员变量,但是对应的成员变量是字段属性,我们还是类似调用的方法一样才能得到变量
    //get_ID  set_ID都是我们在给变量设置为属性时自动生成的
    ILTypeInstance test = appDomain.Instantiate(name);
    
    int id1 = (int) appDomain.Invoke(name, "get_ID", test, null);
    Debug.Log(id1);

    appDomain.Invoke(name, "set_ID", test, 985211);
    int id2 = (int) appDomain.Invoke(name, "get_ID", test, null);
    Debug.Log(id2);
}

/// <summary>
/// 这个方法是调用热更新Dll文件中类的泛型方法,我们只需要给
/// 该方法声明类型即可使用 采用appDomain.GetType(type(string....)注册
/// </summary>
void 调用泛型()
{
    ILTypeInstance test = appDomain.Instantiate(name);
    appDomain.InvokeGenericMethod(name, "泛型函数",
        new IType[] {appDomain.GetType(typeof(string))}, test, "皮学渣");
}

void 调用委托()
{
    //注意:我们的ILRuntime只支持Action以及Func,delegate委托的使用
    //而在unity’中的委托调用是UnityAction,所以我们咋这里用ILRuntime无法直接调用该方法
    //所以我们这里就用到了我们的委托适配器,我们要在生成appDomain变量是进行注册委托适配器
    ILTypeInstance test = appDomain.Instantiate(name);
    appDomain.Invoke(name, "ButtonClick", test, null);
    appDomain.Invoke(name, "调用这些委托", test, null);
}

void 委托适配器()
{
    //普通委托注册
    appDomain.DelegateManager.RegisterMethodDelegate<int>();//这是给Action类型添加委托进行适配,<>里可以写任意个参数类型,要和dll里面的匹配
    appDomain.DelegateManager.RegisterFunctionDelegate<int,int>();//这是给Action类型添加委托进行适配,<>里可以写任意个参数类型,要和dll里面的匹配
    
    //这里是给非Action Func 委托类型进行注册
    appDomain.DelegateManager.RegisterDelegateConvertor<UnityAction>
    (
        (act) =>
        {
            return new UnityAction(()=>
                ((Action) act)()
                );
        }
        );
}

void 调用继承类函数()
{
    string pName = "HotFix.继承unity主程序中的类";
    UIBase uibase = appDomain.Instantiate<UIBase>(pName);
    int id = (int)appDomain.Invoke(pName, "get_MyID", uibase, null);
    Debug.Log(id);
    appDomain.Invoke(pName, "HandleEvent", uibase, 50);
    appDomain.Invoke(pName, "Open", uibase, "皮学渣");
}


unsafe void CLR重定向方法()
{
    MethodInfo method=  typeof(Debug).GetMethod("Log",new System.Type[] { typeof(object) });
    appDomain.RegisterCLRMethodRedirection(method, DLog);
}
public unsafe static StackObject* DLog(ILIntepreter __intp, StackObject* __esp, IList<object> __mStack, CLRMethod __method, bool isNewObj)
{
    ILRuntime.Runtime.Enviorment.AppDomain __domain = __intp.AppDomain;
    StackObject* ptr_of_this_method;
    //只有一个参数,所以返回指针就是当前栈指针ESP - 1
    StackObject* __ret = ILIntepreter.Minus(__esp, 1);
    //第一个参数为ESP -1, 第二个参数为ESP - 2,以此类推
    ptr_of_this_method = ILIntepreter.Minus(__esp, 1);
    //获取参数message的值
    object message = StackObject.ToObject(ptr_of_this_method, __domain, __mStack);
    //需要清理堆栈
    __intp.Free(ptr_of_this_method);
    //如果参数类型是基础类型,例如int,可以直接通过int param = ptr_of_this_method->Value获取值,
    //关于具体原理和其他基础类型如何获取,请参考ILRuntime实现原理的文档。

    //通过ILRuntime的Debug接口获取调用热更DLL的堆栈
    string stackTrace = __domain.DebugService.GetStackTrace(__intp);
    Debug.Log(string.Format("{0}\n----------------\n{1}", message, stackTrace));

    return __ret;
}

}

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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
Load函数
void Load()

{
    //1.获取带两个bytes文件
    CodeReference cr = code.GetComponent<CodeReference>();
    byte[] assBytes = cr.hotFixDll.bytes;
    byte[] pdbBytes = cr.hotFixPdb.bytes;
    
     

if ILRuntime

    //2.在ILRuntime模式下 把这两个文件加载到内存流里面
    dllStream = new MemoryStream(assBytes);
    pdbStream = new MemoryStream(pdbBytes);
    
    //3.构造AppDomain对象 通过它的LoadAssembly来进行加载
    appDomain = new AppDomain();
    appDomain.LoadAssembly(dllStream,pdbStream,new PdbReaderProvider());
    委托适配器();
    跨域继承适配器注册();
    CLR重定向方法();
    Debug.Log("ILRuntime模式加载成功");

else

   Assembly.Load(assBytes,pdbBytes);

endif

    
}

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
Load函数的作用就是进行初始化appDomain,它是我们ILRuntime的集成者,我们都是通过它来访问热更新文件。
而委托适配器();跨域继承适配器注册();CLR重定向方法();这三个函数是进行适配器注册,所以我们在个热更新项目中也是在初始化的时候将所有的适配器加载出来,适配器后面会讲。

HotFix文件夹Test脚本

脚本内容

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

namespace HotFix//这里就是我们要给访问的命名空间
{

public class Test
{
    #region 函数

    public Test()
    {
        Debug.Log("我是无参构造函数");
    }

    public Test(string text)
    {
        Debug.Log($"我是有参构造函数string text:   {text}");
    }
    
    public static void 无参函数()
    {
        Debug.Log("我是静态无参函数无返回函数");
    }

    public static void 有一参数函数(string text)
    {
        Debug.Log("我是静态有一个string参数函数无返回函数:   "+text);
    }

    public static void 有多参函数(string text1, string text2)
    {
        Debug.Log($"我是静态有多个参数无返回函数" +
                  $"string  text1:   {text1}   string text2:   {text2}");
    }

    public static void 多个不同参数类型无返回函数(string text, int m)
    {
        Debug.Log($"我是静态有多个不同参数类型无返回函数 " +
                  $"  string text:   {text}    int m:   {m}");
    }
    
    public static void Log()
    {
        Debug.Log("无参无返回Log函数");
    }

    public static void Log(string text)
    {
        Debug.Log($"有一个string参数无返回Log函数 text:   {text}");
    }

    public static void Log(string text, int m)
    {
        Debug.Log($"有一个string参数一个int参乎上无返回Log函数 text:   {text}      m:     {m}");
    }
    
    public static int 无参函数有返回值()
    {
        Debug.Log("我是静态无参函数有返回函数");
        return 1;
    }
    
    public static int 有一参数函数有返回(string text)
    {
        Debug.Log("我是静态有一个string参数函数有返回函数:   "+text);
        return 2; 
    }
    
    public static int 有多参函数有返回(string text1, string text2)
    {
        Debug.Log($"我是静态有多个参数有返回函数" +
                  $"string  text1:   {text1}   string text2:   {text2}");
        return 3;
    }

    public static int 多个不同参数类型无返回函数有返回(string text, int m)
    {
        Debug.Log($"我是静态有多个不同参数类型有返回函数 " +
                  $"  string text:   {text}    int m:   {m}");
        return 4;
    }
    
    public void 动态无参()
    {
        Debug.Log("我是动态无参无返回函数");
    }

    public void 动态有一参函数(string text)
    {
        Debug.Log("我是动态有一参数函数  string text:     "+text);
    }

    public int 动态无参有返回值()
    {
        Debug.Log("我是动态无参有返回函数");
        return 1;
    }
    
    public int 动态有一参函数有返回值(string text)
    {
        Debug.Log("我是动态有一参数有返回值函数  string text:     "+text);
        return 2;
    }

    #endregion

    #region 字段变量

    private int id=5000;

    public int ID
    {
        get { return id; }
        set { id = value; }
    }


    #endregion

    #region 泛型函数

    public void 泛型函数<T>(T t)
    {
        Debug.Log($"我是泛型函数参数是:{t}");
    }
    

    #endregion

    #region UnityAction委托调用

    public void ButtonClick()
    {
        Button button = GameObject.Find("Canvas/Test").GetComponent<Button>();
        button.onClick.AddListener(OnClike);//这里的注册添加的是UnityAction委托类型
    }

    private void OnClike()
    {
        Debug.Log("点击了Test按钮");
    }
    

    #endregion

    #region 其他委托delegate Func Action

    public delegate void Delegate委托();
    public Action<int> action委托;
    public Func<int, int> func委托;

    public void 注册delegate委托()
    {
        Debug.Log("使用了delegate委托");
    }
    public void 注册Action委托(int n)
    {
        Debug.Log($"使用了Action委托,参数数n:   {n}");
    }
    public int 注册Func委托(int m)
    {
        Debug.Log($"使用了func委托,参数数m:{m}");
        return m;
    }

    public void 调用这些委托()
    {
        //首先给这些委托进行注册
        Delegate委托 delegate委托 = 注册delegate委托;
        action委托 = 注册Action委托;
        func委托 = 注册Func委托;
        
        
        //调用这些委托
        delegate委托();
        action委托(985);
        int m = func委托(985211);
        Debug.Log(m);
    }


    #endregion
}

}

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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
现在开始使用
跨域访问函数
我们在Test脚本中定义了好多函数,我们现在只看我划分分函数部分,所谓什么类型的都包括了,所以看完一遍你就完全可以掌握,并且不会出现什么疑问
我们在HotFixManager中进行访问
这就看我们在HotFixManager中的热更新加载函数();

#region 1.加载静态无返回函数

    //第一个参数:命名空间.类名
    //第二个参数:方法名
    //第三个参数:类的实例(静态函数不用写实例,动态的要添加实例参数
    //第四个参数:参数类型
    appDomain.Invoke(name, "无参函数", null,null);
    appDomain.Invoke(name,"有一参数函数", null, "皮学渣");
    appDomain.Invoke(name, "有多参函数", null, new string[] {"皮学渣", "学渣皮"});
    appDomain.Invoke(name, "多个不同参数类型无返回函数", null, new object[] {"皮学渣", 211});//不同参数类型用object数组

    #endregion

    #region 2.加载静态有返回函数
    //返回值我们用object接受,当然我们可以将返回值进行转化成我们想要的类型
    // int m1 = (int)appDomain.Invoke(name, "无参函数有返回值", null, null);这样也可以
    object m1 = appDomain.Invoke(name, "无参函数有返回值", null, null);
    Debug.Log(m1);
    object m2 = appDomain.Invoke(name, "有一参数函数有返回", null, "皮学渣");
    Debug.Log(m2);
    object m3 = appDomain.Invoke(name, "有多参函数有返回", null, new string[]{"皮学渣","学渣皮"});
    Debug.Log(m3);
    object m4 = appDomain.Invoke(name, "多个不同参数类型无返回函数有返回", null, new object[]{"皮学渣",211985});
    Debug.Log(m4);

    #endregion

    #region 3.加载动态无返回函数

    InstantiateTest();

    #endregion
    
    #region 4.加载重载函数

    //1.第一种和上面记载的方法一样,指定方法名,实例,参数个数,具体实现就不再重写,自己照着上面就可以了
    //2.List<IType>泛型加载参数形式找到对应的函数
    IType type1 = appDomain.LoadedTypes[name];//这里将指定类的所有类型都加载出来
    Debug.Log(type1);
    IMethod method1 = type1.GetMethod("Log", 0);
    appDomain.Invoke(method1, null, null);//这个对应的无参Log函数
    
    IType type2 = appDomain.LoadedTypes[name];//这里将指定类的所有类型都加载出来
    List<IType> param2 = new List<IType>();
    param2.Add(appDomain.GetType(typeof(string)));//这里对应的函数参数什么类型,有几个都这样添加进来
    IMethod method2 = type2.GetMethod("Log", param2, null);
    appDomain.Invoke(method2, null, "皮学渣");
    
    IType type3 = appDomain.LoadedTypes[name];//这里将指定类的所有类型都加载出来
    List<IType> param3 = new List<IType>();
    param3.Add(appDomain.GetType(typeof(string)));
    param3.Add(appDomain.GetType(typeof(int)));
    IMethod method3 = type2.GetMethod("Log", param3, null);
    appDomain.Invoke(method3, null, new object[]{"皮学渣",211985});
    
    //该形式也可和前面的函数类型进行调用,只是那样直接调用的比较方便

endregion

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

/// <summary>
/// 实例化类,从而达到调用动态函数,动态函数字段都要有实例才能进行访问
/// </summary>
void InstantiateTest()
{
    ILTypeInstance test= appDomain.Instantiate(name, null);//生成实例,第一个参数名字是实例的类名,第二个是参数null代表是用无参构造函数进行实例化,有参数就是有参构造创建
    appDomain.Invoke(name, "动态无参", test, null);
    appDomain.Invoke(name, "动态有一参函数", test, "皮学渣");
    object x1=appDomain.Invoke(name, "动态无参有返回值", test, null);
    Debug.Log(x1);
    object x2=appDomain.Invoke(name, "动态有一参函数有返回值", test, "皮学渣");
    Debug.Log(x2) ;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
跨域加载成员变量
HotFix.Test脚本中

region 字段变量

    private int id=5000;

    public int ID//我们要给他一个属性才能进行访问
    {
        get { return id; }
        set { id = value; }
    }


    #endregion

1
2
3
4
5
6
7
8
9
10
11
12
Modle.HotFixManager脚本
热更新加载函数():

region 5.加载成员变量

    调用变量成员();

    #endregion

1
2
3
4
5

 /// <summary>
/// 这个方法是调用热更新dll文件中的类的成员
/// 注意这里的调用热更新文件变量必须是属性
/// 采用get_Name获取和set_Name设置赋值
/// </summary>
void 调用变量成员()
{
    //通过实例化,我们去访问成员变量,但是对应的成员变量是字段属性,我们还是类似调用的方法一样才能得到变量
    //get_ID  set_ID都是我们在给变量设置为属性时自动生成的
    ILTypeInstance test = appDomain.Instantiate(name);
    
    int id1 = (int) appDomain.Invoke(name, "get_ID", test, null);
    Debug.Log(id1);

    appDomain.Invoke(name, "set_ID", test, 985211);
    int id2 = (int) appDomain.Invoke(name, "get_ID", test, null);
    Debug.Log(id2);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
跨域委托
我们在场景创建一个button按钮,接下来我们通过HotFix.Test脚本获取button并注册点击事件

HotFix.Test

    #region UnityAction委托调用

    public void ButtonClick()
    {
        Button button = GameObject.Find("Canvas/Test").GetComponent<Button>();
        button.onClick.AddListener(OnClick);//这里的注册添加的是UnityAction委托类型
    }

    private void OnClick()
    {
        Debug.Log("点击了Test按钮");
    }
    

    #endregion
    #region 其他委托delegate Func Action

    public delegate void Delegate委托();
    public Action<int> action委托;
    public Func<int, int> func委托;

    public void 注册delegate委托()
    {
        Debug.Log("使用了delegate委托");
    }
    public void 注册Action委托(int n)
    {
        Debug.Log($"使用了Action委托,参数数n:   {n}");
    }
    public int 注册Func委托(int m)
    {
        Debug.Log($"使用了func委托,参数数m:{m}");
        return m;
    }

    public void 调用这些委托()
    {
        //首先给这些委托进行注册
        Delegate委托 delegate委托 = 注册delegate委托;
        action委托 = 注册Action委托;
        func委托 = 注册Func委托;
        
        
        //调用这些委托
        delegate委托();
        action委托(985);
        int m = func委托(985211);
        Debug.Log(m);
    }


    #endregion

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
如果我们直接向上面那样调用函数调用ButtonClick的话会进行报错。
报错的原因就是没有进行委托适配
button.onClick.AddListener(OnClick); 这一句参数OnClick是一个UnityAction类型委托,而ILRuntime不支持UnityAction,所以我们要进行委托注册,这样才能调用UnityAction类型委托
官方文档

HotFixManager

void 委托适配器()

{
    //普通委托注册
    appDomain.DelegateManager.RegisterMethodDelegate<int>();//这是给Action类型添加委托进行适配,<>里可以写任意个参数类型,要和dll里面的匹配
    appDomain.DelegateManager.RegisterFunctionDelegate<int,int>();//这是给Action类型添加委托进行适配,<>里可以写任意个参数类型,要和dll里面的匹配
    
    //这里是给非Action Func 委托类型进行注册
    appDomain.DelegateManager.RegisterDelegateConvertor<UnityAction>
    (
        (act) =>
        {
            return new UnityAction(()=>
                ((Action) act)()
                );
        }
        );
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
我们进行注册后,在load函数初始化的后面进行调用

void 调用委托()

{
    //注意:我们的ILRuntime只支持Action以及Func,delegate委托的使用
    //而在unity’中的委托调用是UnityAction,所以我们咋这里用ILRuntime无法直接调用该方法
    //所以我们这里就用到了我们的委托适配器,我们要在生成appDomain变量是进行注册委托适配器
    ILTypeInstance test = appDomain.Instantiate(name);
    appDomain.Invoke(name, "ButtonClick", test, null);
    appDomain.Invoke(name, "调用这些委托", test, null);
}

1
2
3
4
5
6
7
8
9
这个函数就是调用我们在Test脚本中的委托

跨域继承适配
当我们在热更新中的脚本要继承主工程中的类时,我们要对被继承的类进行适配,这样我们调用hotfix中派生类才能成功。

我们在Model文件夹创建一个抽象基类UIBase

UIase

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace Unity.Model
{

public abstract class UIBase
{
    public virtual int MyID
    {
        get { return 100; }
    }

    public virtual void Open(string text)
    {
        Debug.Log("UIBase中的Open方法");
    }

    public abstract void HandleEvent(int id);

}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
现在再Model问价中创建Adapter文件夹,在该文件夹创建UIBaseAdapter脚本

接下来我们要根据官方文档来进行适配器的编写

using ILRuntime.CLR.Method;
using ILRuntime.Runtime.Enviorment;
using ILRuntime.Runtime.Intepreter;
using System;
using System.Collections;
using System.Collections.Generic;
using Unity.Model;
using UnityEngine;

public class UIBaseAdapter : CrossBindingAdaptor//这是一个接口,我们要实现里面的方法即可
{

public override Type BaseCLRType
{
    get
    {
        return typeof(UIBase);//这里的参数填写基类名字(哪个类被热更新问价继承就填哪个)
    }
}

public override Type[] BaseCLRTypes => base.BaseCLRTypes;

public override Type AdaptorType
{
    get { return typeof(Adapter); }
}

public override object CreateCLRInstance(ILRuntime.Runtime.Enviorment.AppDomain appdomain, ILTypeInstance instance)
{
    return new Adapter(appdomain, instance);
}


class Adapter : UIBase, CrossBindingAdaptorType//继承基类,在继承这个接口,重写基类的方法,和实现接口方法,一定要有一个无参构造函数,下面的两个参数一个AppDomain和ILTyoeInstance的构造函数
{
    private ILRuntime.Runtime.Enviorment.AppDomain appdomain;
    private ILTypeInstance instance;

    public Adapter()
    {

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

    public ILTypeInstance ILInstance
    {
        get
        {
            return instance;
        }
    }
    //到这里我们都是按照上面的进行


    //下面开始进行适配方法和字段

    bool mGetMyIDGot = false;//定义一个标识未  标识是否已经缓存了热更里的方法get_xxx
    IMethod mGetMyID;//缓存获取到的方法
    bool isGetMyIDInvoking = false;//判断是否运行该方法
    public override int MyID
    {
        get
        {
            if(mGetMyIDGot==false)
            {
                //字段获取的话就是根据属性一样获取get__xxx方法
                mGetMyID = instance.Type.GetMethod("get_MyID", 0);
                mGetMyIDGot = true;
            }

            if(mGetMyID!=null&& isGetMyIDInvoking==false)
            {
                isGetMyIDInvoking = true;
                int m=(int)appdomain.Invoke(mGetMyID, instance, null);
                isGetMyIDInvoking=false;
                return m;
            }
            return base.MyID;    
        }
    }


    IMethod mHandleEvent;
    bool isHandleEventCalled = false;
    object[] parame1=new object[1];//参数列表
    public override void HandleEvent(int id)
    {
        if(mHandleEvent==null)
        {
            mHandleEvent = instance.Type.GetMethod("HandleEvent", 1);
        }
        if(mHandleEvent!=null)
        {
            parame1[0] = id;
            appdomain.Invoke(mHandleEvent, instance, parame1);
        }
    }


    bool mOpenGot=false;
    IMethod mOpen;
    bool isOpenCalled = false;
    object[] paream2=new object[1];
    public override void Open(string text)
    {
        if(mOpenGot==false)
        {
            mOpen = instance.Type.GetMethod("Open", 1);
        }
        if(mOpen!=null&&isOpenCalled==false)
        {
            isOpenCalled = true;
            paream2[0]=text;
            appdomain.Invoke(mOpen, instance, paream2);
            isOpenCalled=false;  
        }
        else
        {
            base.Open(text);
        }
    }
}

}

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
在 HotFixManager 脚本

void 跨域继承适配器注册()

{
    appDomain.RegisterCrossBindingAdaptor(new UIBaseAdapter());
}

1
2
3
4
在Load函数下面添加进行适配

现在我们添加了委托适配,跨域继承适配

HotFix文件夹创建 继承unity主程序中的类脚本

using System.Collections;
using System.Collections.Generic;
using Unity.Model;
using UnityEngine;

namespace HotFix
{

public class 继承unity主程序中的类 : UIBase
{
    public override int MyID => 985211;

    public override void HandleEvent(int id)
    {
        Debug.Log("现在是调用了重写的HandleEvent方法参数t是:" + id);
    }

    public override void Open(string text)
    {
        Debug.Log($"现在是调用了重写的Open方法参数text:{text}");
    }
}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
HotFixManager

void 调用继承类函数()

{
    string pName = "HotFix.继承unity主程序中的类";//命名空间.类名
    UIBase uibase = appDomain.Instantiate<UIBase>(pName);
    int id = (int)appDomain.Invoke(pName, "get_MyID", uibase, null);
    Debug.Log(id);
    appDomain.Invoke(pName, "HandleEvent", uibase, 50);
    appDomain.Invoke(pName, "Open", uibase, "皮学渣");
}

1
2
3
4
5
6
7
8
9
CLR重定向
我的理解重定向就是重新定义一个类和方法
接下来我们重定向Debug.Log方法

HotFixManager

unsafe void CLR重定向方法()

{
    //                         类名              方法名      参数列表
    MethodInfo method=  typeof(Debug).GetMethod("Log",new System.Type[] { typeof(object) });
    appDomain.RegisterCLRMethodRedirection(method, DLog);
}
public unsafe static StackObject* DLog(ILIntepreter __intp, StackObject* __esp, IList<object> __mStack, CLRMethod __method, bool isNewObj)
{
    ILRuntime.Runtime.Enviorment.AppDomain __domain = __intp.AppDomain;
    StackObject* ptr_of_this_method;
    //只有一个参数,所以返回指针就是当前栈指针ESP - 1
    StackObject* __ret = ILIntepreter.Minus(__esp, 1);
    //第一个参数为ESP -1, 第二个参数为ESP - 2,以此类推
    ptr_of_this_method = ILIntepreter.Minus(__esp, 1);
    //获取参数message的值
    object message = StackObject.ToObject(ptr_of_this_method, __domain, __mStack);
    //需要清理堆栈
    __intp.Free(ptr_of_this_method);
    //如果参数类型是基础类型,例如int,可以直接通过int param = ptr_of_this_method->Value获取值,
    //关于具体原理和其他基础类型如何获取,请参考ILRuntime实现原理的文档。

    //通过ILRuntime的Debug接口获取调用热更DLL的堆栈
    string stackTrace = __domain.DebugService.GetStackTrace(__intp);
    Debug.Log(string.Format("{0}\n----------------\n{1}", message, stackTrace));

    return __ret;
}

然后在Load函数是下面添加该函数

运行各种的结果

点击Button按钮

大总结
希望我这篇文章能够让你快速入门
————————————————
版权声明:本文为CSDN博主「皮学渣」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/m0_48554728/article/details/123977368

标签:
    暂无数据