InjectFix是腾讯最新对外开源的Unity代码逻辑热修复方案,可实如今Unity线上客户端内,不用迭代新版本,就能快速修复游戏的线上bug。html
先说几个亮点:
一、直接在Unity工程上修改C#便可更新;老项目无需修改原有代码便可使用;
二、更符合苹果热更新条款;
三、每一个游戏一份私有补丁格式,安全更有保障。ios
InjectFix经腾讯内部多个项目应用反馈十分良好,不只能解决线上bug,还能够有效的提升平常开发效率,下面咱们聊下这项目的前世此生。安全
热更方案大乱斗
全部支持ios的热更方案都有个共同点:更新后代码都是解析执行。若是按其更新前是否解析执行,能够分为两大类:
一类是某些模块甚至整个游戏,都一直解析执行。这是最传统的方式,目前市面上全部主流方案(xLua,slua,tolua,ILRuntime,jsb等等)都支持这种方式。这种方式的特色:
一、或多或少都会有些侵入性:ILRuntime解析执行C#编译后的程序集,在这些方案里头侵入性可能最小,但也须要对代码重构,把要更新的逻辑拆到单独程序集。各类非C#的脚本侵入性最大,一个已经完成的纯C#项目要用意味着重写。
ps:也有一种思路是经过一个C#转XX脚本工具来实现C#编码,解析执行,但若是你是一个已有项目想这么转一下,大几率是失败的,除非你一开始就在用这方式在开发,碰到坑就避开,由于这类方案每每不是完整支持所有语法,支持的语法也不必定能彻底一致。
二、基于性能,实现便利性等的考虑,通常游戏有些地方要以原生的方式跑,这些原生跑的代码出了bug这种方式是无能为力的。
三、若是使用的脚本是动态类型语言,还会带来代码维护困难的问题。
四、优势是能够新增功能,有的游戏甚至能够作到一次下载,后续不用整包更新。但苹果条款分析的章节能够看到,这也不必定是好事。
另一类是以原生方式跑,若是有bug,把逻辑重定向到新的,解析执行的逻辑。这种方式的特色:
一、侵入性低,后期项目也可使用。
二、正常逻辑是原生方式运行,有问题只是局部切换到解析执行,因此性能比较好。
三、会致使代码段增大,增大正比于注入的类的数量。
四、这种方式每每难以新增功能。
第二种方式是接下来讨论的重点,方便起见,咱们称之为“热修复”,热修复最先的成熟方案是xLua提供,通过两年来的使用已经逐渐被接受,tolua#后来也加入了这功能,也有一些网友基于ILRuntime作了热修复功能。app
InjectFix是什么?
InjectFix就是一个热修复的实现。那它和其它热修复方案又有什么不一样呢?
设想这么个场景,咱们有一个一千行代码的函数,其中有一行有问题,咱们须要修复它。
若是用xLua,须要用lua去从新实现一遍这个函数,工做量大。而基于ILRuntime的热修复,因为其补丁是另外一个程序集,它没法直接访问原类的私有成员,因此那999行正常代码通常也不能直接使用,须要作较多修改。
而InjectFix不须要用lua,也不须要像ILRuntime热修复那样另外建一个工程把那一千行逻辑重实现。只须要在Unity原工程直接改掉这行代码,而后标注这函数要更新便可。
不只如此,InjectFix还有其它优点:
运行时很是小巧,仅100K左右,比各lua方案,ILRuntime都要小不少,并且不依赖第三方库,纯C#实现。
支持每一个游戏生成一份本身私有的补丁格式,私有的指令定义。这样相比通用的lua原代码,lua字节码,clr程序集都更安全些。
支持Assembly-CSharp.dll以外的dll的修复。
免代码生成,更干净。
它也有缺点,不支持新增类,也不支持在已有类新增字段,修bug仍是够用的,但难以经过热更为游戏增长新功能。InjectFix就一个纯粹的修bug工具而已。ide
黑科技
因为InjectFix支持重复加载补丁,新加载补丁会自动覆盖上一个,这特性能够用来实现真机代码逻辑实时修改。
(嵌入视频,视频地址:https://v.qq.com/x/page/v0924... )函数
苹果政策合规性
各热更方案群的问的频率最高的问题之一:这方案会不会致使我游戏苹果审核不经过。
让咱们看看苹果的热更新条款:工具
能够看到最新条款容许下载代码解析执行,但前提是不能经过新增特性和功能来把程序改得(和审核时相比)面目全非。再看看一般被拒时的理由中的Guideline 2.5.2里的一句:Your app, extension, or linked framework appears to contain code designed explicitly with the capability to change your app’s behavior or functionality after App Review approval。
有“新增特性和功能”能力的热更新方案的尴尬之处在于有“改得面目全非”的能力。而InjectFix从它提供的能力(只能修改已有函数)来看,并不具有“新增特性和功能”的能力,这原本是弱点,放在这里却成为合规性的保证了。性能
基本原理
InjectFix项目的研发挺曲折的。InjectFix和xLua是同一个做者,也是本文笔者,当时xLua开源后,不断有人提但愿提供个C#转lua的工具,而深刻研究以为实现个il虚拟机工做量还更小,这样还能避免lua的一些gc问题。
决定要作il虚拟机后,也曾想过直接使用ILRuntime,评估后以为不太符合咱们的使用场景:ILRuntime并不能实现和原生代码的函数级别配合,这是咱们能实现原工程直接改Bug的关键;ILRuntime运行时部分依赖cecil,除了资源占用大以外,还容易和unity自带或者某些插件的cecil冲突;加载的是标准的程序集在安全性方面也比较堪忧。虽然说这些均可以改,但修改的工做量也挺大的,还不如本身写一个。
InjectFix实现bug修复主要靠这两部分:虚拟机负责新逻辑的解析执行;注入代码负责把调用重定向到虚拟机;下面咱们结合最简单的例子介绍下这两部分。ui
虚拟机
关键部分用几行伪码就能够描述清楚:this
导读
一、pc指向的是函数的第一条指令;
二、argumentBase指向的是第一个参数;
三、while+switch一条条指令往下执行,具体指令的操做在case那;
argumentBase指向的是求值栈该函数的栈帧,栈帧是这么安排的:
先放参数(若是有的话),再放本地变量(若是有的话),接着是临时区域,当函数返回时弹掉全部东西,若是有返回值就放到栈顶(函数执行前参数0的位置)。
用以下一个静态方法来演示下虚拟机怎么运行:
public static float Add(float a, float b)
{
return a - b;
}
这函数编译后是这四条指令
Add函数的执行过程
一、指令1把参数0 Push到栈顶;
二、指令2把参数1 Push到栈顶;
三、指令3把两个栈顶元素弹出(Pop)并相加,结果Push到栈顶;
四、指令4把栈顶拷贝到参数0的位置,清理栈,退出循环,Execute函数执行结束。
代码注入
上面的Add函数注入后是这样的
public static float Add(float a, float b)
{
if (WrappersManagerImpl.IsPatched(92)) { return WrappersManagerImpl.GetPatch(92).__Gen_Wrap_25(a, b); } return a - b;
}
比较简单,发现这函数有patch的话,就重定向到虚拟机。
而__Gen_Wrap_25是个适配器函数,赋值把参数压栈,调用虚拟机的Execute函数,并把结果返回。__Gen_Wrap_25的实现以下:
public float __Gen_Wrap_25(float P0, float P1)
{
Call call = Call.Begin(); call.PushSingle(P0); call.PushSingle(P1); this.virtualMachine.Execute(this.methodId, ref call, 2, 0); return call.GetSingle(0);
}
PS:咱们的例子仅有三种指令,和这几条指令无关的代码所有简化了,真正复杂得多,有兴趣能够看源码了解。
关于开源
闭门造车很难作出好项目。须要用心聆听,根据反馈不断的改进本身。而开源,可以听到更多的声音,也能更好的改进这个项目。
总结下InjectFix使用简单,小巧,合规且安全。即便你不打算用它来更新线上版本,只要你程序有原生部分,接入也能必定程度上提升开发效率,没什么拒绝它的理由,是吧?