[Unity]基于IL代碼注入的Lua補丁方案

本分享的想法源于看了這篇分享
由于在對Unity項目后期進行l(wèi)ua熱更新方案實施, 我也不想造成源代碼的修改, 故在此對上文提及到的后續(xù)方案進行補充

本文轉載請注明出處: http://m.itdecent.cn/p/4bef7f66aefd

1.我為何有IL[1]代碼注入的想法

  • Unity項目如果初期沒有很好的規(guī)劃代碼熱更, 基本都會選擇C#作為開發(fā)語言, 那么項目后期引入lua機制, 把舊模塊用lua重寫并非很好的方案, 此時更希望是給舊代碼留一個lua熱更入口.
  • 為了減少重復代碼, 借鑒J2EE領域中AOP[2]實現(xiàn)思路, 應用到此次需求上.

2.lua補丁代碼雛形

public class FooBar
{
    public void Foo(string params1, int params2, Action params3)
    {
        if(LuaPatch.HasPatch("path/to/lua/file", "luaFuncName"))
        {
            LuaPatch.CallPatch("path/to/lua/file", "luaFuncName", params1, params2, params3);
            return;
        }
        // the old code here
        Debug.Log("這里是原來的邏輯代碼, 無返回值");
    }
    public Vector2 Bar(string params1, int params2, Action params3)
    {
        if (LuaPatch.HasPatch("path/to/lua/file", "luaFuncName"))
        {
            return (Vector2)LuaPatch.CallPatch("path/to/lua/file", "luaFuncName", params1, params2, params3);
        }
        // the old code here
        Debug.Log("這里是原來的邏輯代碼, 有返回值");
        return Vector2.one;
    }
}

至于是使用sLua或者toLua方案, 大家各自根據項目需要自由選擇.

https://github.com/pangweiwei/slua
https://github.com/topameng/tolua
如果沒有使用lua做大量向量,三角函數(shù)運算, 兩個方案沒有太大差異

3.初識IL

IL語法參考文章:http://www.cnblogs.com/Jax/archive/2009/05/29/1491523.html

上面LuaPatch判斷那一段先使用IL語法重新書寫
由于大家時間都很寶貴, 為了節(jié)省時間這里不精通IL語法也行, 這里有一個取巧的方法

  • 請自行下載利器: .NET Reflector
  • 我們使用Reflector打開Unity工程下\Library\ScriptAssemblies\Assembly-CSharp.dll
    找到你事先寫好的希望注入到代碼模板, 這里我以上面Foobar.cs為例
.NET Reflector
  • 篇幅限制, 我把核心的IL代碼貼出并加上注釋, 大家根據具體情況自行使用Reflector獲取
# 代碼后附帶MSDN文檔鏈接
L_0000: ldstr "path/to/lua/file"    -- 壓入string參數(shù)
L_0005: ldstr "luaFuncName"
L_000a: call bool LuaPatch::HasPatch (string, string) -- 調用方法, 并指定參數(shù)形式
L_000f: brfalse L_0040              -- 相當于 if(上述返回值為false) jump L_0040行
L_0014: ldstr "path/to/lua/file"    -- 同樣壓入參數(shù)
L_0019: ldstr "luaFuncName"
L_001e: ldc.i4.3                    -- 對應params不定參數(shù), 需要根據具體不定參個數(shù)聲明對應數(shù)組, 這里newarr object, 長度為3
L_001f: newarr object
L_0024: dup                         -- 復制棧頂(數(shù)組)的引用并壓入計算堆棧中
L_0025: ldc.i4.0                    -- 0下標存放本函數(shù)傳入第一個參數(shù)的引用
L_0026: ldarg.1                     -- #這里要注意static方法ldarg.0是第一個參數(shù), 非static的ldarg.0存放的是"this"
L_0027: stelem.ref                  -- 聲明上述傳入數(shù)組的參數(shù)為其對象的引用
L_0028: dup                         -- 作用同上一個dup
L_0029: ldc.i4.1                    
L_002a: ldarg.2
L_002b: box int32
L_0030: stelem.ref
L_0031: dup
L_0032: ldc.i4.2
L_0033: ldarg.3
L_0034: stelem.ref
L_0035: call object LuaPatch ::CallPatch (string, string, object[])
L_003a: unbox.any [UnityEngine]UnityEngine.Vector2
L_003f: ret

對IL語法有個大致理解, 有助于稍后用C#進行代碼注入, 對于指令可以參考msdn的OpCodes文檔.

