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

[Unity]基于IL代码注入的Lua补丁方案_第1张图片

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

本文转载请注明出处: http://www.jianshu.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为例
[Unity]基于IL代码注入的Lua补丁方案_第2张图片
.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())
            {
                foreach (var method in type.Methods)
                {
                    if (method.HasCustomAttribute()) continue;

                    DoInjectMethod(assembly, method, type);
                    modified = true;
                }
            }
            else
            {
                foreach (var method in type.Methods)
                {
                    if (!method.HasCustomAttribute()) 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);
    }
    /// 
    /// 语句前插入Instruction, 并返回当前语句
    /// 
    private static Instruction InsertBefore(ILProcessor worker, Instruction target, Instruction instruction)
    {
        worker.InsertBefore(target, instruction);
        return instruction;
    }

    /// 
    /// 语句后插入Instruction, 并返回当前语句
    /// 
    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补丁" ↩

你可能感兴趣的:([Unity]基于IL代码注入的Lua补丁方案)