ILRuntime中的编程1

思考并回答以下问题:

本章涵盖:

虽然ILRuntime在加载的流程上和直接热更dll很像,但在一些代码的编写上还是和原生的C#代码不同。用C#原生的LoadAssembly,加载dll后,用法和我们原来写代码差别不大,但是ILRuntime就不一样了,今天我们来逐一看看代码中常用到的功能。

ILRuntime中的编程

使用ILRuntime的基础代码结构我们上次已经学习过,在这里再加深下印象。

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
using UnityEngine;
using System.Collections;
using System.IO;
using ILRuntime.Runtime.Enviorment;

public class ILRuntimeManager: MonoBehaviour
{
//AppDomain是ILRuntime的入口,正式项目中请全局只创建一个AppDomain,最好是在一个单例类中保存
AppDomain appdomain;

void Start()
{
StartCoroutine(LoadHotFixAssembly());
}

IEnumerator LoadHotFixAssembly()
{
//首先实例化ILRuntime的AppDomain,AppDomain是一个应用程序域,每个AppDomain都是一个独立的沙盒
appdomain = new AppDomain();
//正常项目中应该是自行从其他地方下载dll,或者打包在AssetBundle中读取,平时开发以及为了演示方便直接从StreammingAssets中读取,
//正式发布的时候需要大家自行从其他地方读取dll

//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
//这个DLL文件是直接编译HotFix_Project.sln生成的,已经在项目中设置好输出目录为StreamingAssets,在VS里直接编译即可生成到对应目录,无需手动拷贝
#if UNITY_ANDROID
WWW www = new WWW(Application.streamingAssetsPath + "/HotFix_Project.dll");
#else
WWW www = new WWW("file:///" + Application.streamingAssetsPath + "/HotFix_Project.dll");
#endif
while (!www.isDone)
yield return null;
if (!string.IsNullOrEmpty(www.error))
UnityEngine.Debug.LogError(www.error);
byte[] dll = www.bytes;
www.Dispose();

//PDB文件是调试数据库,如需要在日志中显示报错的行号,则必须提供PDB文件,不过由于会额外耗用内存,正式发布时请将PDB去掉,下面LoadAssembly的时候pdb传null即可
#if UNITY_ANDROID
www = new WWW(Application.streamingAssetsPath + "/HotFix_Project.pdb");
#else
www = new WWW("file:///" + Application.streamingAssetsPath + "/HotFix_Project.pdb");
#endif
while (!www.isDone)
yield return null;
if (!string.IsNullOrEmpty(www.error))
UnityEngine.Debug.LogError(www.error);
byte[] pdb = www.bytes;
using (MemoryStream fs = new MemoryStream(dll))
{
using (MemoryStream p = new MemoryStream(pdb))
{
appdomain.LoadAssembly(fs, p, new Mono.Cecil.Pdb.PdbReaderProvider());
}
}

InitializeILRuntime();
OnHotFixLoaded();
}

void InitializeILRuntime()
{
//这里做一些ILRuntime的初始化操作
}

void OnHotFixLoaded()
{
//执行热更dll中的代码
}

void Update()
{

}
}

代码中通过协程加载

我们要执行的热更代码会放在OnHotFixLoaded方法中。

下面示例代码中所用的热更的类为:

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
namespace HotFix_Project
{
public class InstanceClass
{
private int id;

public InstanceClass()
{
UnityEngine.Debug.Log("!!! InstanceClass::InstanceClass()");
this.id = 0;
}

public InstanceClass(int id)
{
UnityEngine.Debug.Log("!!! InstanceClass::InstanceClass() id = " + id);
this.id = id;
}

public int ID
{
get { return id; }
}

// static method
public static void StaticFunTest()
{
UnityEngine.Debug.Log("!!! InstanceClass.StaticFunTest()");
}

public static void StaticFunTest2(int a)
{
UnityEngine.Debug.Log("!!! InstanceClass.StaticFunTest2(), a=" + a);
}

public static void GenericMethod<T>(T a)
{
UnityEngine.Debug.Log("!!! InstanceClass.GenericMethod(), a=" + a);
}
}
}

调用静态方法
直接调用
调用方法的通用函数原型为:
appdomain.Invoke(“类名”, “方法名”, 对象引用, 参数列表);

当调用静态方法时,对象引用传null。

1
2
3
4
//调用无参数静态方法
appdomain.Invoke("HotFix_Project.InstanceClass", "StaticFunTest", null, null);
//调用带参数的静态方法
appdomain.Invoke("HotFix_Project.InstanceClass", "StaticFunTest2", null, 123);

先获取Method再调用
预先获得IMethod,可以减低每次调用查找方法耗用的时间。

下面是根据方法名称和参数个数获取并调用方法:

1
2
3
4
5
IType type = appdomain.LoadedTypes["HotFix_Project.InstanceClass"];
//根据方法名称和参数个数获取方法
IMethod method = type.GetMethod("StaticFunTest", 0);

appdomain.Invoke(method, null, null);

下面是根据根据方法名称和参数类型列表获取并调用方法:

