UH4L9——CLR重定向和CLR绑定

CLR是什么

关于CLR是什么,详见:CLR —— 让应用程序在操作系统上运行

CLR(Common Language Runtime)公共语言运行时,它是.Net Framework的基础,
所有的.Net技术都是建立在此之上的,它帮助我们实现了跨语言和跨平台

它是一个在执行时管理代码的代码,提供内存管理、线程管理等等核心服务,
就好像一个小型的操作系统一样,所以形象的把它称为“.Net虚拟机”。
如果想要应用程序在目标操作系统上能够运行,就必须依靠.Net提供的CLR环境来支持

本章代码关键字

1
appDomain.RegisterCLRMethodRedirection()            //对反射出来的方法消息进行重定向,重定向到另外一个方法上,使得反射调用变为直接调用

CLR重定向和CLR绑定

CLR重定向主要是用于对热更工程中的一些方法进行"挟持",就是将原本的方法"挟持"了,重新实现方法里面的逻辑,达到 重定向的目的

说人话:我们可以通过CLR重定向,将某一个方法的执行定位到我们的自定义逻辑中,而不是执行原本的方法逻辑,有点类似重写

CLR绑定就是利用CLR重定向将原本需要反射调用的内容变为直接调用,可以帮助我们:

  1. 提升ILRuntime的性能
  2. 避免IL2CPP打包时裁剪我们需要用的内容

CLR绑定的作用和原理

默认情况下,ILRuntime热更工程调用Unity主工程相关内容都会通过反射来调用,这样有2个缺点:

  1. 性能较低,反射调用比直接调用效率低
  2. IL2CPP打包时容易被裁剪

因此ILRuntime提供了自动分析生成CLR绑定的工具,它的作用是:

  1. 可以提高性能,将反射调用变为了直接调用
  2. 避免IL2CPP裁剪有用内容

原理:CLR绑定,就是借助了ILRuntime的CLR重定向机制来实现的,
本质上就是将方法的反射调用重定向到了我们自定义的方法里面来

注意:每次我们打包发布工程之前都要记得生成CLR绑定

如何进行CLR绑定

  1. 打开 Samples\ILRuntime\2.1.0\Demo\Editor\ILRuntimeCLRBinding.cs 代码,
    InitILRuntime​ 函数中注册跨域继承相关的类 以及 其他内容

    image

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class ILRuntimeCLRBinding
    {
    static void InitILRuntime(ILRuntime.Runtime.Enviorment.AppDomain domain)
    {
    //这里需要注册所有热更DLL中用到的跨域继承Adapter,否则无法正确抓取引用
    domain.RegisterCrossBindingAdaptor(new MonoBehaviourAdapter());
    domain.RegisterCrossBindingAdaptor(new CoroutineAdapter());
    domain.RegisterCrossBindingAdaptor(new TestClassBaseAdapter());
    //在这里注册我们自定义的一些需要跨域继承的类
    domain.RegisterCrossBindingAdaptor(new ILRuntimeAdapter.Lesson11_TestAdapter());

    domain.RegisterValueTypeBinder(typeof(Vector3), new Vector3Binder());
    domain.RegisterValueTypeBinder(typeof(Vector2), new Vector2Binder());
    domain.RegisterValueTypeBinder(typeof(Quaternion), new QuaternionBinder());
    }
    }
  2. 点击 工具栏 ——> ILRuntime ——> 通过自动分析热更DLL生成CLR绑定

    image

    此时就可以在 ILRuntimeCLRBinding.cs​ 代码中设置的到处路径中看到生成的绑定代码

    image

  3. 在初始化处,添加:ILRuntime.Runtime.Generated.CLRBindings.Initialize(appDomain);

    注意

    如果在CLR绑定注册前进行了CLR重定向相关设置,为了保证自定义的重定向能够正常使用
    初始化CLR绑定一定要放在最后一步,这样就不会影响自己想要保留的重定向等初始化操作了

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //初始化ILRuntime相关的方法
    private void InitILRuntime()
    {
    //注册委托和委托转换器...
    //注册跨域继承类
    appDomain.RegisterCrossBindingAdaptor(new ILRuntimeAdapter.Lesson11_TestAdapter());
    //注册 CLR绑定相关信息
    ILRuntime.Runtime.Generated.CLRBindings.Initialize(appDomain);
    //初始化ILRuntime相关信息(目前只需要告诉ILRuntime主线程的线程ID,主要目的是能够在Unity的Profiler剖析器窗口中分析问题)
    appDomain.UnityMainThreadID = Thread.CurrentThread.ManagedThreadId;
    }

CLR绑定性能提升测试

我们在ILRuntime中去调用Unity中的方法,如果不进行CLR绑定,就是通过反射
如果绑定了,就是直接调用,我们可以通过测试函数来体现出性能的提升

测试函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Lesson13 : MonoBehaviour
{
void Start()
{
ILRuntimeMgr.Instance.StartILRuntime(() =>
{
ILRuntimeMgr.Instance.appDomain.Invoke("HotFix_Project.ILRuntimeMain", "Main", null, null);
});
}

public static int TestFun(int i, int j)
{
return i + j;
}
}

