Unity IL代碼注入(基礎)

一、什麼是代碼注入

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));
    }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章