1
2
3
4
5
6
7
IType intType = appdomain.GetType(typeof(int));
//参数类型列表
List<IType> paramList = new List<ILRuntime.CLR.TypeSystem.IType>();
paramList.Add(intType);
//根据方法名称和参数类型列表获取方法
method = type.GetMethod("StaticFunTest2", paramList, null);
appdomain.Invoke(method, null, 456);

类的相关操作
实例化
实例化热更里的类:

1
2
3
4
object obj = appdomain.Instantiate("HotFix_Project.InstanceClass", new object[] { 233 });
//第二种方式
IType type = appdomain.LoadedTypes["HotFix_Project.InstanceClass"];
object obj2 = ((ILType)type).Instantiate();

调用成员方法

1
2
3
4
int id = (int)appdomain.Invoke("HotFix_Project.InstanceClass", "get_ID", obj, null);
Debug.Log("!! HotFix_Project.InstanceClass.ID = " + id);
id = (int)appdomain.Invoke("HotFix_Project.InstanceClass", "get_ID", obj2, null);
Debug.Log("!! HotFix_Project.InstanceClass.ID = " + id);

调用泛型方法
直接调用:

1
2
3
IType stringType = appdomain.GetType(typeof(string));
IType[] genericArguments = new IType[] { stringType };
appdomain.InvokeGenericMethod("HotFix_Project.InstanceClass", "GenericMethod", genericArguments, null, "TestString");

获取泛型方法的IMethod:

1
2
3
4
5
List<IType> paramList = new List<ILRuntime.CLR.TypeSystem.IType>();
paramList.Add(intType);
genericArguments = new IType[] { intType };
method = type.GetMethod("GenericMethod", paramList, genericArguments);
appdomain.Invoke(method, null, 33333);

委托
如果只是在热更的DLL项目中使用委托,不需要进行额外操作,和通常的C#用法是一样的。

但是如果你要在Unity工程中使用Dll中的委托,就需要额外添加适配器或者转换器。

注意:一些编译器生成的代码也会将委托传出为外部使用,比如:Linq当中where xxxx == xxx,会需要将xxx == xxx这个作为lambda表达式传给Linq.Where这个外部方法使用,还有OrderBy()方法,原因相同

如果在运行时发现缺少注册某个指定类型的委托适配器或者转换器时,ILRuntime会抛出相应的异常,根据提示添加注册即可。

委托部分的全部代码可以参见Demo中的03_DelegateDemo场景。Demo地址:https://github.com/Ourpalm/ILRuntimeU3D/

完全在热更DLL内部使用的委托
完全在热更DLL内部使用的委托,调用时和调用静态方法类似,直接可用,不需要做任何额外处理

1
2
appdomain.Invoke("HotFix_Project.TestDelegate", "Initialize", null, null);
appdomain.Invoke("HotFix_Project.TestDelegate", "RunTest", null, null);

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

1
2
Cannot find Delegate Adapter for:HotFix_Project.TestDelegate.Method(Int32 a), Please add following code:
appdomain.DelegateManager.RegisterMethodDelegate<System.Int32>();

因为iOS的IL2CPP模式下,不能动态生成类型,为了避免出现不可预知的问题,ILRuntime没有通过反射的方式创建委托实例,因此需要手动进行一些注册。

首先需要注册委托适配器,刚刚的报错的错误提示中,有提示需要的注册代码:

1
2
3
4
5
6
7
8
// 下面这1行代码可以解决上面的报错
appdomain.DelegateManager.RegisterMethodDelegate<int>();

// 其他的委托类型举例:
//带返回值的委托的话需要用RegisterFunctionDelegate,返回类型为最后一个
appdomain.DelegateManager.RegisterFunctionDelegate<int, string>();
//Action<string> 的参数为一个string
appdomain.DelegateManager.RegisterMethodDelegate<string>();

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

1
2
3
4
5
6
7
8
9
Cannot find convertor for TestDelegateMethod
Please add following code:
appdomain.DelegateManager.RegisterDelegateConvertor<TestDelegateMethod>((act) =>
{
return new TestDelegateMethod((a) =>
{
((Action<System.Int32>)act)(a);
});
});

这个报错的原因是:ILRuntime内部是用Action和Func这两个系统内置的委托类型来创建实例的,所以其他的委托类型都需要写转换器

报错中的修复提示也非常具体,直接将那个代码拿过来就可以了,将Action或者Func转换成目标委托类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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);
});
});

UGUI的委托
UGUI中有一些事件,经常用到委托,一定要注意添加适配器和转换器,例如UnityAction,

1
2
3
4
5
6
7
8
appdomain.DelegateManager.RegisterMethodDelegate<float>();
appdomain.DelegateManager.RegisterDelegateConvertor<UnityEngine.Events.UnityAction<float>>((action) =>
{
return new UnityEngine.Events.UnityAction<float>((a) =>
{
((System.Action<float>)action)(a);
});
});

总结

ILRuntime中委托有一些技巧,也以节省大量的工作量:

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