热更工程内先记录一次测试前时间,循环执行100000次该函数,然后将测试后时间减去测试前时间,得到消耗时间

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ILRuntimeMain
{
public static void Main()
{
//得到当前系统时间
System.DateTime currentTime = System.DateTime.Now;
for (int i = 0; i < 100000; i++)
{
Lesson13.TestFun(i, i);
}
Debug.Log("花费的时间:" + (System.DateTime.Now - currentTime).Milliseconds + "ms");
}
}

此时不进行CLR绑定,不对Lesson13​做CLR绑定,直接反射运行得到的耗时如下:

image

然后点击 ILRuntime ——> 通过自动分析热更DLL生成CLR绑定,让ILRuntime分析热更工程内使用了哪些类,然后生成绑定,再运行,耗时如下:

imageimage

image

可见,进行CLR绑定带来了至少十倍以上的性能提升,提升效果是极大的

自定义CLR重定向

我们以Debug.Log​举例,如果再ILRuntime工程中调用Debug.Log
我们是无法获取到热更工程中的脚本、行号相关信息的,我们可以通过CLR重定向的形式获取到信息后再打印

image

这时,我们可以自定义重定向的方法的逻辑,将显示热更工程中的脚本、行号相关信息的逻辑添加到重定向方法内

步骤:

  1. 仿照CLR绑定中自动生成的绑定代码,反射获取类中的函数,对函数进行CLR重定向,以Debug.Log​为例

    先获取Debug​类的类型消息,根据类型消息反射出Log​方法的方法消息,
    然后 将 Log​ 方法消息 和 重定向的方法 一起传入到 appDomain.RegisterCLRMethodRedirection​ 方法内,完成重定向

    对于重定向的方法,同样可以先复制CLR绑定中自动生成的重定向方法(将其中的AutoList​参数改为List<Object>​参数),之后再做修改

    注意:
    要使用CLR重定向时,需要在unsafe​​语句块中使用
    所以我们需要在定义重定向函数和使用重定向函数的地方加上unsafe​​

    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
    //初始化ILRuntime相关的方法
    private unsafe void InitILRuntime()
    {
    //注册委托和委托转换器...
    //注册跨域继承类
    appDomain.RegisterCrossBindingAdaptor(new ILRuntimeAdapter.Lesson11_TestAdapter());

    //CLR重定向内容,必须要写到CLR绑定之前!!!
    System.Type debugType = typeof(Debug);
    MethodInfo methodInfo = debugType.GetMethod("Log", new System.Type[] { typeof(object) });
    appDomain.RegisterCLRMethodRedirection(methodInfo, MyLog);

    //注册 CLR绑定相关信息
    ILRuntime.Runtime.Generated.CLRBindings.Initialize(appDomain);
    //初始化ILRuntime相关信息(目前只需要告诉ILRuntime主线程的线程ID,主要目的是能够在Unity的Profiler剖析器窗口中分析问题)
    appDomain.UnityMainThreadID = Thread.CurrentThread.ManagedThreadId;
    }

    private unsafe StackObject* MyLog(ILIntepreter __intp, StackObject* __esp, List<object> __mStack, CLRMethod __method, bool isNewObj)
    {
    ILRuntime.Runtime.Enviorment.AppDomain __domain = __intp.AppDomain;
    StackObject* ptr_of_this_method;
    StackObject* __ret = ILIntepreter.Minus(__esp, 1);

    ptr_of_this_method = ILIntepreter.Minus(__esp, 1);
    System.Object @message = (System.Object)typeof(System.Object).CheckCLRTypes(StackObject.ToObject(ptr_of_this_method, __domain, __mStack), (ILRuntime.CLR.Utils.Extensions.TypeFlags)0);
    __intp.Free(ptr_of_this_method);

    //获取对应的行号等相关消息
    var stackTrace = __domain.DebugService.GetStackTrace(__intp);

    UnityEngine.Debug.Log(@message + "\n" + stackTrace);

    return __ret;
    }
  2. 在重定向中调用Debug.Log​之前,获取DLL内的堆栈信息 最后拼接打印

    调用 __domain.DebugService.GetStackTrace(__intp)​ 并获取其返回值,然后拼接到重定向方法调用的Debug.Log​方法内

    关于重定向的原理,详见:UH4L21——重定向的书写规则

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    private unsafe StackObject* MyLog(ILIntepreter __intp, StackObject* __esp, List<object> __mStack, CLRMethod __method, bool isNewObj)
    {
    ILRuntime.Runtime.Enviorment.AppDomain __domain = __intp.AppDomain;
    StackObject* ptr_of_this_method;
    StackObject* __ret = ILIntepreter.Minus(__esp, 1);

    ptr_of_this_method = ILIntepreter.Minus(__esp, 1);
    System.Object @message = (System.Object)typeof(System.Object).CheckCLRTypes(StackObject.ToObject(ptr_of_this_method, __domain, __mStack), (ILRuntime.CLR.Utils.Extensions.TypeFlags)0);
    __intp.Free(ptr_of_this_method);

    //获取对应的行号等相关消息
    var stackTrace = __domain.DebugService.GetStackTrace(__intp);

    UnityEngine.Debug.Log(@message + "\n" + stackTrace);

    return __ret;
    }

此时执行方法并输出,就可以看到调用消息:

image