[Unity]基于IL代码注入的Lua补丁方案

本分享的想法源于看了这篇分享
由于在对Unity项目后期进行lua热更新方案实施, 我也不想造成源代码的修改, 故在此对上文提及到的后续方案进行补充

本文转载请注明出处: //www.greatytc.com/p/4bef7f66aefd

1.我为何有IL[1]代码注入的想法

  • Unity项目如果初期没有很好的规划代码热更, 基本都会选择C#作为开发语言, 那么项目后期引入lua机制, 把旧模块用lua重写并非很好的方案, 此时更希望是给旧代码留一个lua热更入口.
  • 为了减少重复代码, 借鉴J2EE领域中AOP[2]实现思路, 应用到此次需求上.

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做大量向量,三角函数运算, 两个方案没有太大差异

3.初识IL

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

上面LuaPatch判断那一段先使用IL语法重新书写
由于大家时间都很宝贵, 为了节省时间这里不精通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参数
L_0005: ldstr "luaFuncName"
L_000a: call bool LuaPatch::HasPatch (string, string) -- 调用方法, 并指定参数形式
L_000f: brfalse L_0040              -- 相当于 if(上述返回值为false) jump L_0040行
L_0014: ldstr "path/to/lua/file"    -- 同样压入参数
L_0019: ldstr "luaFuncName"
L_001e: ldc.i4.3                    -- 对应params不定参数, 需要根据具体不定参个数声明对应数组, 这里newarr object, 长度为3
L_001f: newarr object
L_0024: dup                         -- 复制栈顶(数组)的引用并压入计算堆栈中
L_0025: ldc.i4.0                    -- 0下标存放本函数传入第一个参数的引用
L_0026: ldarg.1                     -- #这里要注意static方法ldarg.0是第一个参数, 非static的ldarg.0存放的是"this"
L_0027: stelem.ref                  -- 声明上述传入数组的参数为其对象的引用
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()
    {
        // 这个类所有函数都会被注入
    }
    [LuaInjectorIgnore]
    public static void Dog()
    {
        // 只有LuaInjectorIgnore标记的会被忽略
    }
}

上述作为实现参考, 当然你也可以对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;
        // 创建 args参数 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;
            // 压入参数
            current = InsertAfter(worker, current, worker.Create(OpCodes.Dup));
            current = InsertAfter(worker, current, worker.Create(OpCodes.Ldc_I4, index));
            var paramType = method.Parameters[index].ParameterType;
            // 获取参数类型定义, 用来区分是否枚举类 [若你所使用的类型不在本assembly, 则此处需要遍历其他assembly以取得TypeDefinition]
            var paramTypeDef = assembly.MainModule.GetType(paramType.FullName);
            // 这里很重要, 需要判断出 值类型数据(不包括枚举) 是不需要拆箱的
            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出栈区中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打包时自动执行IL注入
    使用特性PostProcessScene进行标记, 不过注意如果你的项目中有多个Scene需要打包, 这里避免重复调用, 需要添加一个_hasMidCodeInjectored用来标记, 达到只在一个场景时机执行注入处理.
    // 代码片段
    [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的一种实现方案, AOP(面向切面编程)的思想源自GOF设计模式, 你可以理解为: 用横向的思考角度, 来统一切入一类相同逻辑的某个"切面"(Aspect), 让使用者(逻辑程序员)无需重复关注这个"横向面"需要做的工作.这里的切面就是"判断是否有对应Lua补丁"

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,651评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,468评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,931评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,218评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,234评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,198评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,084评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,926评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,341评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,563评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,731评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,430评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,036评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,676评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,829评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,743评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,629评论 2 354

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,652评论 18 139
  • 上次我们翻译了由Unity开发人员JOSH PETERSON所写的、IL2CPP深入讲解系列的第一期,现在第二期的...
    IndieACE阅读 9,501评论 0 11
  • 男闺蜜突然发来一张图片,问我能否看出照片里那个背影是谁。我一眼就看出了那是我初恋,可是我回过去的却是:我又不认...
    大象ELE阅读 466评论 0 0
  • 那天,我和她漫步在河边,这是我们经常走的河岸,以前,她总是走的漫不经心,那天却走的小心翼翼,她走的很慢,我不得不迁...
    半疯___守正阅读 252评论 2 1
  • 茶树精油:市面上唯一添加T40-C4顶级澳洲树精油配方的润喉糖,能深层滋养、迅速舒缓喉咙不适 维他命C:天然免疫系...
    a66268b2d356阅读 444评论 0 0