一、什么是代码注入

C#程序编译生成中间代码IL,为了实现问题修复和一些通用功能扩展,通常所使用的就是代码注入。

xlua的热修复方案即采用了代码注入的方式,没有污染C#代码,也不需要提前埋点,十分方便。

再比如这篇文章讲述了使用代码注入来做一些工具,:自动注入代码统计每个函数的执行效率以及内存分配

方案

代码注入需要借助一个注入工具,就是Mono.Cecil开发包,它可以让我们对编译好的DLL程序集进行IL代码注入。

http://www.mono-project.com/docs/tools+libraries/libraries/Mono.Cecil/

当然,我们也可以在Unity安装目录下,通话在Unity\Editor\Data\Managed下找到Mono.Cecil.dll、Mono.Cecil.Mdb.dll、Mono.Cecil.Pdb.dll其放在Assets/Plugins/Cecil目录中;(Plugins文件夹是专门用来方式Native插件的,会被自动Build);

扩展(什么是DLL)

动态链接库是微软公司在windows操作系统中实现共享函数库概念的一种方式,所谓动态链接就是把一些经常会共享的代码制作成DLL文件,当可执行文件调用到DLL文件时,操作系统才会将DLL加载到内存中,也就是按需加载,能够减少内存浪费。

DLL的最初目的是为了节约应用程序所需的磁盘和内存空间,通过将许多应用共享的代码切分到同一个DLL中,在硬盘上存为一个文件,在内存中使用一个实例。

二、实际案例

使用Mono.Cecil实现IL代码注入

这个案例主要参考上面这位同学的,主要内容是写了两个用于获取最大值的方法,其中一个方法是错误的,然后通过代码注入进行修复。

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;

public class TestInjectAttribute : Attribute {
    
}

public class Normal {
    public static int Max(int a, int b) {
        Debug.LogFormat("a = {0}, b = {1}", a, b);
        return a > b ? a : b;
    }
}
[TestInject]
public class Inject {
    public static int Max(int a, int b) {
        return a;
    }
}
public class ILTest : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        Debug.LogFormat("Normal Max: {0}", Normal.Max(3, 5));
        Debug.LogFormat("Inject Max: {0}", Inject.Max(3, 5));
    }
}

 在实现代码注入中,尤其是在InjectMethod中真正做代码注入工作的地方,这里需要使用到CIL的中间语言指令,维基百科中给出了所有命令的列表,命令都是根据其含义进行的缩写,比较容易理解:维基百科-List of CIL instructions

同时如果需要了解CIL,了解C#代码和IL中间代码的转换,可以参考:维基百科-Common Intermediate Language

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using Mono.Cecil;
using Mono.Cecil.Cil;
using UnityEditor;
using System.Linq;

