[Unity3d杂记]InjectFix热修复c#

Posted old张

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[Unity3d杂记]InjectFix热修复c#相关的知识,希望对你有一定的参考价值。

目前项目中大部分逻辑还是用C#来实现的,关于线上版本修复C#的bug,之前一直采用xlua的hotfix来修复的,虽然能满足大部分修复需求,但是函数稍微长点,或者中间带有回调的各种嵌套,用lua来写修复代码变的非常困难,因此如果能编写C#代码生成patch文件,来热修复旧的C#代码,无疑是非常方便的方案,InjectFix正好解决了这个问题,本文主要介绍和记录游戏中使用InjectFix的流程和注意事项,


示例演示

  1. 在github下载源码工程,【https://github.com/Tencent/InjectFix】
  2. 编译IFix.exe(后续生成patch和对原始代码插桩都要用到)
    • Windows下打开Source\VSProj\build_for_unity.bat,将UNITY_HOME变量的值修改为指向本机unity安装目录,运行build_for_unity.bat;
    • Mac下打开Source\VSProj\build_for_unity.sh,将UNITY_HOME变量的值修改为指向本机unity安装目录,运行build_for_unity.sh;
    • 查看是否生成Source\UnityProj\IFixToolKit\IFix.exe;
  3. 用unity打开工程Source\UnityProj
  4. 打开场景Assets\Helloworld\Helloworld.unity,此时运行游戏是错误函数的逻辑
//Calc.cs
        public int Add(int a, int b)
        {
            return a * b;
        }

输出:10 + 9 = 90

  1. 修改代码,并添加patch标签
//Calc.cs
        [Patch]
        public int Add(int a, int b)
        {
            return a + b;
        }
  1. 点击菜单【InjectFix/Fix】,生成patch文件,注意默认生成在工程的根目录下,拷贝patch文件到Assets\Resources目录下,因为helloworld测试用的是Resources.Load
  2. 将5修改的代码还原为4的原始代码,待编译完成,此时游戏的代码应该还是错误的
  3. 点击菜单【InjectFix/Inject】,对代码插桩
  4. 启动游戏,查看结果,修复完成,输出:10 + 9 = 19

原理简介

  1. 查看官方手册【https://github.com/Tencent/InjectFix/blob/master/Doc/user_manual.md】

  2. inject插桩 与xlua一样,能被修复的C#代码需要提前插桩才行,通过[Configure]标记的Editor类,下的[IFix]标记的属性表示需要被插桩的类型列表,通过[IFix.Filter]可以过滤掉一些方法不被插桩,如下HelloworldCfg所示:

//1、配置类必须打[Configure]标签
//2、必须放Editor目录
[Configure]
public class HelloworldCfg
{
    [IFix]
    static IEnumerable<Type> hotfix
    {
        get
        {
            return new List<Type>()
            {
                typeof(Helloworld),
                typeof(IFix.Test.Calculator),
                //AnotherClass在Pro Standard Assets下,会编译到Assembly-CSharp-firstpass.dll下,用来演示多dll的修复
                typeof(AnotherClass),
            };
        }
    }

    [IFix.Filter]
    static bool Filter(System.Reflection.MethodInfo methodInfo)
    {
        return methodInfo.DeclaringType.FullName == "IFix.Test.Calculator" 
            && (methodInfo.Name == "Div" || methodInfo.Name == "Mult");
    }
}

执行菜单【InjectFix/Inject】,可以对代码插桩,我们通过dnspy查看插桩后的Assembly-CSharp.dll

namespace IFix.Test
{
 // Token: 0x02000002 RID: 2
 public class Calculator
 {
  // Token: 0x06000002 RID: 2 RVA: 0x00002058 File Offset: 0x00000258
  public int Add(int a, int b)
  {
   if (WrappersManagerImpl.IsPatched(0))
   {
    return WrappersManagerImpl.GetPatch(0).__Gen_Wrap_0(this, a, b);
   }
   return a * b;
  }

  // Token: 0x06000003 RID: 3 RVA: 0x0000208C File Offset: 0x0000028C
  public int Sub(int a, int b)
  {
   if (WrappersManagerImpl.IsPatched(1))
   {
    return WrappersManagerImpl.GetPatch(1).__Gen_Wrap_0(this, a, b);
   }
   return a / b;
  }

  // Token: 0x06000004 RID: 4 RVA: 0x000020C0 File Offset: 0x000002C0
  public int Mult(int a, int b)
  {
   return a * b;
  }

