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

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

本文轉(zhuǎn)載請注明出處: http://www.reibang.com/p/4bef7f66aefd

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

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

2.lua補(bǔ)丁代碼雛形

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方案, 大家各自根據(jù)項(xiàng)目需要自由選擇.

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

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代碼貼出并加上注釋, 大家根據(jù)具體情況自行使用Reflector獲取
# 代碼后附帶MSDN文檔鏈接
L_0000: ldstr "path/to/lua/file"    -- 壓入string參數(shù)
L_0005: ldstr "luaFuncName"
L_000a: call bool LuaPatch::HasPatch (string, string) -- 調(diào)用方法, 并指定參數(shù)形式
L_000f: brfalse L_0040              -- 相當(dāng)于 if(上述返回值為false) jump L_0040行
L_0014: ldstr "path/to/lua/file"    -- 同樣壓入?yún)?shù)
L_0019: ldstr "luaFuncName"
L_001e: ldc.i4.3                    -- 對應(yīng)params不定參數(shù), 需要根據(jù)具體不定參個數(shù)聲明對應(yīng)數(shù)組, 這里newarr object, 長度為3
L_001f: newarr object
L_0024: dup                         -- 復(fù)制棧頂(數(shù)組)的引用并壓入計算堆棧中
L_0025: ldc.i4.0                    -- 0下標(biāo)存放本函數(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#進(jìn)行代碼注入, 對于指令可以參考msdn的OpCodes文檔.

4.Mono.Ceil庫

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

2)使用特性進(jìn)行標(biāo)記

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

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

var assembly = AssemblyDefinition.ReadAssembly("path/to/Library/ScriptAssemblies/Assembly-CSharp.dll");
foreach (var type in assembly.MainModule.Types)
{
  // 判斷Attribute是否LuaInjector等等
}
  1. C#進(jìn)行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;
            // 壓入?yún)?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ù)據(jù)(不包括枚舉) 是不需要拆箱的
            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()返回的結(jié)果
            if (methodReturnVoid) current = InsertAfter(worker, current, worker.Create(OpCodes.Pop));
            // 方法有返回值時, 返回值進(jìn)行拆箱
            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, 并返回當(dāng)前語句
    /// </summary>
    private static Instruction InsertBefore(ILProcessor worker, Instruction target, Instruction instruction)
    {
        worker.InsertBefore(target, instruction);
        return instruction;
    }

    /// <summary>
    /// 語句后插入Instruction, 并返回當(dāng)前語句
    /// </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進(jìn)行標(biāo)記, 不過注意如果你的項(xiàng)目中有多個Scene需要打包, 這里避免重復(fù)調(diào)用, 需要添加一個_hasMidCodeInjectored用來標(biāo)記, 達(dá)到只在一個場景時機(jī)執(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打包會進(jìn)行IL2Cpp轉(zhuǎn)換為C++代碼, 所以此處對IL這一中間代碼(dll文件)的修改, 可以達(dá)成注入的目的. ?

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末修陡,一起剝皮案震驚了整個濱河市洋机,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌摧阅,老刑警劉巖帅掘,帶你破解...
    沈念sama閱讀 223,126評論 6 520
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件委煤,死亡現(xiàn)場離奇詭異,居然都是意外死亡修档,警方通過查閱死者的電腦和手機(jī)碧绞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,421評論 3 400
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來吱窝,“玉大人讥邻,你說我怎么就攤上這事≡合浚” “怎么了兴使?”我有些...
    開封第一講書人閱讀 169,941評論 0 366
  • 文/不壞的土叔 我叫張陵,是天一觀的道長照激。 經(jīng)常有香客問我发魄,道長,這世上最難降的妖魔是什么实抡? 我笑而不...
    開封第一講書人閱讀 60,294評論 1 300
  • 正文 為了忘掉前任欠母,我火速辦了婚禮,結(jié)果婚禮上吆寨,老公的妹妹穿的比我還像新娘赏淌。我一直安慰自己,他們只是感情好啄清,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,295評論 6 398
  • 文/花漫 我一把揭開白布六水。 她就那樣靜靜地躺著,像睡著了一般辣卒。 火紅的嫁衣襯著肌膚如雪掷贾。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,874評論 1 314
  • 那天荣茫,我揣著相機(jī)與錄音想帅,去河邊找鬼仙蚜。 笑死麦备,一個胖子當(dāng)著我的面吹牛腹缩,可吹牛的內(nèi)容都是我干的抄瑟。 我是一名探鬼主播,決...
    沈念sama閱讀 41,285評論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼浅缸,長吁一口氣:“原來是場噩夢啊……” “哼轨帜!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起衩椒,我...
    開封第一講書人閱讀 40,249評論 0 277
  • 序言:老撾萬榮一對情侶失蹤蚌父,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后毛萌,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體苟弛,經(jīng)...
    沈念sama閱讀 46,760評論 1 321
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,840評論 3 343
  • 正文 我和宋清朗相戀三年朝聋,在試婚紗的時候發(fā)現(xiàn)自己被綠了嗡午。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片囤躁。...
    茶點(diǎn)故事閱讀 40,973評論 1 354
  • 序言:一個原本活蹦亂跳的男人離奇死亡冀痕,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出狸演,到底是詐尸還是另有隱情言蛇,我是刑警寧澤,帶...
    沈念sama閱讀 36,631評論 5 351
  • 正文 年R本政府宣布宵距,位于F島的核電站腊尚,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏满哪。R本人自食惡果不足惜婿斥,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,315評論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望哨鸭。 院中可真熱鬧民宿,春花似錦、人聲如沸像鸡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,797評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽只估。三九已至志群,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蛔钙,已是汗流浹背锌云。 一陣腳步聲響...
    開封第一講書人閱讀 33,926評論 1 275
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留吁脱,地道東北人桑涎。 一個月前我還...
    沈念sama閱讀 49,431評論 3 379
  • 正文 我出身青樓子漩,卻偏偏與公主長得像,于是被迫代替她去往敵國和親石洗。 傳聞我的和親對象是個殘疾皇子幢泼,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,982評論 2 361

推薦閱讀更多精彩內(nèi)容

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