转载自我的小站:原文地址
通过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
感谢大大们的分享,让我这样的小透明也可以不断学习到新的技术。
顺便给大家拜个晚年吧!