  // Token: 0x06000005 RID: 5 RVA: 0x000020D8 File Offset: 0x000002D8
  public int Div(int a, int b)
  {
   return a / b;
  }
 }
}

发现每个方法内都在初始位置加入了“if (WrappersManagerImpl.IsPatched(x))“,由此可见,通过patch修复的代码的流程为,加载patch文件,标记某个方法被patch,真正执行到这个方法时,if判断为true,然后跳转到patch函数的执行,从而达到替换旧函数的执行

  1. patch的修复生成
  • 注意前面提到只有被inject的方法,才是可以被修复的,所以要提前设置好需要修复的类型列表
  • 在要修复的函数上添加标签[IFix.Patch],然后修改代码,执行菜单【InjectFix/Fix】,即可生成代码的patch文件
  • 在修复代码中可以新增类或者方法,需添加[IFix.Interpret]标签,注意新增的类不能继承自原有的类,但可以继承通过[IFix.CustomBridge]标记的生成适配代码的接口 ,在patch代码中lamda或者函数赋值为原始代码中的delegate或者patch代码中新增的类转为原始代码中接口类型时,必须提前生成原始代码中类型适配代码,通过dnspy查看插桩后的Assembly-CSharp.dll

delegate的适配代码:

namespace IFix
{
 // Token: 0x0200000C RID: 12
 public class ILFixDynamicMethodWrapper
 {
  // Token: 0x06000029 RID: 41 RVA: 0x00002668 File Offset: 0x00000868
  public ILFixDynamicMethodWrapper(VirtualMachine virtualMachine, int methodId, object anonObj)
  {
   this.virtualMachine = virtualMachine;
   this.methodId = methodId;
   this.anonObj = anonObj;
  }

  // Token: 0x0600002A RID: 42 RVA: 0x00002688 File Offset: 0x00000888
  public int __Gen_Wrap_0(object P0, int P1, int P2)
  {
   Call call = Call.Begin();
   if (this.anonObj != null)
   {
    call.PushObject(this.anonObj);
   }
   call.PushObject(P0);
   call.PushInt32(P1);
   call.PushInt32(P2);
   this.virtualMachine.Execute(this.methodId, ref call, (this.anonObj != null) ? 4 : 3, 0);
   return call.GetInt32(0);
  }

  // Token: 0x0600002B RID: 43 RVA: 0x000026F4 File Offset: 0x000008F4
  public void __Gen_Wrap_1(string P0)
  {
   Call call = Call.Begin();
   if (this.anonObj != null)
   {
    call.PushObject(this.anonObj);
   }
   call.PushObject(P0);
   this.virtualMachine.Execute(this.methodId, ref call, (this.anonObj != null) ? 2 : 1, 0);
  }

  // Token: 0x0600002C RID: 44 RVA: 0x00002748 File Offset: 0x00000948
  public int __Gen_Wrap_2(int P0)
  {
   Call call = Call.Begin();
   if (this.anonObj != null)
   {
    call.PushObject(this.anonObj);
   }
   call.PushInt32(P0);
   this.virtualMachine.Execute(this.methodId, ref call, (this.anonObj != null) ? 2 : 1, 0);
   return call.GetInt32(0);
  }

  // Token: 0x0600002D RID: 45 RVA: 0x000027A4 File Offset: 0x000009A4
  public void __Gen_Wrap_3(object P0)
  {
   Call call = Call.Begin();
   if (this.anonObj != null)
   {
    call.PushObject(this.anonObj);
   }
   call.PushObject(P0);
   this.virtualMachine.Execute(this.methodId, ref call, (this.anonObj != null) ? 2 : 1, 0);
  }

  // Token: 0x04000007 RID: 7
  private VirtualMachine virtualMachine;

  // Token: 0x04000008 RID: 8
  private int methodId;

  // Token: 0x04000009 RID: 9
  private object anonObj;

  // Token: 0x0400000A RID: 10
  public static ILFixDynamicMethodWrapper[] wrapperArray = new ILFixDynamicMethodWrapper[0];
 }
}

接口的适配代码:

namespace IFix
{
 // Token: 0x0200000D RID: 13
 public class ILFixInterfaceBridge : AnonymousStorey, ISubSystem, IMonoBehaviour
 {
  // Token: 0x17000005 RID: 5
  // (get) Token: 0x0600002F RID: 47 RVA: 0x0000280C File Offset: 0x00000A0C
  bool ISubSystem.running
  {
   [IDTag(0)]
   get
   {
    Call call = Call.Begin();
    call.PushObject(this);
    this.virtualMachine.Execute(this.methodId_0, ref call, 1, 0);
    return call.GetBoolean(0);
   }
  }

  // Token: 0x06000030 RID: 48 RVA: 0x00002848 File Offset: 0x00000A48
  [IDTag(1)]
  void ISubSystem.Destroy()
  {
   Call call = Call.Begin();
   call.PushObject(this);
   this.virtualMachine.Execute(this.methodId_1, ref call, 1, 0);
  }

  // Token: 0x06000031 RID: 49 RVA: 0x0000287C File Offset: 0x00000A7C
  [IDTag(2)]
  void ISubSystem.Start()
  {
   Call call = Call.Begin();
   call.PushObject(this);
   this.virtualMachine.Execute(this.methodId_2, ref call, 1, 0);
  }

  // Token: 0x06000032 RID: 50 RVA: 0x000028B0 File Offset: 0x00000AB0
  [IDTag(3)]
  void ISubSystem.Stop()
  {
   Call call = Call.Begin();
   call.PushObject(this);
   this.virtualMachine.Execute(this.methodId_3, ref call, 1, 0);
  }

  // Token: 0x06000033 RID: 51 RVA: 0x000028E4 File Offset: 0x00000AE4
  [IDTag(4)]
  void IMonoBehaviour.Start()
  {
   Call call = Call.Begin();
   call.PushObject(this);
   this.virtualMachine.Execute(this.methodId_4, ref call, 1, 0);
  }

  // Token: 0x06000034 RID: 52 RVA: 0x00002918 File Offset: 0x00000B18
  [IDTag(5)]
  void IMonoBehaviour.Update()
  {
   Call call = Call.Begin();
   call.PushObject(this);
   this.virtualMachine.Execute(this.methodId_5, ref call, 1, 0);
  }

  // Token: 0x06000035 RID: 53 RVA: 0x0000294C File Offset: 0x00000B4C
  public ILFixInterfaceBridge(int fieldNum, int[] fieldTypes, int typeIndex, int[] vTable, int[] methodIdArray, VirtualMachine virtualMachine) : base(fieldNum, fieldTypes, typeIndex, vTable, virtualMachine)
  {
   if (methodIdArray.Length != 6)
   {
    throw new Exception("invalid length of methodId array");
   }
   this.methodId_0 = methodIdArray[0];
   this.methodId_1 = methodIdArray[1];
   this.methodId_2 = methodIdArray[2];
   this.methodId_3 = methodIdArray[3];
   this.methodId_4 = methodIdArray[4];
   this.methodId_5 = methodIdArray[5];
  }

  // Token: 0x0400000B RID: 11
  private int methodId_0;

  // Token: 0x0400000C RID: 12
  private int methodId_1;

  // Token: 0x0400000D RID: 13
  private int methodId_2;

  // Token: 0x0400000E RID: 14
  private int methodId_3;

  // Token: 0x0400000F RID: 15
  private int methodId_4;

  // Token: 0x04000010 RID: 16
  private int methodId_5;
 }
}
  1. patch的加载 在游戏启动时需要最先加载patch文件,加载完patch文件之后,代码修复才会生效,注意只有通过执行inject插桩后的代码,才能有效加载patch文件,如示例中
// Helloworld.cs

    void Start () {
        VirtualMachine.Info = (s) => UnityEngine.Debug.Log(s);
        //try to load patch for Assembly-CSharp.dll
        var patch = Resources.Load<TextAsset>("Assembly-CSharp.patch");
        if (patch != null)
        {
            UnityEngine.Debug.Log("loading Assembly-CSharp.patch ...");
            var sw = Stopwatch.StartNew();
            PatchManager.Load(new MemoryStream(patch.bytes));
            UnityEngine.Debug.Log("patch Assembly-CSharp.patch, using " + sw.ElapsedMilliseconds + " ms");
        }
        //try to load patch for Assembly-CSharp-firstpass.dll
        patch = Resources.Load<TextAsset>("Assembly-CSharp-firstpass.patch");
        if (patch != null)
        {
            UnityEngine.Debug.Log("loading Assembly-CSharp-firstpass ...");
            var sw = Stopwatch.StartNew();
            PatchManager.Load(new MemoryStream(patch.bytes));
            UnityEngine.Debug.Log("patch Assembly-CSharp-firstpass, using " + sw.ElapsedMilliseconds + " ms");
        }

        test();
    }