4.Mono.Ceil庫

  1. 能夠標記需要注入的類或者方法
    利用C#的 特性(Attribute)
    1)聲明特性如下:
using System;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class LuaInjectorAttribute : Attribute
{
}
[AttributeUsage(AttributeTargets.Method)]
public class LuaInjectorIgnoreAttribute : Attribute
{
}

2)使用特性進行標記

[LuaInjector]
public class CatDog
{
    public void Cat()
    {
        // 這個類所有函數(shù)都會被注入
    }
    [LuaInjectorIgnore]
    public static void Dog()
    {
        // 只有LuaInjectorIgnore標記的會被忽略
    }
}

上述作為實現(xiàn)參考, 當然你也可以對Namespace, cs代碼目錄進行遍歷, 或者通過代碼主動Add(Type targetType)等方式來進行注入標記.
3)遍歷dll中所有的類型

var assembly = AssemblyDefinition.ReadAssembly("path/to/Library/ScriptAssemblies/Assembly-CSharp.dll");
foreach (var type in assembly.MainModule.Types)
{
  // 判斷Attribute是否LuaInjector等等
}
  1. C#進行IL代碼注入的核心代碼
    // 代碼片段
    private static bool DoInjector(AssemblyDefinition assembly)
    {
        var modified = false;
        foreach (var type in assembly.MainModule.Types)
        {
            if (type.HasCustomAttribute<LuaInjectorAttribute>())
            {
                foreach (var method in type.Methods)
                {
                    if (method.HasCustomAttribute<LuaInjectorIgnoreAttribute>()) continue;

                    DoInjectMethod(assembly, method, type);
                    modified = true;
                }
            }
            else
            {
                foreach (var method in type.Methods)
                {
                    if (!method.HasCustomAttribute<LuaInjectorAttribute>()) continue;

                    DoInjectMethod(assembly, method, type);
                    modified = true;
                }
            }
        }
        return modified;
    }

    private static void DoInjectMethod(AssemblyDefinition assembly, MethodDefinition method, TypeDefinition type)
    {
        if (method.Name.Equals(".ctor") || !method.HasBody) return;

        var firstIns = method.Body.Instructions.First();
        var worker = method.Body.GetILProcessor();

        // bool result = LuaPatch.HasPatch(type.Name)
        var hasPatchRef = assembly.MainModule.Import(typeof(LuaPatch).GetMethod("HasPatch"));
        var current = InsertBefore(worker, firstIns, worker.Create(OpCodes.Ldstr, type.Name));
        current = InsertAfter(worker, current, worker.Create(OpCodes.Ldstr, method.Name));
        current = InsertAfter(worker, current, worker.Create(OpCodes.Call, hasPatchRef));

        // if(result == false) jump to the under code
        current = InsertAfter(worker, current, worker.Create(OpCodes.Brfalse, firstIns));

        // else LuaPatch.CallPatch(type.Name, method.Name, args)
        var callPatchMethod = typeof(LuaPatch).GetMethod("CallPatch");
        var callPatchRef = assembly.MainModule.Import(callPatchMethod);
        current = InsertAfter(worker, current, worker.Create(OpCodes.Ldstr, type.Name));
        current = InsertAfter(worker, current, worker.Create(OpCodes.Ldstr, method.Name));
        var paramsCount = method.Parameters.Count;
        // 創(chuàng)建 args參數(shù) object[] 集合
        current = InsertAfter(worker, current, worker.Create(OpCodes.Ldc_I4, paramsCount));
        current = InsertAfter(worker, current, worker.Create(OpCodes.Newarr, assembly.MainModule.Import(typeof(object))));
        for (int index = 0; index < paramsCount; index++)
        {
            var argIndex = method.IsStatic ? index : index + 1;
            // 壓入參數(shù)
            current = InsertAfter(worker, current, worker.Create(OpCodes.Dup));
            current = InsertAfter(worker, current, worker.Create(OpCodes.Ldc_I4, index));
            var paramType = method.Parameters[index].ParameterType;
            // 獲取參數(shù)類型定義, 用來區(qū)分是否枚舉類 [若你所使用的類型不在本assembly, 則此處需要遍歷其他assembly以取得TypeDefinition]
            var paramTypeDef = assembly.MainModule.GetType(paramType.FullName);
            // 這里很重要, 需要判斷出 值類型數(shù)據(不包括枚舉) 是不需要拆箱的
            if (paramType.IsValueType && (paramTypeDef == null || !paramTypeDef.IsEnum))
            {
                current = InsertAfter(worker, current, worker.Create(OpCodes.Ldarg, argIndex));
            }
            else
            {
                current = InsertAfter(worker, current, worker.Create(OpCodes.Ldarg, argIndex));
                current = InsertAfter(worker, current, worker.Create(OpCodes.Box, paramType));
            }
            current = InsertAfter(worker, current, worker.Create(OpCodes.Stelem_Ref));
        }
        current = InsertAfter(worker, current, worker.Create(OpCodes.Call, callPatchRef));
        var methodReturnVoid = method.ReturnType.FullName.Equals("System.Void");
        var patchCallReturnVoid = callPatchMethod.ReturnType.FullName.Equals("System.Void");
        // LuaPatch.CallPatch()有返回值時
        if (!patchCallReturnVoid)
        {
            // 方法無返回值, 則需先Pop出棧區(qū)中CallPatch()返回的結果
            if (methodReturnVoid) current = InsertAfter(worker, current, worker.Create(OpCodes.Pop));
            // 方法有返回值時, 返回值進行拆箱
            else current = InsertAfter(worker, current, worker.Create(OpCodes.Unbox_Any, method.ReturnType));
        }
        // return
        InsertAfter(worker, current, worker.Create(OpCodes.Ret));

        // 重新計算語句位置偏移值
        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();
        }
    }
  1. 能夠在Unity打包時自動執(zhí)行IL注入
    使用特性PostProcessScene進行標記, 不過注意如果你的項目中有多個Scene需要打包, 這里避免重復調用, 需要添加一個_hasMidCodeInjectored用來標記, 達到只在一個場景時機執(zhí)行注入處理.
    // 代碼片段
    [PostProcessScene]
    private static void MidCodeInjectoring()
    {
        if (_hasMidCodeInjectored) return;
        D.Log("PostProcessBuild::OnPostProcessScene");

        // Don't CodeInjector when in Editor and pressing Play
        if (Application.isPlaying || EditorApplication.isPlaying) return;
        //if (!EditorApplication.isCompiling) return;

        BuildTarget buildTarget = EditorUserBuildSettings.activeBuildTarget;

        if (buildTarget == BuildTarget.Android)
        {
            if (DoCodeInjectorBuild("Android"))
            {
                _hasMidCodeInjectored = true;
            }
            else
            {
                D.LogWarning("CodeInjector: Failed to inject Android build!");
            }
        }
        else if (buildTarget == BuildTarget.iPhone)
        {
            if (DoCodeInjectorBuild("iOS"))
            {
                _hasMidCodeInjectored = true;
            }
            else
            {
                D.LogWarning("CodeInjector: Failed to inject iOS build!");
            }
        }
    }

