在ILRuntime中挟持主工程方法(CLR重定向)

1.有些主工程的方法和接口无法正常处理热更DLL里的类型和对象。
2.CLR重定向可以对这些方法进行挟持。
3.重定向方法需要理解IL底层机制,建议参考CLR绑定生成的代码。

1
2
3
4
5
6
7
8
9
10
    unsafe void InitializeILRuntime()
{
#if DEBUG && (UNITY_EDITOR || UNITY_ANDROID || UNITY_IPHONE)
//由于Unity的Profiler接口只允许在主线程使用,为了避免出异常,需要告诉ILRuntime主线程的线程ID才能正确将函数运行耗时报告给Profiler
appdomain.UnityMainThreadID = System.Threading.Thread.CurrentThread.ManagedThreadId;
#endif
//这里做一些ILRuntime的注册
var mi = typeof(Debug).GetMethod("Log", new System.Type[] { typeof(object) });
appdomain.RegisterCLRMethodRedirection(mi, Log_11);
}

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
 //编写重定向方法对于刚接触ILRuntime的朋友可能比较困难,比较简单的方式是通过CLR绑定生成绑定代码,然后在这个基础上改,比如下面这个代码是从UnityEngine_Debug_Binding里面复制来改的
//如何使用CLR绑定请看相关教程和文档
unsafe static StackObject* Log_11(ILIntepreter __intp, StackObject* __esp, IList<object> __mStack, CLRMethod __method, bool isNewObj)
{
//ILRuntime的调用约定为被调用者清理堆栈,因此执行这个函数后需要将参数从堆栈清理干净,并把返回值放在栈顶,具体请看ILRuntime实现原理文档
ILRuntime.Runtime.Enviorment.AppDomain __domain = __intp.AppDomain;
StackObject* ptr_of_this_method;
//这个是最后方法返回后esp栈指针的值,应该返回清理完参数并指向返回值,这里是只需要返回清理完参数的值即可
StackObject* __ret = ILIntepreter.Minus(__esp, 1);
//取Log方法的参数,如果有两个参数的话,第一个参数是esp - 2,第二个参数是esp -1, 因为Mono的bug,直接-2值会错误,所以要调用ILIntepreter.Minus
ptr_of_this_method = ILIntepreter.Minus(__esp, 1);

//这里是将栈指针上的值转换成object,如果是基础类型可直接通过ptr->Value和ptr->ValueLow访问到值,具体请看ILRuntime实现原理文档
object message = typeof(object).CheckCLRTypes(StackObject.ToObject(ptr_of_this_method, __domain, __mStack));
//所有非基础类型都得调用Free来释放托管堆栈
__intp.Free(ptr_of_this_method);

//在真实调用Debug.Log前,我们先获取DLL内的堆栈
var stacktrace = __domain.DebugService.GetStackTrace(__intp);

//我们在输出信息后面加上DLL堆栈
UnityEngine.Debug.Log(message + "\n" + stacktrace);

return __ret;
}

1
2
3
4
5
6
7
8
9
10
unsafe void OnHotFixLoaded()
{
Debug.Log("什么时候需要CLR重定向呢,当我们需要挟持原方法实现,添加一些热更DLL中的特殊处理的时候,就需要CLR重定向了");
Debug.Log("详细文档请参见Github主页的相关文档");
Debug.Log("CLR重定向对ILRuntime底层实现密切相关,因此要完全理解这个Demo,需要大家先看关于ILRuntime实现原理的Demo");
Debug.Log("下面介绍一个CLR重定向的典型用法,比如我们在DLL里调用Debug.Log,默认情况下是无法显示DLL内堆栈的");
Debug.Log("但是经过CLR重定向之后可以做到输出DLL内堆栈,接下来进行CLR重定向注册");
Debug.Log("请注释和解除InitializeILRuntime方法里的重定向注册,对比下一行日志的变化");
appdomain.Invoke("HotFix_Project.TestCLRRedirection", "RunTest", null, null);
}

提升热更调用主工程方法的性能

CLR绑定

1.默认情况调用主工程接口会使用反射来调用。
2.性能较低,且IL2CPP环境下容易被剪裁。
3.ILRuntime提供了自动分析生成绑定的工具。
4.每次出包前一定得记得生成CLR绑定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    void InitializeILRuntime()
{
#if DEBUG && (UNITY_EDITOR || UNITY_ANDROID || UNITY_IPHONE)
//由于Unity的Profiler接口只允许在主线程使用,为了避免出异常,需要告诉ILRuntime主线程的线程ID才能正确将函数运行耗时报告给Profiler
appdomain.UnityMainThreadID = System.Threading.Thread.CurrentThread.ManagedThreadId;
#endif
//这里做一些ILRuntime的注册,如委托适配器,值类型绑定等等


//初始化CLR绑定请放在初始化的最后一步!!
//初始化CLR绑定请放在初始化的最后一步!!
//初始化CLR绑定请放在初始化的最后一步!!

//请在生成了绑定代码后解除下面这行的注释
//请在生成了绑定代码后解除下面这行的注释
//请在生成了绑定代码后解除下面这行的注释
ILRuntime.Runtime.Generated.CLRBindings.Initialize(appdomain);
}

提升使用值类型的性能

1
2
3
4
5
6
7
8
9
10
11
    void InitializeILRuntime()
{
#if DEBUG && (UNITY_EDITOR || UNITY_ANDROID || UNITY_IPHONE)
//由于Unity的Profiler接口只允许在主线程使用,为了避免出异常,需要告诉ILRuntime主线程的线程ID才能正确将函数运行耗时报告给Profiler
appdomain.UnityMainThreadID = System.Threading.Thread.CurrentThread.ManagedThreadId;
#endif
//这里做一些ILRuntime的注册,这里我们注册值类型Binder,注释和解注下面的代码来对比性能差别
appdomain.RegisterValueTypeBinder(typeof(Vector3), new Vector3Binder());
appdomain.RegisterValueTypeBinder(typeof(Quaternion), new QuaternionBinder());
appdomain.RegisterValueTypeBinder(typeof(Vector2), new Vector2Binder());
}

在ILRuntime使用异步操作

协程和异步操作的使用

1.协程和异步都能在ILRuntime中使用。
2.编译器会自动生成继承系统接口的匿名类造成跨域继承。
3.使用跨域继承器适配器可以完成异步操作的适配。
4.避免在异步操作中使用foreach。

1
2
3
4
5
6
7
8
9
10
11
    void InitializeILRuntime()
{
#if DEBUG && (UNITY_EDITOR || UNITY_ANDROID || UNITY_IPHONE)
//由于Unity的Profiler接口只允许在主线程使用,为了避免出异常,需要告诉ILRuntime主线程的线程ID才能正确将函数运行耗时报告给Profiler
appdomain.UnityMainThreadID = System.Threading.Thread.CurrentThread.ManagedThreadId;
#endif
//这里做一些ILRuntime的注册
//使用Couroutine时,C#编译器会自动生成一个实现了IEnumerator,IEnumerator<object>,IDisposable接口的类,因为这是跨域继承,所以需要写CrossBindAdapter(详细请看04_Inheritance教程),Demo已经直接写好,直接注册即可
appdomain.RegisterCrossBindingAdaptor(new CoroutineAdapter());
appdomain.DebugService.StartDebugService(56000);
}