InjectFix是一个基于Unity的C#代码热修复的实现方案(支持IL2CPP)。和XLua热更方案是同一位作者。
与各类lua解决方案和ILRutime等传统主流实现方式相比,最大区别在于InjectFix在正常情况下是执行原生代码,打补丁后需要修复的部分才局部重定向到IL虚拟机解释执行。(ILRutime是全程解释执行C#编译后的dll,Lua本身就是解释型语言)具有以下优缺点:
InjectFix的功能特性如下(摘抄自源码文档)
标签 | 使用阶段 | 用途 | 用法 |
---|---|---|---|
[IFix.Patch] | 补丁 | 修复函数 | 只能放在函数上 |
[IFix.Interpret] | 补丁 | 新增属性,函数,类型 | 放在属性,函数,类型上 |
[IFix.CustomBridge] | 注入 | interface和delegate桥接 | 只能放在单独写一个静态类上,存储虚拟机的类适配到原生interface或者虚拟机的函数适配到原生delegate,该类不能放Editor目录 |
[Configure] | 注入 | 配置类 | 只能放在单独写一个存放在Editor目录下的类上 |
[IFix] | 注入 | 可能需要修复函数的类的集合 | 只能放在[Configure]类的一个静态属性上 |
[Filter] | 注入 | 不想发生注入的函数 | 只能放在[Configure]类的一个静态函数上 |
tips:该流程存在的一个问题是,InjectFix的补丁是差异累计型的,即每个补丁包含所有打母包之后修改的内容,官方没有提供补丁版本管理的解决方案,如果有外网要存在多个不同版本的母包的需求,需要考虑如何管理[patch]标签。
//这里使用Resources.Load简单演示
var patch = Resources.Load<TextAsset>("Assembly-CSharp.patch");
if (patch != null)
{
//如果加载到补丁 使用PatchManager.Load加载即可生效
PatchManager.Load(new MemoryStream(patch.bytes));
}
[Configure]
public class TestCfg
{
[IFix]
static IEnumerable<Type> hotfix
{
get
{
//单个类逐个配置的写法
//return new List()
//{
//typeof(Test)
//};
//LinQ + 反射批量反射某个命名空间下的多个类的写法
return (from type in Assembly.Load("Assembly-CSharp").GetTypes()
where type.Namespace == "XXXX"
select type).ToList()
);
};
}
[Filter]
static bool Filter(System.Reflection.MethodInfo methodInfo)
{
//过滤掉Test类中的Div和Mult方法
return methodInfo.DeclaringType.FullName == "Test"
&& (methodInfo.Name == "Div" || methodInfo.Name == "Mult");
}
}
注意事项
代码参考
//新增一个类,实现原有的Interface
public interface ISubSystem
{
bool running { get; }
void Print();
}
[IFix.Interpret]
public class SubSystem : ISubSystem
{
public bool running { get { return true; } }
public void Print()
{
UnityEngine.Debug.Log("SubSystem1.Print");
}
}
//新增函数(或者修复代码[IFix.Patch]的Unity协程),用到了 yield return
[IFix.Interpret]
public IEnumerator TestInterface()
{
yield return new WaitForSeconds(1);
UnityEngine.Debug.Log("wait one second");
}
//新增函数(或者修复代码[IFix.Patch]),赋值到一个delegate变量
public class Test
{
public delegate int MyDelegate(int a, int b);//这个delegate是原有的
[IFix.Interpret]
public MyDelegate TestDelegate()
{
//该方法使用lambda表达式(闭包的一种形式)给MyDelegate变量赋值
return (a,b) => a + b;
}
}
//如果需要进行类似上述的改动 必须在inject前在[CustomBridge]的类中进行配置
[IFix.CustomBridge]
public static class AdditionalBridge
{
static List<Type> bridge = new List<Type>()
{
typeof(ISubSystem),
typeof(IEnumerator),
typeof(Test.MyDelegate)
};
}
//外网发现了C#侧的bug
public int Add(int a,int b)
{
//错误的逻辑
return a*b;
}
//在原方法上修改正确 添加[IFix.Patch]标签
[IFix.Patch]
public int Add(int a,int b)
{
//正确的逻辑
return a+b;
}
//新增一个属性
private string name;//这个name字段是在inject时就存在的
//[IFix.Interpret] 要打在下面的set & get方法上 打在这是无效的
public string Name
{
[IFix.Interpret]
set
{
name = value;
}
[IFix.Interpret]
get
{
return name;
}
}
//新增一个函数
[IFix.Interpret]
public int Sub(int a,int b)
{
return a-b;
}
//新增一个类
[IFix.Interpret]
public class NewClass
{
//private string name; //新增类中新增字段也是无效的
...
}
下面对InjectFix可能会造成的性能方面的影响进行测试
Assembly-CSharp.dll 大小 | |
---|---|
Inject前 | 712KB |
Inject后 | 997KB |
可见,注入代码还是会明显造成代码块的增大,增加的大小和注入方法的数量基本成正比,建议挑选可能会出现bug的代码选择性注入,不要给全部C#代码注入
函数 | 耗时(执行一百万次) | 结果分析 |
---|---|---|
原生函数,使用[Filter]过滤,不进行代码注入 | 51ms | C#原生代码,作为对照组 |
注入代码之后 | 58ms | 相较原生代码,多了一步数组中能否取到的判断,对性能影响不大(一百万次执行才看出差别) |
Patch热修复之后 | 36015ms | 由于InjectFix设计理念是有问题才修复,没有预先wrapper任何代码,依赖反射执行开销较大 |
反编译dll看到注入前后代码的变化如下:
//原生不注入的方法
public Vector3 NoInjectFunc(Vector3 a, Vector3 b)
{
return a + b;
}
//注入后反编译dll得到的方法 在原方法的开头注入了IsPatched的判断
public int InjectFunc(int a, int b)
{
//如果补丁中有该函数的实现,就到虚拟机解释执行
if (WrappersManagerImpl.IsPatched(3))
{
return WrappersManagerImpl.GetPatch(3).__Gen_Wrap_1(this, a, b);
}
return a + b;
}
真机Profiler中的数据如下
综上,单纯的Inject代码造成的运行效率开销基本可以忽略,但在Patch代码时要特别注意,避开频繁调用的函数(比如如果Patch了Update函数,Update下的所有方法都会到虚拟机解释执行),修复敏感函数后,最好到真机环境下检查运行效率是否能接受。
Demo工程简单模拟了正式环境热更的流程(省略了版本验证和资源MD5校验的过程)
Demo工程截屏:
tips:
一旦修改了代码(触发了重新编译),就会生成新的未注入的 dll覆盖掉之前已经注入的dll,故如果想在Editor下测试加载补丁后的效果,应该先Fix后Inject,流程应当如下:
作者介绍的他们腾讯内部团队的使用情况是XLua + InjectFix,injectFix代替XLua的HotFix进行原生C#代码的热修复(需关闭XLua的Hotfix功能,注入方法的实现相同会冲突)
InjectFix方案主要应用还是用于目前主流C# + Lua的项目中原生C#代码的热修复,减少上线后由于C#侧的bug,被迫换包的情况。靠InjectFix实现纯C#开发热更还是不实际的。