C#Unity热更新

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

Spread the love

通过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

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

顺便给大家拜个晚年吧!

发表评论

电子邮件地址不会被公开。 必填项已用*标注