在ILRuntime中挟持主工程方法
CLR重定向
1.有些工程的方法和接口无法正常处理热更DLL里的类型和对象
2.CLR重定向可以对这些方法进行挟持
3.重定向方法需要理解IL底层机制,建议参考CLR绑定生成的代码
主工程:
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using ILRuntime.CLR.TypeSystem;
using ILRuntime.CLR.Method;
using ILRuntime.CLR.Utils;
using ILRuntime.Runtime.Intepreter;
using ILRuntime.Runtime.Stack;
using ILRuntime.Runtime.Enviorment;
//下面这行为了取消使用WWW的警告,Unity2018以后推荐使用UnityWebRequest,处于兼容性考虑Demo依然使用WWW
#pragma warning disable CS0618
public class CLRRedirectionDemo : MonoBehaviour
{
//AppDomain是ILRuntime的入口,最好是在一个单例类中保存,整个游戏全局就一个,这里为了示例方便,每个例子里面都单独做了一个
//大家在正式项目中请全局只创建一个AppDomain
AppDomain appdomain;
System.IO.MemoryStream fs;
System.IO.MemoryStream p;
void Start()
{
StartCoroutine(LoadHotFixAssembly());
}
IEnumerator LoadHotFixAssembly()
{
//首先实例化ILRuntime的AppDomain,AppDomain是一个应用程序域,每个AppDomain都是一个独立的沙盒
appdomain = new ILRuntime.Runtime.Enviorment.AppDomain();
//正常项目中应该是自行从其他地方下载dll,或者打包在AssetBundle中读取,平时开发以及为了演示方便直接从StreammingAssets中读取,
//正式发布的时候需要大家自行从其他地方读取dll
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
//这个DLL文件是直接编译HotFix_Project.sln生成的,已经在项目中设置好输出目录为StreamingAssets,在VS里直接编译即可生成到对应目录,无需手动拷贝
//工程目录在Assets\Samples\ILRuntime\1.6\Demo\HotFix_Project~
#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;
fs = new MemoryStream(dll);
p = new MemoryStream(pdb);
try
{
appdomain.LoadAssembly(fs, p, new ILRuntime.Mono.Cecil.Pdb.PdbReaderProvider());
}
catch
{
Debug.LogError("加载热更DLL失败,请确保已经通过VS打开Assets/Samples/ILRuntime/1.6/Demo/HotFix_Project/HotFix_Project.sln编译过热更DLL");
}
InitializeILRuntime();
OnHotFixLoaded();
}
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);
}
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);
}
//编写重定向方法对于刚接触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;
}
void Update()
{
}
private void OnDestroy()
{
if (fs != null)
fs.Close();
if (p != null)
p.Close();
fs = null;
p = null;
}
}
热更工程:
using System;
using System.Collections.Generic;
namespace HotFix_Project
{
public class TestCLRRedirection
{
public static void RunTest()
{
UnityEngine.Debug.Log("看看这行的详细Log信息");
}
}
}
CLR绑定
1.默认情况调用主工程接口会使用反射来调用
2.性能较低,且IL2CPP环境下容易被剪裁
3.ILRuntime提供了自动分析生成绑定的工具
4.每次出包前一定得记得生成CLR绑定
注意:
CRL绑定函数
appdomain.InitializeBindings();
要放在所有注册函数(如委托适配器,值类型绑定等等)之后
未绑定前效果:
可见执行时间为2002ms
绑定操作:
此时工程会多出一个文件夹 Generated
绑定后效果:
运行时间为305ms,效率快了好几倍
完整代码参考如下:
主工程:
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using ILRuntime.CLR.TypeSystem;
using ILRuntime.CLR.Method;
using ILRuntime.CLR.Utils;
using ILRuntime.Runtime.Intepreter;
using ILRuntime.Runtime.Stack;
using ILRuntime.Runtime.Enviorment;
#if UNITY_5_5_OR_NEWER
using UnityEngine.Profiling;
#endif
//下面这行为了取消使用WWW的警告,Unity2018以后推荐使用UnityWebRequest,处于兼容性考虑Demo依然使用WWW
#pragma warning disable CS0618
public class CLRBindingTestClass
{
public static float DoSomeTest(int a, float b)
{
return a + b;
}
}
public class CLRBindingDemo : MonoBehaviour
{
//AppDomain是ILRuntime的入口,最好是在一个单例类中保存,整个游戏全局就一个,这里为了示例方便,每个例子里面都单独做了一个
//大家在正式项目中请全局只创建一个AppDomain
AppDomain appdomain;
System.IO.MemoryStream fs;
System.IO.MemoryStream p;
void Start()
{
StartCoroutine(LoadHotFixAssembly());
}
IEnumerator LoadHotFixAssembly()
{
//首先实例化ILRuntime的AppDomain,AppDomain是一个应用程序域,每个AppDomain都是一个独立的沙盒
appdomain = new ILRuntime.Runtime.Enviorment.AppDomain();
//正常项目中应该是自行从其他地方下载dll,或者打包在AssetBundle中读取,平时开发以及为了演示方便直接从StreammingAssets中读取,
//正式发布的时候需要大家自行从其他地方读取dll
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
//这个DLL文件是直接编译HotFix_Project.sln生成的,已经在项目中设置好输出目录为StreamingAssets,在VS里直接编译即可生成到对应目录,无需手动拷贝
//工程目录在Assets\Samples\ILRuntime\1.6\Demo\HotFix_Project~
#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;
fs = new MemoryStream(dll);
p = new MemoryStream(pdb);
try
{
appdomain.LoadAssembly(fs, p, new ILRuntime.Mono.Cecil.Pdb.PdbReaderProvider());
}
catch
{
Debug.LogError("加载热更DLL失败,请确保已经通过VS打开Assets/Samples/ILRuntime/1.6/Demo/HotFix_Project/HotFix_Project.sln编译过热更DLL");
}
InitializeILRuntime();
OnHotFixLoaded();
}
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绑定请放在初始化的最后一步!!
//请在生成了绑定代码后解除下面这行的注释
//请在生成了绑定代码后解除下面这行的注释
//请在生成了绑定代码后解除下面这行的注释
appdomain.InitializeBindings();
//ILRuntime.Runtime.Generated.CLRBindings.Initialize(appdomain);
}
unsafe void OnHotFixLoaded()
{
ilruntimeReady = true;
}
bool ilruntimeReady = false;
bool executed = false;
void Update()
{
if (ilruntimeReady && !executed && Time.realtimeSinceStartup > 3)
{
executed = true;
//这里为了方便看Profiler,代码挪到Update中了
System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
Debug.LogWarning("运行这个Demo前请先点击菜单ILRuntime->Generate来生成所需的绑定代码,并按照提示解除下面相关代码的注释");
Debug.Log("默认情况下,从热更DLL里调用Unity主工程的方法,是通过反射的方式调用的,这个过程中会产生GC Alloc,并且执行效率会偏低");
Debug.Log("请在Unity菜单里面的ILRuntime->Generate CLR Binding Code by Analysis来生成绑定代码");
var type = appdomain.LoadedTypes["HotFix_Project.TestCLRBinding"];
var m = type.GetMethod("RunTest", 0);
Debug.Log("请解除InitializeILRuntime方法中的注释对比有无CLR绑定对运行耗时和GC开销的影响");
sw.Reset();
sw.Start();
Profiler.BeginSample("RunTest2");
appdomain.Invoke(m, null, null);
Profiler.EndSample();
sw.Stop();
Debug.LogFormat("刚刚的方法执行了:{0} ms", sw.ElapsedMilliseconds);
Debug.Log("可以看到运行时间和GC Alloc有大量的差别,RunTest2之所以有20字节的GC Alloc是因为Editor模式ILRuntime会有调试支持,正式发布(关闭Development Build)时这20字节也会随之消失");
}
}
void RunTest()
{
appdomain.Invoke("HotFix_Project.TestCLRBinding", "RunTest", null, null);
}
void RunTest2(IMethod m)
{
appdomain.Invoke(m, null, null);
}
private void OnDestroy()
{
if (fs != null)
fs.Close();
if (p != null)
p.Close();
fs = null;
p = null;
}
}
热更工程:
using System;
using System.Collections.Generic;
namespace HotFix_Project
{
public class TestCLRBinding
{
public static void RunTest()
{
for (int i = 0; i < 100000; i++)
{
CLRBindingTestClass.DoSomeTest(i, i);
}
}
}
}
注意 GenerateCLRBindingByAnalysis方法
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;
using System;
using System.Text;
using System.Collections.Generic;
using ILRuntimeDemo;
[System.Reflection.Obfuscation(Exclude = true)]
public class ILRuntimeCLRBinding
{
[MenuItem("ILRuntime/通过自动分析热更DLL生成CLR绑定")]
static void GenerateCLRBindingByAnalysis()
{
//用新的分析热更dll调用引用来生成绑定代码
ILRuntime.Runtime.Enviorment.AppDomain domain = new ILRuntime.Runtime.Enviorment.AppDomain();
using (System.IO.FileStream fs = new System.IO.FileStream("Assets/StreamingAssets/HotFix_Project.dll", System.IO.FileMode.Open, System.IO.FileAccess.Read))
{
domain.LoadAssembly(fs);
//Crossbind Adapter is needed to generate the correct binding code
InitILRuntime(domain);
ILRuntime.Runtime.CLRBinding.BindingCodeGenerator.GenerateBindingCode(domain, "Assets/Samples/ILRuntime/Generated");
}
AssetDatabase.Refresh();
}
[MenuItem("ILRuntime/删除所有CLR绑定")]
static void DeleteCLRBindins()
{
System.IO.Directory.Delete("Assets/Samples/ILRuntime/Generated", true);
AssetDatabase.Refresh();
}
static void InitILRuntime(ILRuntime.Runtime.Enviorment.AppDomain domain)
{
//这里需要注册所有热更DLL中用到的跨域继承Adapter,否则无法正确抓取引用
domain.RegisterCrossBindingAdaptor(new MonoBehaviourAdapter());
domain.RegisterCrossBindingAdaptor(new CoroutineAdapter());
domain.RegisterCrossBindingAdaptor(new TestClassBaseAdapter());
domain.RegisterValueTypeBinder(typeof(Vector3), new Vector3Binder());
domain.RegisterValueTypeBinder(typeof(Vector2), new Vector2Binder());
domain.RegisterValueTypeBinder(typeof(Quaternion), new QuaternionBinder());
}
}
#endif