写在前头
今天一面,面试官问了一个问题:
Lua语言本身进行热更新的原理/优势 是什么?
我模棱两可的回答了出来,但心头语塞,我知道lua是一个解释型语言,知道他零碎的语言特质,但问题就是零碎,我回想着之前从各个地方收集的琐碎信息,动态语言,编译器,解释器,JIT,AOH,lua虚拟机,代码编译的过程,却不能给出一个让自己满意的答案,我很难受,我在这方面的知识并没有形成体系,这就是问题所在。
所以本文就是一个信息的统一,一次体系化的过程。并且在此过程中发现之前接收的一部分信息是多么的trash!我还自以为是正确答案🙄。。。
动态语言/静态语言
动态编程语言是一类高级编程语言,它在运行时执行静态语言在编译期间执行的许多常见编程行为。总体来说动态语言和静态语言的区别如下:
- 类型/对象运行时变更,即支持运行时生成新的类或对象。例如c#就可以运用反射,获取类的构造器创建一个新的实例,改变一个对象状态。,借助
System.Reflection.Emit库运行时动态创建新的程序集,添加新的类型/接口/特性。 - 类型检查:静态语言通常在编译时就确定了变量类型,比如c#在声明变量时就要确定类型,即使使用
var关键字也是在编译时确定的(由于var关键字声明变量时必须初始化的规则,根据赋值在编译时确定类型),动态语言通常在运行时确定类型,比如lua,c#中的Dynamic关键字声明变量,is as运行时类型检查和匹配。 - 可变内存分配:静态编程语言(可能是间接的)要求开发人员在编译之前定义所使用的内存大小(除非使用指针逻辑)。动态语言隐式地需要基于程序个体操作(重新)分配内存。
其实当前语言的动态还是静态划分的不需要多么清晰,一门语言可以同时具备动态静态的特点。(当然c#还是被称为静态语言)例如我大.NET,通过DLR动态语言运行时,给旗下静态语言添加了动态语言的特点。(怎么实现的?我不知道)
编译型语言/解释型语言
编译器
编译器(compiler)是一种计算机程序,它会将某种编程语言写成的源代码转换成另一种目标代码(包括中间代码/机器码)。它从前最主要的特点是什么呢?是把代码全部编译完之后再执行。
解释器
解释器(英语:interpreter),也是一个程序,能够把解释型语言解释执行。它最主要的特点是什么呢?在程序执行时将代码逐句解释’逐句‘执行。因此依赖于解释器的程序运行速度比较缓慢。解释器的好处是它不需要重新编译整个程序,从而减轻了每次程序更新后编译的负担。
然后我看到了一个JIT编译器:他的描述是程序执行时边编译边执行,然后我就WTF了,你又叫编译器又不是先编译完再执行!你搁这犟嘴呢!
我们先把JIT编译器分开来看,我们知道了编译器是什么,JIT是什么?
JIT
Just-in-time compilation 即时编译,是一种执行计算机代码的方法模式,这种方法设计在程序执行过程中而不是在执行之前进行编译。通常,这包括源代码或更常见的字节码到机器码的转换,然后直接执行。
JIT编译是两种传统的机器代码翻译方法——提前编译(AOT)和解释器——的结合,它结合了两者的优点和缺点。大致来说,JIT编译,包含了解释器的开销以及编译和链接的开销,又结合了编译的速度与解释的灵活性。
JIT就这意思,但这还是没能解释这种命名的冲突?为什么不叫JIT解释器,为什么不叫JIT编译器pro? 为什么不叫JIT编译plus解释器?
我的回答是什么呢?一个名字而已你纠结个der啊!(很显然这是我纠结后的回答),重点不是编译器三字,重点是它结合了AOT和解释器双方的优点,是动态编译的一种实现方式。
既然说到了AOT,我们再看看AOT又是个什么东西。
AOT
提前编译(AOT compilation)是在程序执行之前将高级编程语言编译为低级语言的方法模式,于此行为模式相对应的就是先前说的’’编译器’’了。它有两大好处:
- 减少运行时开销。
- AOT编译器可以执行复杂和高级的代码优化。
这里我做一个简单的总结:
- 动态语言和静态语言区别:类型检查的时机不同,类型/对象可否运行时变更,是否允许可变内存分配
- 将源代码转换成目标代码的程序一般有三个,AOT编译器,解释器,JIT编译器(结合了解释器)。
- JIT,AOT都是处理代码的一种方法模式,前者在执行时,后者在执行前,两者概念上对立,但在编译器的实现上是可以并存共融的。
- 无论动态语言还是静态语言,无论编译型语言,还是解释型语言,语言发展的趋势永远是取精华去糟粕的,这些严格的概念分离其实会变得越来越模糊,需要理解,但无需纠结。
回头看C#的编译执行过程
我们知道c#.NetCore有两种运行时,一种叫CoreCLR,另外一个叫CoreRT。
c#在.CoreRT下编译执行过程:
c#源代码=>Roslyn 编译器=>中间代码IL(.dll/.exe)=> CoreRT.RyuJIT 编译器(AOT式)=>机器码运行。
c#在CoreCLR下编译执行过程:
c#源代码=>Roslyn 编译器=>中间代码IL(.dll/.exe)=>CoreCLR.RyuJIT 编译器(JIT式)=>机器码运行
|编 译 阶 段 | |执 行 阶 段|
首先看看编译阶段Roslyn对c#原生代码做了什么事[1]

我们反射利用的程序集的元数据就是该编译器生成的。
于是就生成了dll动态链接库,或者exe程序,其本质就是IL代码。
然后执行阶段,我们点击exe文件,其实就是IL指令,由RyuJIT 编译器动态运行,cpu加载。
注意的是,非托管代码由NativeRuntime‘管理’,他会被直接编译为本机机器代码。
Unity
Mono运行时虚拟机JIT/苹果平台下Full AOT,IL2CPP虚拟机 AOT
最后再来回答最先的问题:“Lua语言本身进行热更新的原理/优势 是什么?”
脚本热更新本质就是更新代码文件,c#也可以实现热更新,我们把游戏逻辑代码分为无需热更新模块A,需要热更新的模块B,
对于模块A,通过动态编程/反射的模式,获取B类数据,实例化对象,调用方法。更新时对于模块B直接替换为同名Dll就好了。
更高级的,就利用HybirdClR,ILRuntime的那种c#热更新的方式。
而Lua语言的优势
- 作为一门解释型的语言,通过ANSI C 编译器生成字节码,由lua虚拟机解释为机器码本地执行,也适合IOS平台的热更新,且开发和调试效率更高。
- 作为一门纯动态型的语言,其编程本就是动态性的,比如其动态模块加载机制require(lua脚本),在热更新时直接替换脚本即可。
- 包体小,轻便
劣势就是不好写。
参考链接: