热更新的含义
首先想说的是这是一个工业界提出的概念,其实没有特别严谨权威的定义。简单来说,热更新(hotfix)是指让应用能够在无需重新安装的情况下实现更新。它描述了一种方案流程,但其实在各个细分的应用领域又有一些差异,不能一概而论。
对一个游戏软件而言,为什么要更新呢?一般是出于下面几点:
修复游戏bug(包括代码bug、配置bug或其他游戏资源bug)
增加新的游戏内容(包括新增代码、配置或其他游戏资源)
我们可以将一个游戏软件从逻辑上拆分为程序代码部分和资源文件部分。在游戏启动时,由程序的执行文件负责执行代码逻辑、加载各种需要的资源文件,展现游戏的内容。
资源文件部分一般包括数据文件(如各种格式的配置表),图片,音视频文件,或者其它游戏程序可识别的资源文件。它的更新比较纯粹,一般直接替换或者新增文件即可。
而程序代码由于不同平台执行程序的差异性,则要变得复杂很多,并不是仅仅替换一个exe文件就完事了。
代码跨平台
所谓跨平台,可以简单理解为一份程序代码可以在多个平台上运行。它的实现方案也有很多种
一次编译,到处运行(如java,实际上时依赖于不同平台都要有相对应的java虚拟机来解析“编译”出来的java字节码)
一次编码,到处编译(如C/C++,需要不同平台提供语言本身的编译支持)
还有lua这种嵌入式语言,依赖于不同平台实现了ANSI C,只要有C编译器的平台都可以让lua在虚拟机中运行自己的字节码
那么Unity是怎么实现跨平台的呢?
在这之前我们要再先说明几个概念:
Native code和托管代码
我们知道程序员写的程序代码最终会被转化为二进制代码后再由CPU执行。但不同架构的CPU他们的指令集一般是不同的,也就是说用来执行的程序的二进制代码也是不同的。
Native code是指以被编译为特定CPU的机器码的代码。
托管代码:编译器把代码编译城中间语言(IL),它是独立于CPU且面向对象的指令集。中间语言一般被封装在一个叫程序集(assembly)的文件中,这个文件包含了描述你所橙将的类、方法和属性的所有元数据
CLR公共语言运行时
一种虚拟机,用来做内存管理、程序集的加载、异常处理、线程同步等事情
.Net
一个抽象的平台概念(注意它是指一个概念)
.Net framework
.Net的一种实现,它包含两个主要的部分:
CLR的实现
CTS(通用类型系统)的实现
c#
仅仅是一种语言,但它是运行在.Net CLR上的一种语言
Mono
.Net 的另一种实现,也就是说它主要实现了
一个c#编译器
CLR(mono虚拟机)
一组类库
介绍完上面这些概念后,我们再来看一下Unity到底是怎么实现跨平台的
Unity引擎最底层是由C++写出的,而Mono被嵌入到了Unity当中,为Unity提供了一个完整的虚拟机运行环境。这样Mono的嵌入接口会将Mono Runtime暴露给Unity底层的C++代码。通过这些接口,开发者就可以控制Mono Runtime,以及依托于Mono Runtime的托管代码。
Unity3D的开发过程中,代码的编译主要分为两个过程:
首先将对应的脚本代码编译成CIL(之后CIL还会被编译成一种位元码,生成一个CLI集合)。
然后Mono在运行时将CLI集合中的位元码编译为本地运行的原生指令。
附带说一下,Mono是以dll的方式来存放程序集的,比如Assembly-CSharp.dll。这个dll(mono assembly)是一种特殊的动态链接库,扩展了Windows PE 格式,使其可含有编译后生成的IL元数据和代码。就算是再Android平台和IOS平台,它也是dll文件。我需要再解释一下为什么为什么Android的手机版apk解包出来会有dll文件。
首先Android系统是啥:
Android代码包括三部分:
AOSP(Android开源系统Android Open Source Project) 基于Linux内核,提供了Android系统的框架
GMS(Google 移动服务Google Mobile Service),由Google提供的一系列提高用户移动体验的应用和服务,包括各种服务和内购功能,还有一些 Google 的应用
基于AOSP的源码开发独立的Android系统
好了,可以看到Android实际上是linux内核,那它为什么可以使用window下的dll呢?Windows下的库有两种:
一种是native code,这种只能在window下使用(因为编译器把它们编译成了windows操作系统能够识别的机器码组织形式)。严谨地来说,是链接器和加载器的不同,以及各自的系统调用压根就没有互映射性,导致了linux不能使用它们。
第二种是.Net平台编译出来的类库,这个是生成了中间语言(IL),它是由.Net来负责的,如果能够完全满足.Net的标准,那么.Net就能让它再linux上跑起来。
看到这里我们大概可以有一个稍微清晰一点的认识,我们的代码在unity编译之后被放到了某个.dll中,如果我们要做更新,那就把对应的.dll文件替换掉就行了?
诶,在android平台上确实可以这么做。而在另外一篇中提到的ILRuntime(一种商业化的热更新方案),它的基本思想也是这样。
在这种思想的指导下,开发者可以将游戏分为两个部分,Unity和Hotfix。其中主要的游戏逻辑部分也就是可能需要热更的部分都写在Hotfix当中,然后将其导出为Hotfix.dll文件供Unity使用。游戏上线后若需要更新代码,只需要修改Hotfix中的代码,然后生成新的Hotfix.dll文件热更上去即可。
然而直接通过mono来实现这种做法会被IOS平台限制(至于想了解ios是怎么限制的可以移步这篇文章)
为什么用Lua
为了绕过IOS的限制,游戏开发者想到了通过Lua这种非编译又能够跨平台的语言来开发主要的(并且是容易被修改)的业务逻辑。lua文件不需要编译,可以被打包进游戏的资源中,在游戏启动的过程中加载对应的脚本资源,进而解释执行转换为字节码,在lua的虚拟机中执行,这种天然的设计,可以规避“程序文件”的更新限制。
Unity和lua的通信方式
首先要明确一点,lua代码始终是运行在lua虚拟机中的,由于lua的运行时(虚拟机)是由纯C语言写的,所以C和lua的实际上是基于一套C语言的API(如果你想十分详尽的了解这个过程,强烈推荐去看《游戏脚本高级编程》这本书)。
在unity中,这个lua虚拟机和相关C API由lua.dll提供(或者包含了相关代码的其他集成dll)
Lua call c#
所谓lua调用c#,实际上是将c#的一些类型和方法通过C API注册到lua虚拟机中。这样lua就可以通过相关的类名、方法命来创建相关的对象和实行相关的函数方法。
为了自动化的注册想用的类型和方法,xlua设计了一套叫Wrap文件的方式。每个wrap文件都是对一个c#类的包装,在lua中通过对wrap类中函数的调用,间接的对c#实例进行操作
c# call lua
C#可以通过P/Invoke方式调用lua的dll(包含了lua运行时的实现),通过这个dll执行lua的C API,即c#是借助C/C++来与lua进行数据通信的。
在xlua的LuaDLL.cs中实现了加载lua的dll的方法
相关参考文章
Unity实现c#热更新方案探究
https://www.cnblogs.com/zblade/p/9089105.html
深入xlua实现原理
https://www.cnblogs.com/iwiniwin/p/15323970.html
wrap文件的原理与使用
https://www.cnblogs.com/msxh/p/9813147.html
Unity编译Android的原理解析和apk打包分析
https://blog.csdn.net/u011490813/article/details/54577671
Unity跨平台的机制原理
https://www.cnblogs.com/0kk470/p/7468054.html
Unity引擎编译后的程序是如何运行在iOS和Android上的?
https://www.zhihu.com/question/25045484
对C#热更新方案ILRuntime的探究
https://www.cnblogs.com/zblade/p/9041400.html