[Unity3d杂记]InjectFix热修复c#
Posted old张
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[Unity3d杂记]InjectFix热修复c#相关的知识,希望对你有一定的参考价值。
目前项目中大部分逻辑还是用C#来实现的,关于线上版本修复C#的bug,之前一直采用xlua的hotfix来修复的,虽然能满足大部分修复需求,但是函数稍微长点,或者中间带有回调的各种嵌套,用lua来写修复代码变的非常困难,因此如果能编写C#代码生成patch文件,来热修复旧的C#代码,无疑是非常方便的方案,InjectFix正好解决了这个问题,本文主要介绍和记录游戏中使用InjectFix的流程和注意事项,
示例演示
-
在github下载源码工程,【https://github.com/Tencent/InjectFix】 -
编译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; -
用unity打开工程Source\UnityProj -
打开场景Assets\Helloworld\Helloworld.unity,此时运行游戏是错误函数的逻辑
//Calc.cs
public int Add(int a, int b)
{
return a * b;
}
输出:10 + 9 = 90
-
修改代码,并添加patch标签
//Calc.cs
[Patch]
public int Add(int a, int b)
{
return a + b;
}
-
点击菜单【InjectFix/Fix】,生成patch文件,注意默认生成在工程的根目录下,拷贝patch文件到Assets\Resources目录下,因为helloworld测试用的是Resources.Load -
将5修改的代码还原为4的原始代码,待编译完成,此时游戏的代码应该还是错误的 -
点击菜单【InjectFix/Inject】,对代码插桩 -
启动游戏,查看结果,修复完成,输出:10 + 9 = 19
原理简介
-
查看官方手册【https://github.com/Tencent/InjectFix/blob/master/Doc/user_manual.md】
-
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函数的执行,从而达到替换旧函数的执行
-
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;
}
}
-
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();
}
接入实战
-
删除官方的示例,只保留必要代码,只需要保留以下目录: -
UnityProj\IFixToolKit -
UnityProj\Assets\IFix -
UnityProj\Assets\Plugins -
自定义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();
}
-
改善示例的测试流程,添加宏菜单切换旧代码与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:确定需要修复的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 -
整体的测试流程
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热更新Unity3D 零成本高性能的C#的热更新框架:HybridCLR
Unity3D热更新Unity3D 零成本高性能的C#的热更新框架:HybridCLR