接入实战

  1. 删除官方的示例,只保留必要代码,只需要保留以下目录:
    • UnityProj\IFixToolKit
    • UnityProj\Assets\IFix
    • UnityProj\Assets\Plugins
  2. 自定义Config 当代码量很庞大时,一个个添加patch的类型,十分麻烦,而且可能漏掉,用反射获取所有的类型加以筛选是个比较不错的方法
[Configure]
public class ILFixCfg
{
    private static Assembly _mainAssembly = Assembly.Load("Assembly-CSharp");

    [IFix]
    static IEnumerable<Type> hotfix
    {
        get
        {
            var types = (from type in _mainAssembly.GetTypes()
                         where
                         (
                            type.Namespace != null &&
                            type.Namespace.StartsWith("Game") &&
                            !type.IsGenericType
                         )
                         select type
                         );

            return new List<Type>(types);
        }
    }

    [IFix.Filter]
    static bool Filter(System.Reflection.MethodInfo methodInfo)
    {
        return methodInfo.DeclaringType.FullName.Contains("Editor");
    }
}

如上,所有game命名空间下的所有类型,排除掉方法名中带有Editor的方法,都进行插桩。3. 自定义接口和委托的warp代码:

添加菜单"InjectFix/Generate Warp Code"来生成warp配置定义类型

    [MenuItem("InjectFix/Generate Warp Code")]
    static void GeneWarpCode()
    {
        var types = hotfix;

        HashSet<Type> typeSet = new HashSet<Type>();
        foreach (var type in types)
        {
            if(type.IsNestedPrivate)
            {
                continue;
            }
            if (typeof(Delegate).IsAssignableFrom(type))
            {
                typeSet.Add(type);
                continue;
            }
            if (type.IsInterface)
            {
                typeSet.Add(type);
            }
            var methods = type.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
            foreach (var method in methods)
            {
                if(method.IsGenericMethod)
                {
                    continue;
                }
                var parameters = method.GetParameters();
                foreach (var parameter in parameters)
                {
                    if (typeof(Delegate).IsAssignableFrom(parameter.ParameterType) && !parameter.ParameterType.IsByRef)
                    {
                        typeSet.Add(parameter.ParameterType);
                    }
                }
            }
        }

        string warpFormat = @"
using System;
using System.Collections.Generic;
namespace IFix.Bridge
{
    [IFix.CustomBridge]
    public static class ILFixWarpBridge
    {
        static List<Type> bridge = new List<Type>()
        {
${TypeList}
        };
    }
}"
;

        StringBuilder sb = new StringBuilder();
        foreach(var type in typeSet)
        {
            sb.AppendFormat("\t\t\ttypeof({0}),\n", TypeToString(type));
        }
        string filePath = Application.dataPath + "/Scripts/IFixCfg/ILFixWarpBridge.cs";
        File.WriteAllText(filePath, warpFormat.Replace( "${TypeList}", sb.ToString()));
    }


    static string TypeToString(Type type)
    {
        string typeStr = type.ToString();
        if (type.IsGenericType)
        {
            StringBuilder sb = new StringBuilder();
            Type defineType = type.GetGenericTypeDefinition();
            string defineStr = defineType.ToString();
            int index = defineStr.IndexOf("`");
            if (index != -1)
            {
                defineStr = defineStr.Substring(0, index);
            }
            sb.Append(defineStr);

            Type[] argTypes = type.GetGenericArguments();
            sb.Append("<");
            for (int i = 0; i < argTypes.Length; ++i)
            {
                if (i > 0)
                {
                    sb.Append(",");
                }
                sb.Append(TypeToString(argTypes[i]));
            }
            sb.Append(">");
            typeStr = sb.ToString();
        }
        
        if(type.IsNested)
        {
            typeStr = typeStr.Replace("+"".");
        }
        return typeStr;
    }

如上,遍历所有类型中的接口和委托,以及函数参数中引用的委托类型,生成CustomBridge代码 4. 修改patch输出路径

