什么是热更新?
热更新是指当游戏出现bug,或者需要修改,增加某个功能的时候,我们不需要重新下载安装包,就可以更新游戏内容。
游戏上线后,遇见bug或者需要更新内容,一般有两种做法:
第一种:重新打包上传一个新的安装包到应用商店(需要审核,费时间),然后让玩家下载新的客户端安装包(需要重新下载,费流量)
第二种:在游戏内更新,游戏启动时去下载需要更新的资源,即热更新。
实现热更新方案
- ILRuntime:
是掌趣科技开发的开源unity热更框架,这个框架打破了C#不能做ios平台热更的规则(在ios平台中是以ILRuntime+dll的方式解释执行,当然是在IL2CPP下)。有了这个框架,开发者只需使用C#这一种语言即可,而不用在C#和lua之间切换。 - xLua:
腾讯开发的Lua插件,目前比较主流。
热更新原理
客户端和服务器上分别保存有配置文件,程序启动时使用http协议下载服务器的配置文件来与客户端的进行逐行对比,通过比较(比如md5码)找出不同的资源和脚本代码(需要更新的部分),把对应的资源和代码下载下来并将本地的覆盖。
C#直接反射热更新
由于Android支持JIT(Just In Time)即时编译(动态编译)的模式,即可以边运行边编译,支持在运行时动态生成代码和类型。从Android N开始引入了一种同时使用JIT和AOT的混合编译模式。JIT的优点是支持在运行时动态生成代码和类型,APP安装快,不占用太多内存。缺点是编译时占用运行时资源,执行速度比AOT慢。比如,C#中的虚函数和反射都是在程序运行时才确定对应的重载方法和类。因此,Android平台可以不借助任何第三方热更新方案,直接使用C#反射执行DLL文件。实际开发时通过System.Reflection.Assembly类加载程序集DLL文件,然后再利用System.Type类获取程序集中某个类的信息,还可以通过Activator类来动态创建实例对象。
而IOS平台采用AOT(Ahead Of Time)预先编译(静态编译)的模式,不支持JIT编译模式,即程序运行前就将代码编译成机器码存储在本地,然后运行时直接执行即可,因此AOT不能在运行时动态生成代码和类型。AOT的优点是执行速度快,安全性更高。缺点是由于AOT需要提前编译,所以APP的安装时间长且占内存。Mono在IOS平台上采用Full AOT模式运行,如果直接使用C#反射执行DLL文件,就会触发Mono的JIT编译器,而Full AOT模式下又不允许JIT,于是Mono就会报错。因此,IOS平台上不允许直接使用C#反射执行DLL文件来实现热更新。
Lua 一些要点
简介
轻量级脚本语言,常用于热更,Lua本身是由C语言编写。
理解面向对象的实现
本质上就是用表当作类,然后用元表及其元方法实现各种套娃操作。
闭包
像Lua,JavaScript这类脚本语言,都可以在方法内部定义内部方法,再将内部方法作为返回值传递出去。
在Lua中,变量具有作用域,从全局变量,再到第一层方法(局部作用域),再到第二层方法(局部中的局部作用域),一层层传递下去。
在Lua里,方法的定义有一个函数原型,其中有一个UpValue值,在传递给变量时,就会形成一个闭包,获取到上层作用域的值。
简单来说,闭包就是内部方法可以调用到外部方法的局部变量,原理是方法的函数原型会在形成闭包时存储上层作用域的值,因此,不同闭包之间的数据不会互相影响(Table除外)
Lua与C#交互原理
lua调用c#就是将lua层的参数和c#导出函数入栈,然后执行函数。c#调用lua就是将c#层的参数和lua函数入栈,然后执行函数。
C# Call Lua:
- 导入必要的dll文件
- 先将方法入栈
- 再将方法需要的参数按顺序入栈
- 调用 lua_pcall
- 此时lua会把返回值入栈
C#->dll->Lua
Lua Call C#:
- 生成C#源文件所对应的Wrap文件或者编写C#源文件所对应的c模块
- 将源文件内容通过Wrap文件或者C模块注册到Lua解释器中
- 由Lua去调用这个模块的函数
lua->wrap->C#
Lua与其他宿主语言交互
Lua与其它宿主语言进行交互的原理,是通过虚拟栈进行的,Lua底层是用C语言编写的,因此提供了一些用于操作虚拟栈的C API,通过这些API就可以实现其它语言与Lua之间的交互了。
当C/C++想调用lua中一个值时,lua将数值压入lua虚拟栈中,然后通过lua提供api来读取,当C/C++想向lua传入一个值时,C++通过lua提供的api将数值压入lua虚拟栈中,lua便可进行调用,通过这样交互完成相互调用。
luaState 中放的是 lua 虚拟机中的环境表、注册表、运行堆栈、虚拟机的上下文等数据,在xLua中luaState被封装成了LuaEnv类,在toLua中叫作LuaState。
Lua的GC垃圾回收机制算法
Lua的GC使用了标记清除算法Mark and Sweep。
AssetBundle
简介
Assetbundle是游戏安装之后,对非代码内容进行更新的主要方式,同时也可以做到减少初始包体大小、游戏可以做分包处理、利用二进制文件做资源加密的功能。
AssetBundle是一个“archive” 文件,包含的是特定平台的非代码资产(assets),比如说Models, Textures, Prefabs等。并且可以表达这些资产的互相引用,甚至不在同一个assetbundle中的资产的引用,比如这个assetbundle里面的Material可以关联到另一个Assetbundle里的Texture。
压缩
AssetBundle可以使用压缩方法进行压缩,提供了LZMA和LZ4两种压缩方法,官方建议使用LZ4方法。压缩率会低一点但是速度快了10倍。
加载与卸载
加载分为网络加载和本地加载,视情况选择对应的API。
卸载的API:
- AssetBundle.Unload(false): 参数为false时,AssetBundle内的序列化数据会被释放,实例化的物体还都保持完好。简单的说就是断开了AssetBundle和实例之间的联系。值得注意的是这样做并不会卸载AssetBundle,意思是AssetBundle还会继续占用内存。然而如果再次实例化对象,也不会返回以前初例化过的AssetBundle,而是重新实例化一个新的AssetBundle,那么这样就出现了冗余,同样的资源,内存中会出现多份。所以适合这样来卸载的资源,一定是不需要多次创建实例的资源,例如:一次性使用的配置信息。而卸载的话要当创建的实例删除回收以后,手动调用Resources.UnloadUnusedAssets或切换场景来释放。
- AssetBundle.Unload(true)。参数为true时,就简单多了,卸载AssetBundle,并且删除被引用的资源。注意!如果AssetBundle中有资源在场景中被引用,则会出现资源丢失的情况。这种卸载方式,最为彻底,完全从内存移除,缺点是你需要一套机制,来关注是不是还有资源引用,会不会引起异常。
- 加载的AssetBundle,可以通过Resources.UnloadUnusedAssets来释放。但是前提是加载的AssetBundle一定要先释放干净,即没有任何引用。在调用AssetBundle.Unload(true)后会强行删除创建的实例和引用;而AssetBundle.Unload(false),必须先手动删除资源的实例和引用才能释放,Resources.UnloadUnusedAssets的开销其实并不小(因为不仅仅是释放AssetBundle还包括Resources.Load的其他资源),建议每隔一段时间进行调用比较合适。
依赖打包
- 对于引用的资源,必须明确出现在其他AssetBundle的资源列表中,Unity才能识别为依赖关系,形成依赖打包。
- 合理规划AssetBundle包,公共的资源单独抽离打包。(如果两个对象分配个两个不同的assetbundles,但是同时和一个常见的对象产生了依赖,那么这个对象就会被同时打包进两个assetbundles中,这不仅会造成应用大小的负担,而且如果场景加载这两个父对象,那它就会被加载两遍。)
- 注意资源与AssetBundle的关系,一个AssetBundle可以包含多个资源,但同一份资源不允许存在于多个AssetBundle中。
分组策略
- AssetBundle 数量太少:
- 会增加运行时内存使用
- 会增加加载时间
- 需要下载大量数据
- AssetBundle 数量太多:
- 会增加构建的时间
- 会加大开发的复杂性
- 会增加总的下载时间
- 分配策略:
- 逻辑实体分组:通过项目功能来分组对象
- 类型分组:相似或者相同类型的对象被放置到同一个 AssetBundle 中
- 不相干(concurrent)内容分组:将需要同时加载和使用内容分组到同一个 AssetBundle 的策略
浏览与检测
要检测资源是否冗余,推荐使用AssetBundles-Browser、UnityStudio、disunity、UWA GOT等工具进行检测。
优秀的开源库
MonsterABSystem,ABSystem,KSFramework,xasset,zcode-AssetBundlePacker
XLua
简介
xLua为Unity、 .Net、 Mono等C#环境增加Lua脚本编程的能力,借助xLua,这些Lua代码可以方便的和C#相互调用。
使用流程:标记需要热更的资源,打包构建AssetBundle资源(包括lua脚本),上传到服务器,客户端通过服务器下载读取AssetBundle资源,执行lua脚本即可。
github:https://github.com/Tencent/xLua
简单的使用流程
- 创建xLua虚拟环境,定义加载器
- 在需要热更新的类加上[Hotfix]标签,需要热更的方法加上 [LuaCallCSharp]标签
- 释放:及时将LuaEnv给Dispose掉,释放LuaEnv之前还要反注册,那些注册到C#中的回调函数
XLua的原理
xLua是C#和Lua进行交互的桥梁。
流程:
- 通过对C#的类与函数设置Hotfix标签。来标识需要支持热更的类和函数。
- 生成函数连接器来连接LUA脚本与C#函数。
- 在C#脚本编译结束后,使用Mono提供的一套C#的API函数,对已经编译过的.Net体系生成的DLL文件进行修改。
- 通过LUA脚本修改C#带有标签的类中静态变量,把代码的执行路径修改到LUA脚本中。
打上[HotFix]标签后,执行XLua/Generate Code后,xLua会根据内置的模板代码生成器在XLua目录下的Gen目录中生成一个DelegatesGensBridge.cs文件,该文件在XLua命名空间下生成一个DelegateBridge类,这个类中的__Gen_Delegate_Imp*函数会映射到xlua.hotfix中的function。
ILruntime
ILRuntime项目为基于C#的平台(例如Unity)提供了一个纯C#实现,快速、方便且可靠的IL运行时,使得能够在不支持JIT的硬件环境(如iOS)能够实现代码的热更新。
同市面上的其他热更方案相比,ILRuntime主要有以下优点:
- 无缝访问C#工程的现成代码,无需额外抽象脚本API
- 执行效率是L#的10-20倍
- 选择性的CLR绑定使跨域调用更快速,绑定后跨域调用的性能能达到slua的2倍左右(从脚本调用GameObject之类的接口)
- 支持跨域继承
- 完整的泛型支持
- 拥有Visual Studio的调试插件,可以实现真机源码级调试。
原理:
ILRuntime借助Mono.Cecil库来读取DLL的PE信息,以及当中类型的所有信息,最终得到方法的IL汇编码,然后通过内置的IL解译执行虚拟机来执行DLL中的代码。
IL托管栈和托管对象栈:
为了高性能进行运算,尤其是栈上的基础类型运算,如int,float,long之类类型的运算,直接借助C#的Stack类实现IL托管栈肯定是个非常糟糕的做法。因为这意味着每次读取和写入这些基础类型的值,都需要将他们进行装箱和拆箱操作,这个过程会非常耗时并且会产生巨量的GC Alloc,使得整个运行时执行效率非常低下。
因此ILRuntime使用unsafe代码以及非托管内存,实现了自己的IL托管栈。