4.完整源碼
https://github.com/rayosu/UnityDllInjector


  1. Unity中不管使用C#還是其他語言, 都會編譯為IL代碼存放為dll形式, iOS打包會進行IL2Cpp轉換為C++代碼, 所以此處對IL這一中間代碼(dll文件)的修改, 可以達成注入的目的. ?

  2. IL代碼注入只是AOP的一種實現(xiàn)方案, AOP(面向切面編程)的思想源自GOF設計模式, 你可以理解為: 用橫向的思考角度, 來統(tǒng)一切入一類相同邏輯的某個"切面"(Aspect), 讓使用者(邏輯程序員)無需重復關注這個"橫向面"需要做的工作.這里的切面就是"判斷是否有對應Lua補丁" ?

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • Spring Cloud為開發(fā)人員提供了快速構建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,695評論 19 139
  • 上次我們翻譯了由Unity開發(fā)人員JOSH PETERSON所寫的、IL2CPP深入講解系列的第一期,現(xiàn)在第二期的...
    IndieACE閱讀 9,799評論 0 11
  • 男閨蜜突然發(fā)來一張圖片,問我能否看出照片里那個背影是誰。我一眼就看出了那是我初戀,可是我回過去的卻是:我又不認...
    大象ELE閱讀 522評論 0 0
  • 那天,我和她漫步在河邊,這是我們經常走的河岸,以前,她總是走的漫不經心,那天卻走的小心翼翼,她走的很慢,我不得不遷...
    半瘋___守正閱讀 309評論 2 1
  • 茶樹精油:市面上唯一添加T40-C4頂級澳洲樹精油配方的潤喉糖,能深層滋養(yǎng)、迅速舒緩喉嚨不適 維他命C:天然免疫系...
    a66268b2d356閱讀 485評論 0 0

友情鏈接更多精彩內容