public static class InjectTool
{
    public const string AssemblyPath = "./Library/ScriptAssemblies/Assembly-CSharp.dll";
    [MenuItem("CustomTools/Inject")]
    public static void Inject() {
        Debug.Log("Inject Start");
        if (Application.isPlaying || EditorApplication.isCompiling) {
            Debug.Log("Failed: Play Mode or Is Compliling");
            return;
        }
        var readerParameters = new ReaderParameters { ReadSymbols = true };
        var assembly = AssemblyDefinition.ReadAssembly(AssemblyPath, readerParameters);
        if (assembly == null) {
            Debug.LogError(string.Format("Inject Load Assembly Failed: {0}", AssemblyPath));
            return;
        }
        try
        {
            var module = assembly.MainModule;
            foreach (var type in module.Types)
            {
                var needInjectAttr = typeof(TestInjectAttribute).FullName;
                bool needInject = type.CustomAttributes.Any(typeAttribute => typeAttribute.AttributeType.FullName == needInjectAttr);
                if (!needInject)
                {
                    continue;
                }
                foreach (var method in type.Methods)
                {
                    if (method.IsConstructor || method.IsGetter || method.IsSetter || !method.IsPublic)
                    {
                        continue;
                    }
                    InjectMethod(module, method);
                }
            }
            assembly.Write(AssemblyPath, new WriterParameters { WriteSymbols = true });

        }
        catch (Exception ex)
        {
            Debug.LogError(string.Format("InjectTool Inject failed: {0}", ex));
            throw;
        }
        finally {
            if (assembly.MainModule.SymbolReader != null) {
                Debug.Log("InjectTool inject SymbolReader.Dispose success");
                assembly.MainModule.SymbolReader.Dispose();
            }
        }
        Debug.Log("Inject End");
    }
    private static void InjectMethod(ModuleDefinition module, MethodDefinition method) {
        var objType = module.ImportReference(typeof(System.Object));
        var intType = module.ImportReference(typeof(System.Int32));
        var logFormatMethod = module.ImportReference(typeof(Debug).GetMethod("LogFormat", new[] {typeof(string), typeof(object[]) }));
        // 开始注入代码
        var insertPoint = method.Body.Instructions[0];
        var ilProcessor = method.Body.GetILProcessor();
        // 设置一些标签用于语句跳转
        var label1 = ilProcessor.Create(OpCodes.Ldarg_1);
        var label2 = ilProcessor.Create(OpCodes.Stloc_0);
        var label3 = ilProcessor.Create(OpCodes.Ldloc_0);

        ilProcessor.InsertBefore(insertPoint, ilProcessor.Create(OpCodes.Nop));
        ilProcessor.InsertBefore(insertPoint, ilProcessor.Create(OpCodes.Ldstr, "a = {0}, b = {1}"));
        ilProcessor.InsertBefore(insertPoint, ilProcessor.Create(OpCodes.Ldc_I4_2));
        ilProcessor.InsertBefore(insertPoint, ilProcessor.Create(OpCodes.Newarr, objType));
        ilProcessor.InsertBefore(insertPoint, ilProcessor.Create(OpCodes.Dup));
        ilProcessor.InsertBefore(insertPoint, ilProcessor.Create(OpCodes.Ldc_I4_0));
        ilProcessor.InsertBefore(insertPoint, ilProcessor.Create(OpCodes.Ldarg_0));
        ilProcessor.InsertBefore(insertPoint, ilProcessor.Create(OpCodes.Box, intType));
        ilProcessor.InsertBefore(insertPoint, ilProcessor.Create(OpCodes.Stelem_Ref));
        ilProcessor.InsertBefore(insertPoint, ilProcessor.Create(OpCodes.Dup));
        ilProcessor.InsertBefore(insertPoint, ilProcessor.Create(OpCodes.Ldc_I4_1));
        ilProcessor.InsertBefore(insertPoint, ilProcessor.Create(OpCodes.Ldarg_1));
        ilProcessor.InsertBefore(insertPoint, ilProcessor.Create(OpCodes.Box, intType));
        ilProcessor.InsertBefore(insertPoint, ilProcessor.Create(OpCodes.Stelem_Ref));
        ilProcessor.InsertBefore(insertPoint, ilProcessor.Create(OpCodes.Call, logFormatMethod));
        ilProcessor.InsertBefore(insertPoint, ilProcessor.Create(OpCodes.Ldarg_0));
        ilProcessor.InsertBefore(insertPoint, ilProcessor.Create(OpCodes.Ldarg_1));
        ilProcessor.InsertBefore(insertPoint, ilProcessor.Create(OpCodes.Ble, label1));
        ilProcessor.InsertBefore(insertPoint, ilProcessor.Create(OpCodes.Ldarg_0));
        ilProcessor.InsertBefore(insertPoint, ilProcessor.Create(OpCodes.Br, label2));
        ilProcessor.InsertBefore(insertPoint, label1);
        ilProcessor.InsertBefore(insertPoint, label2);
        ilProcessor.InsertBefore(insertPoint, ilProcessor.Create(OpCodes.Br, label3));
        ilProcessor.InsertBefore(insertPoint, label3);
        ilProcessor.InsertBefore(insertPoint, ilProcessor.Create(OpCodes.Ret));
    }
}