在Unity中利用Mono.Cecil将代码注入到Dll中

来源:互联网 发布:网络表格怎么加斜线 编辑:程序博客网 时间:2024/05/19 10:15

转载自我的小站:原文地址

通过Mono.Cecil我们可以通过Emit的方式将代码注入到已有的dll中,以实现AOP等高级功能。
Unity的代码在修改之后会自动编译到Library\ScriptAssemblies下的两个Assembly中,所以我会尝试着将代码注入到其中。

public class Test : MonoBehaviour{void Start(){   InjectMod();}void InjectMod () {   Debug.Log("Heihei asdasd");}}

将Test绑定到场景物体上,运行后我们会发现输出“Heihei asdasd”,就像我们预期的一样。
然后我们尝试着将代码注入到该函数中。

private static bool hasGen = false;[PostProcessBuild(1000)]private static void OnPostprocessBuildPlayer(BuildTarget buildTarget, string buildPath){   hasGen = false;}[PostProcessScene]public static void TestInjectMothodOnPost(){   if (hasGen == true) return;   hasGen = true;   TestInjectMothod();}[InitializeOnLoadMethod]public static void TestInjectMothod(){   var assembly = AssemblyDefinition.ReadAssembly(@"D:\Documents\Unity5Projects\UnityDllInjector\Library\ScriptAssemblies\Assembly-CSharp.dll");   var types = assembly.MainModule.GetTypes();   foreach(var type in types)   {      foreach(var Method in type.Methods)      {         if(Method.Name == "InjectMod")         {            InjectMethod(Method, assembly);         }      }   }   var writerParameters = new WriterParameters { WriteSymbols = true };   assembly.Write(@"D:\Documents\Unity5Projects\UnityDllInjector\Library\ScriptAssemblies\Assembly-CSharp.dll", new WriterParameters());}

我们首先看TestInjectMothod,这是我们在编辑器下进行注入的函数,这里我们需要注意的是,每当我们修改代码之后我们注入的结果会被覆盖掉,所以我们在每次修改代码之后都需要进行注入,所以我们这里添加了标签:InitializeOnLoadMethod
这个标签的意思是,当初始化的时候都进行执行,所以编译完成之后就会自动执行。

然后我们看前面两个函数,这两个函数是为了在打包时进行注入而存在的,其中hasGen是为了防止重复注入而定义的flag。

然后我们查看一下我们的注入方法:

private static void InjectMethod(MethodDefinition method, AssemblyDefinition assembly){   var firstIns = method.Body.Instructions.First();   var worker = method.Body.GetILProcessor();   //获取Debug.Log方法引用   var hasPatchRef = assembly.MainModule.Import(   typeof(Debug).GetMethod("Log", new Type[] { typeof(string) }));   //插入函数   var current = InsertBefore(worker, firstIns, worker.Create(OpCodes.Ldstr, "Inject"));   current = InsertBefore(worker, firstIns, worker.Create(OpCodes.Call, hasPatchRef));   //计算Offset   ComputeOffsets(method.Body);}

在这个函数中我们可以看到,我们首先将我们所需要的函数导入,然后插入到方法的最前端。

会用到的一些工具函数

/// <summary>/// 语句前插入Instruction, 并返回当前语句/// </summary>private static Instruction InsertBefore(ILProcessor worker, Instruction target, Instruction instruction){   worker.InsertBefore(target, instruction);   return instruction;}/// <summary>/// 语句后插入Instruction, 并返回当前语句/// </summary>private static Instruction InsertAfter(ILProcessor worker, Instruction target, Instruction instruction){   worker.InsertAfter(target, instruction);   return instruction;}//计算注入后的函数偏移值private static void ComputeOffsets(MethodBody body){   var offset = 0;   foreach (var instruction in body.Instructions)   {      instruction.Offset = offset;      offset += instruction.GetSize();   }}

等待编译完成,并且运行程序,我们发现在输出原来的语句之前多了一句“Inject”
可是我们在查看代码的时候并没有发生任何改变,这是因为我们只修改了dll而并非修改源代码。
实际代码

通过反编译软件ILSpy我们可以通过IL来反编译出我们的dll中的语句。

代码变为:

public class Test : MonoBehaviour{   private void Start()   {      this.InjectMod();   }   private void InjectMod()   {      Debug.Log("Inject");      Debug.Log("Heihei asdasd");   }}

反编译结果
注入成功,也达成了我们的目的。

这个东西到底有什么用呢?
之前在看知乎上的一篇文章,slua的作者分析了一下腾讯最近xlua的思路,也就是luapatch。
大概就是在每一个需要热补丁的函数前面加上一个[hotfix]就可以通过热更新lua代码来进行热补丁。
这是一种非侵入式的方法来为我们的框架添加额外的功能,这类似于AOP。
例如,原来我们的代码是

[hotfix]void Test(){   //DoSomething}

在注入之后就变成了

[hotfix]void Test(){   //如果存在热补丁   if(hasPatch()){      //加载luaPatch并且执行      return;   }   //DoSomething}

也就是通过lua完全替代了原本的函数。

如果是手动添加这些代码的话想必是一个不小的工作量,但是如果使用了我们以上所写的方式来做这个东西则会轻松非常多。

也就是不侵入代码的情况下自动注入我们想要的额外代码。

或许在不久的将来会出现不少基于此类的框架,就像Java中的Spring等等。虽然C#的代码生成方式比起Java来说要麻烦不少,但是也是可以做的!

在公司的IOS版本中,我也想加入这样的方法来进行框架的构筑,而并非热更新而已。

以上。

本文章参考了:
http://www.jianshu.com/p/481994e8b7df
https://www.zhihu.com/question/54344452/answer/138990189

感谢大大们的分享,让我这样的小透明也可以不断学习到新的技术。

顺便给大家拜个晚年吧!

0 0