写在前头

今天一面,面试官问了一个问题:

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编译器可以执行复杂和高级的代码优化。

这里我做一个简单的总结:

  1. 动态语言和静态语言区别:类型检查的时机不同,类型/对象可否运行时变更,是否允许可变内存分配
  2. 将源代码转换成目标代码的程序一般有三个,AOT编译器解释器JIT编译器(结合了解释器)
  3. JIT,AOT都是处理代码的一种方法模式,前者在执行时,后者在执行前,两者概念上对立,但在编译器的实现上是可以并存共融的
  4. 无论动态语言还是静态语言,无论编译型语言,还是解释型语言,语言发展的趋势永远是取精华去糟粕的,这些严格的概念分离其实会变得越来越模糊,需要理解,但无需纠结

回头看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]

roslyn

我们反射利用的程序集的元数据就是该编译器生成的。

于是就生成了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语言的优势

  1. 作为一门解释型的语言,通过ANSI C 编译器生成字节码,由lua虚拟机解释为机器码本地执行,也适合IOS平台的热更新,且开发和调试效率更高。
  2. 作为一门纯动态型的语言,其编程本就是动态性的,比如其动态模块加载机制require(lua脚本),在热更新时直接替换脚本即可。
  3. 包体小,轻便

劣势就是不好写。

参考链接:

  1. .编译器 - 维基百科,自由的百科全书 (wikipedia.org)

  2. .解释器 - 维基百科,自由的百科全书 (wikipedia.org)

  3. 即时编译 - 维基百科,自由的百科全书 (wikipedia.org)

  4. 【深入浅出C#】章节 9: C#高级主题:反射和动态编程-腾讯云开发者社区-腾讯云 (tencent.com)

  5. 简析.NET Core 以及与 .NET Framework的关系 - 帅虫哥 - 博客园 (cnblogs.com)

  6. 为什么iOS无法动态加载Dll? | 车斌的博客 (chebincarl.github.io)