在ILFixEditor.cs中修改Patch函数,patch文件生成路径改为"Assets/StreamingAssets/patch"

        [MenuItem("InjectFix/Fix"false, 2)]
        public static void Patch()
        {
            EditorUtility.DisplayProgressBar("Generate Patch for Edtior""patching...", 0);
            try
            {
                string patchDir = "Assets/StreamingAssets/patch";
                if(!Directory.Exists(patchDir))
                {
                    Directory.CreateDirectory(patchDir);
                }
                foreach (var assembly in injectAssemblys)
                {
                    var assembly_path = string.Format("./Library/{0}/{1}.dll", GetScriptAssembliesFolder(), assembly);
                    GenPatch(assembly, assembly_path, "./Assets/Plugins/IFix.Core.dll",
                        string.Format("./{0}/{1}.patch.bytes",patchDir, assembly));
                }
            }
            catch (Exception e)
            {
                UnityEngine.Debug.LogError(e);
            }
            EditorUtility.ClearProgressBar();
        }
  1. 改善示例的测试流程,添加宏菜单切换旧代码与patch

用宏定义INJECTFIX_PATCH_ENABLE来,区分旧代码和patch的代码,宏定义生效时为patch代码,未定义时为原始旧代码,如下

#if !INJECTFIX_PATCH_ENABLE
        void TestPatch()
        {
            Debug.Log("Test");
        }
#else
        [IFix.Patch]
        void TestPatch()
        {
            Debug.Log("Test Patched!");
            TestAction((s) => { Debug.Log(s); });
            StartCoroutine(NewCol());
        }

        [IFix.Interpret]
        IEnumerator NewCol()
        {
            yield return null;
            Debug.Log("new col start");
            yield return null;
            Debug.Log("new col finish");
            yield return null;
        }
#endif

添加菜单"InjectFix/Patched"来切换宏

    [MenuItem("InjectFix/Patched")]
    static void InjectfixEnable()
    {
        BuildTargetGroup buildTargetGroup = GetCurBuildTarget();
        string symbolsStr = PlayerSettings.GetScriptingDefineSymbolsForGroup(buildTargetGroup);
        string[] symbols = symbolsStr.Split(';');
        HashSet<string> symbolSet = new HashSet<string>();
        for(int i = 0; i < symbols.Length; ++i)
        {
            if(!symbolSet.Contains(symbols[i]))
            {
                symbolSet.Add(symbols[i]);
            }
        }
        if(!symbolSet.Contains("INJECTFIX_PATCH_ENABLE"))
        {
            symbolSet.Add("INJECTFIX_PATCH_ENABLE");
        }
        else
        {
            symbolSet.Remove("INJECTFIX_PATCH_ENABLE");
        }
        StringBuilder sb = new StringBuilder();
        foreach(string s in symbolSet)
        {
            sb.Append(s + ";");
        }
        PlayerSettings.SetScriptingDefineSymbolsForGroup(buildTargetGroup, sb.ToString());
        AssetDatabase.SaveAssets();
        AssetDatabase.Refresh();
    }
  1. 整体的修复流程

    1:确定需要修复的C#方法

            int Add(int a, int b)
    {
    return a * b;
    }

    2:拷贝原有的方法,重写,用宏标记INJECTFIX_PATCH_ENABLE区分,宏开启时为新修复方法,宏关闭时为旧的待修复方法

    #if !INJECTFIX_PATCH_ENABLE
            int Add(int a, int b)
            {
                return a * b;
            }
    #else
            [IFix.Patch]
            int Add(int a, int b)
            {
                return a + b;
            }
    #endif
  2. 整体的测试流程

    1: 点击菜单【InjectFix/Patched】,开启宏INJECTFIX_PATCH_ENABLE,确保此时是修复代码生效

    2: 点击菜单【InjectFix/Fix】,生成patch文件,位于Assets\StreamingAssets\patch

    3: 点击菜单【InjectFix/Patched】,关闭宏INJECTFIX_PATCH_ENABLE,确保此时是还是待修复代码

    4: 点击菜单【InjectFix/Inject】,对旧代码进行插桩

    5: 启动游戏,加载patch文件后,验证修复生效


以上是关于[Unity3d杂记]InjectFix热修复c#的主要内容,如果未能解决你的问题,请参考以下文章

Unity3D游戏轻量级xlua热修复框架

unity3d c#支持热更吗

Unity3D热更新Unity3D 零成本高性能的C#的热更新框架:HybridCLR

Unity3D热更新Unity3D 零成本高性能的C#的热更新框架:HybridCLR

Unity3D热更新Unity3D 零成本高性能的C#的热更新框架:HybridCLR

unity3d c#热重载-边运行边改代码