InjectFix扩展——增量补丁方案
一、简介
InjectFix(下文中简称IFix)是一个基于Unity的C#代码热修复的实现方案,详细介绍可以参照上一篇《InjectFix——C#热修复方案分析 & 使用流程》,本篇是针对线上同时运营多个不同版本的母包,需要生成不同版本补丁的需求,对InjectFix进行的扩展方案。
二、需求背景
IFix理想的工作流程如下:
- 打包
- 使用[Patch]/[Interpret] Attribute(下文中简称IFix Attribute)生成补丁修复线上bug
- 在下一次打包前把所有已有的IFix Attribute都删掉,再重新打包
但是当我们线上同时存在不同版本的母包的时候就会出现的问题,比如线上同时存在1.0和2.0的包在运营,原本流程出2.0的包的时候,1.0版本的IFix Attribute已经被删除了,此时如果还要为1.0版本打补丁的话,就得把1.0版本曾有的IFix Attribute再重新加上,版本多了之后管理起来十分困难。
三、设计思路
IFix本质上也是生成一个更新补丁,结合常规的补丁生产方式,有两种解决方案
- 逐版本差异生成补丁
每个版本间生成一个补丁,优点是补丁较小(只包含两个小版本之间差异的内容),缺点是相同的内容可能会重复下载多次 - 累计差异生成补丁
给每个版本到最新版本之间生产一个补丁,缺点是累计多个版本的内容后,补丁大小相对会比较大,优点是每个版本仅需要管理一个补丁
结合IFix的实际需求
- IFix补丁的内容只是.byte的字节流,通常只有几KB的大小,故补丁的大小基本不用考虑
- IFix原本的设计单个程序集只能加载一个补丁,加载新补丁会覆盖掉旧的,修改成支持多个补丁的方式改动较大,不利于后续更新IFix版本
- IFix.Core.dll在Plugins文件夹下,改动加载补丁的部分的话,无法热更,已经上线的包没法兼容
出于以上原因考虑,最终选择了累计差异生产补丁的方式,实现思路为IFix Attribute增加版本号字段,生成补丁的时,根据版本号字段生成不同内容的补丁(补丁中只包含版本号字段 >= 母包版本号的内容),加载时根据版本号加载各自版本的补丁。
四、效果展示
-
新增了一个带版本管理功能的编辑器,打包方法使用了静态方法,所有必要字段都由参数传入,方便自动打包时使用cmd调用
-
新增了日志功能,打包过程中各个补丁中包含的内容都会记录,方便检查确认补丁中的内容是否正确
-
模拟生产环境进行验证,流程如下:
- 打1.0版本母包
- 在1.0版本中修复方法一,并新增字段/属性/方法/类
- 打2.0版本的母包(此时1.0中增改的内容已经包含在2.0版本的母包中了)
- 在2.0版本中修复方法二
- 针对两个版本分别生成补丁,根据版本号加载
- 1.0版本补丁包含1.0和2.0中使用IFix Attribute修复和新增的内容
- 2.0版本补丁仅包含2.0版本以后修复的内容
-
真机结果对比
- 经过IFix Attribute修复或后的代码会在运行时通过IFix虚拟机解释执行,故效率明显要比直接运行C#代码要低,这里通过在每帧循环多次调用,依靠耗时判断是否是在IFix虚拟机下运行
- 1.0母包加载补丁前后对比
母包1.0下载最新补丁后,1.0和2.0版本修改/新增的方法都实现了热更,由于是解释执行,耗时较高。 - 2.0母包加载补丁前后对比
由于1.0版本中新增和修复的内容,已经包含在2.0母包中了,只有2.0修复的方法二是通过IFix虚拟机解释执行,测试结果符合预期。
Ps:示例中也顺带对比了虚拟机解释执行和原生C#的性能情况,实际项目中应尽可能避免依赖IFix增改高频函数。
使用说明
- 第一次接入
- (非必要步骤) 本方案基于IFix2021年04月30日的版本修改,如果时间间隔较大的话,建议下载最新版的IFix按照下文源码修改说明重新修改,扩展过程中本人尽可能的少改动了源码的内容,节省更新源码版本的工作量
- 导入IFixExtension.package
- 重写Configure.cs中的IsNewerOrCurrentVersion方法,这个方法是用来比较版本号的大小,项目组需要更具自身版本号命名风格进行修改
- 自行增加根据版本号下载不同补丁的逻辑,再由PatchManager.Load加载
- 日常开发
- 使用IFix Attribute时传入版本号字符串作为参数
- 每当有了新的线上母包版本要在InjectFixConfig.options中添加版本号配置
- 使用Tools/InjectFix扩展下的编辑器工具生成补丁,补丁和日志会生成到工具中指定的路径下
- 建议检查日志中各个补丁的内容是否正确
源码修改说明
为方便后续更新IFix版本,对源码的修改主要集中在UnityEditor中的Configure脚本
1、修改IFix工程源码
- Attribute添加版本号字段
- 打开InjectFix源码工程 InjectFix-master\Source\VSProj\vs2013\IFix.sln
- 打开SwitchFlags.cs 为其中的InterpretAttribute和PatchAttribute添加版本号字段
[AttributeUsage(AttributeTargets.Method)]
public class PatchAttribute : Attribute
{
public string version;
public PatchAttribute(string version)
{
this.version = version;
}
}
- 修改完成后使用build_for_unity.bat脚本重新编译生成IFix.Core.dll
- 用最新生产的IFix.Core.dll覆盖掉Assets/Plugins下的旧dll
2、修改IFix Editor下的脚本
- ILFixEditor
只修改了两处调用GenPatch的地方传入的参数,使支持打包后的补丁增加版本号后缀以及支持自定义打包路径
//原作者应该是固定删掉一个/后的内容得到路径
//所以这里配合他多加一个/ 就能得到我们自定义的路径了
GenPatch(assembly, string.Format("{0}/{1}.dll", outputDir, assembly),
"./Assets/Plugins/IFix.Core.dll",
string.Format("{0}/{1}{2}.patch.bytes",
InjectFixEditorWindow.Path, assembly, Configure.GetPatchVersion()));
- Configure
Configure脚本主要的作用是根据反射获取添加了IFix Attribute的类/方法/属性/字段,返回对应的IEnumerable,本人主要改动是在结果返回前增加了根据当前打包的补丁版本号进一步筛选的逻辑,反射获取Attribute的版本号字段,只有 >= 当前打包补丁版本号的内容才会包含进该版本的补丁,核心代码如下:
//利用版本号再次筛选
//Type/MethodInfo/FieldInfo/...都是MemberInfo的子类 这里使用泛型减少代码量
public static List<T> FilterByVersion<T>(IEnumerable<T> list,Type targetType)
where T : MemberInfo
{
List<T> resList = new List<T>();
foreach(T item in list)
{
var attribute = item.GetCustomAttribute(targetType);
string methodVersion = attribute.GetType().GetField("version")
.GetValue(attribute).ToString();
if (IsNewerOrCurrentVersion(methodVersion, PatchVersion))
{
resList.Add(item);
}
}
//加入日志打印缓存区
AddContentToLog<T>(resList,targetType);
return resList;
}
//版本号是否大于或等于母包版本号 这个判断方法需要项目组根据自己的版本号规则修改
public static bool IsNewerOrCurrentVersion(string version1, string version2)
{
//正则提取出字符串中的数字再比较大小
string pattern = @"[^0-9]+";
int v1 = int.Parse(Regex.Replace(version1, pattern, ""));
int v2 = int.Parse(Regex.Replace(version2, pattern, ""));
return v1 >= v2;
}