Mono为何能跨平台

概念

  • JIT 编译 (JIT compilation),运行时需要代码时,将 Microsoft 中间语言 (MSIL) 转换为机器码的编译。
  • CLR (Common Language Runtime)是通用语言运行时。和Java虚拟机一样也是一个运行时环境,它负责资源管理(内存分配和垃圾收集等),并保证应用和底层操作系统之间必要的分离。
  • 机器码 (machine code),学名机器语言指令,有时也被称为原生码(Native Code),是电脑的CPU可直接解读的数据。通常情况下它是已经经过编译,但与特定机器码无关。字节码通常不像源码一样可以让人阅读,而是编码后的数值常量、引用、指令等构成的序列。
  • 字节码 (Bytecode) 是一种包含执行程序、由一序列 op 代码/数据对 组成的二进制文件。字节码是一种中间码,它比机器码更抽象,需要直译器转译后才能成为机器码的中间代码。字节码的实现方式是通过编译器和虚拟机。编译器将源码编译成字节码,特定平台上的虚拟机将字节码转译为可以直接执行的指令。
  • 编译器: 在代码运行之前,把所有代码一次性翻译成 目标平台(Windows或者Linux等)的二进制指令,这些指令可以脱离编译器独立运行。
  • 解释器: 在代码运行过程中,把代码逐条生成目标平台指令,并执行,然后再处理下一条。
  • 编译执行: 要先编译再执行,就是使用编译器来将我们的代码全部编译成机器可以识别的二进制代码,然后进行执行。因为先整体进行编译,所以这里会生成编译后的机器代码。比如C,C++等语言都是编译执行的。
  • 解释执行: 是使用解释器会将我们的一句句代码解释成机器可以识别的二进制代码来执行,可以认为是,解释一句,执行一句。在这个过程中,不会生成中间文件。比如python,ruby等语言都是解释执行的。

JIT具体做法

JIT具体的做法是这样的: 当载入一个类型时,CLR为该类型创建一个内部数据结构和相应的函数,当函数第一被调用时,JIT将该函数编译成机器语言.当再次遇到该函数时则直接从cache中执行已编译好的机器语言。

iOS的限制

IL是.NET框架中中间语言(Intermediate Language)的缩写。使用.NET框架提供的编译器可以直接将源程序编译为.exe或.dll文件,但此时编译出来的程序代码并不是CPU能直接执行的机器代码,而是一种中间语言IL(Intermediate Language)
在Mac OS上,因为iOS的现有限制,面向iOS的C#代码会通过AOT编译技术直接编译为ARM汇编代码。而在Android上,应用程序会转换为IL,启动时再进行JIT编译。
Mono的AOT和.NET的Ngen一样,都是通过提前编译来减少JIT的工作,但默认情况下AOT并不编译所有IL代码,而是在优化和JIT之间取得一个平衡。由于iOS平台禁止JIT编译,于是Mono在iOS上需要Full AOT编译和运行。即预先对程序集中的所有IL代码进行AOT编译生成一个本地代码映像,然后在运行时直接加载这个映像而不再使用JIT引擎。

java为什么可以做到跨平台

因为java也是一种解释型语言,虽然java也有编译器,但是编译器做的工作只是预编译,是把java代码编译成了非纯二进制的字节码,并没有直接编译成某个平台的二进制指令,这样的字节码文件再通过解释器进行解释成某个平台的二进制指令的时候,会比直接把java代码解释成二进制指令要快。

C#为什么不能热更新

准确的说,C#在安卓上可以实现热更新,但在苹果上却不能。
在安卓上可以通过C#的语言特性-反射机制实现动态代码加载从而实现热更新。
具体做法是:将需要频繁更改的逻辑部分独立出来做成DLL,在主模块调用这些DLL,主模块代码是不修改的,只有作为业务(逻辑)模块的DLL部分需要修改。游戏运行时通过反射机制加载这些DLL就实现了热更新。
但苹果对反射机制有限制,不能实现这样的热更。为什么限制反射机制?安全起见,不能给程序太强的能力,因为反制机制实在太过强大,会给系统带来安全隐患。

LUA为什么可以热更

LUA解释型语言,并不需要事先编译成块,而是运行时动态解释执行的。那C#为什么不做成解释型语言呢?因为C#的定位是一个追求效率且功能强大的编译型语言。这样LUA就和普通的游戏资源如图片,文本没有区别,因此可以在运行时直接从WEB服务器上下载到持久化目录并被其它LUA文件调用。

Mono为何能跨平台

众所周知,Unity3D引擎凭借着强大的跨平台能力而备受开发者的青睐,在跨平台应用开发渐渐成为主流的今天,具备跨平台开发能力对程序员来说就显得特别重要。传统的针对不同平台进行开发的方式常常让开发者顾此失彼,难以保证应用程序在不同的平台都有着相同的、出色的体验,这种情况下寻找到一种跨平台开发的方式将会为解决这个问题找到一种思路。从目前的开发环境来看,Web应该是最有可能成为跨平台开发的神兵利器,可是长期以来Web开发中前端和后端都有各自不同的工作流,虽然现在出现了前端和后端逐渐融合的趋势,可在博主看来想让Web开发变得像传统开发这样简单还需要一定的过渡期。

从Mono到Xamarin
  对Unity3D来说,Mono是实现它跨平台的核心技术。Mono是一个旨在使得.NET在Linux上运行的开源项目。它通过内置的C#语言编译器、CLR运行时和各种类库,可以使.NET应用程序运行在Windows、Linux、FreeBSD等不同的平台上。而在商业领域,Xamarin则实现了用C#编写Android和iOS应用的伟大创举。Windows10发布的时候,微软提出了通用应用UWP的设想,在这种设想下开发者可以直接在最新的Visual Studio中使用C#编写跨平台应用。最近微软收购了Xamarin,这一举措能够保证Xamarin这样的商业项目可以和微软的产品融合地更好。虽然在传统Web开发中Java和PHP目前占据主要优势,可是虽然云计算技术的流行,服务器成本的降低或许会让C#这样优秀的语言更加成熟。我一直坚信技术没有好坏的区别,一切技术问题的核心是人,所以接下来,我们打算追随着跨平台开发的先驱——Java,最早提出的“一次编写、到处运行”的伟大思想来探索C#程序跨平台的可能性。

Mono跨平台的原理
  在提到Mono跨平台的时候,我们首先需要引入公共语言基础(Common Language Infrastructure,CLI)这个概念,CLI是一套ECMA定义的标准,它定义了一个和语言无关的跨体系结构的运行环境,这使得开发者可以用规范定义内各种高级语言来开发软件,并且无需修正即可让软件运行在不同的计算机体系结构上。因此我们可以说跨平台的原理是因为我们定义了这样一个和语言无关的跨体系结构的运行环境规范,只要符合这个规范的应用程序都可以运行在不同的计算机体系结构上,即实现了跨平台。针对这个标准,微软实现了公共语言运行时(Common Language Runtime,CLR),因此CLR是CLI的一个实现。我们熟悉的.NET框架就是一个在CLR基础上采用系统虚拟机的编程平台,它为我们提供了支持多种编程语言如C#、VB.NET、C++、Python等。我们编写的C#程序首先会被C#编译器编译为公共中间语言即CIL或者是MSIL(微软中间语言),然后再由CLR转换为操作系统的原生代码(Native Code)。

好了,现在我们来回答最开始的问题:Mono为什么能够跨平台。我们回顾.NET程序运行机制可以发现实现.NET跨平台其实需要这三个关键:编译器、CLR和基础类库。在.NET下我们编写一个最简单的“Hello World”都需要mscorlib.dll这个动态链接库,因为.NET框架已经为我们提供了这些,因为在我们的计算机上安装着.NET框架,这是我们编写的应用程序能够在Windows下运行的原因。再回头来看Mono,首先Mono和CLR一样,都是CLI这一标准的实现,所以我们可以理解为Mono实现了和微软提供给我们的类似的东西,因为微软的.NET框架属于商业化闭源产品,所以Mono除了在实现CLR和编译器的同时实现了大量的基础库,而且在某种程度上Mono实现的版本与相同时期.NET的版本有一定的差距,这点使用Unity3D开发游戏的朋友应该深有感触吧!这就决定了我们在将应用程序移植到目标平台时能否实现在目标平台上和当前平台上是否能够具有相同的体验。因为公共中间语言即CIL能够运行在所有实现了CLI标准的环境中,而CLI标准则是和具体的平台或者说CPU无关的,因此只要Mono运行时能够保证CIL的运行,就可以实现应用程序的跨平台。我们可以通过下面这张图来总结下这部分内容:

开发第一个跨平台程序
  下面我们来尝试开发第一个跨平台程序,我们使用Visual Studio或者MonoDevelop编写一个简单的控制台应用程序,为了减少这个程序对平台特性的依赖,我们这里选择System这个命名空间来实现最为基础的Hello World,这意味着我们的应用程序没有使用任何除mscorlib.dll以外的库:

using System;

namespace MonoApplication
{
    class MainClass
    {
        public static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }
}

因为我们的计算机安装了.NET框架,所以我们编写的这个程序会被C#编译器编译为公共中间语言CIL,然后再由CLR转换为Native Code。通常情况下公共中间语言(CIL)会被存储到.il文件中,可是在这里我们在编译的时候好像并没有看到这个文件的生成啊,这是因为这里生成的可执行文件(.exe)本质上是公共中间语言(CIL)形态的可执行文件。这一点我们可以通过ildasm这个工具来验证,该工具可以帮助我们查看IL代码,通常它位于C:\Program Files\Microsoft SDKs\Windows\v7.0A\bin这个位置。下面是通过这个工具获得的IL代码:

.method public hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // 代码大小       13 (0xd)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ldstr      "Hello World!"
  IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_000b:  nop
  IL_000c:  ret
} // end of method MainClass::Main

可以看到这段代码和我们编写的程序中的Main方法完全对应,关于这段代码的含义,大家可以通过搜索引擎来了解IL代码的语法。因为我们这里想要说明的是,这里生成的可执行文件(.exe)从本质上来讲并非是一个可执行文件。因为它能否执行完全是取决于CPU的,这和我们直接用C++编写的应用程序不同,我们知道不同的编译器如Windows下的VC++和Linux下的GCC都是和硬件紧密相连的,所以我们编译的程序能够在各自的平台直接运行,即CPU是认识这些程序的。可是在.NET这里就不一样了,因为我们通过C#编译器即csc.exe编译出来的文件,其实是一个看起来像可执行文件,实际上却是一个和平台无关、和CPU无关的IL文件。

那么我们就会感到迷茫了啊,平时我们编译完C#程序双击就可以打开啊,哈哈,现在隆重请出.NET程序的家长公共语言运行时(CLR)。公共语言运行时实际上是程序运行的监管者,程序运行的情况完全由运行时来决定。我们双击这个文件的时候,公共语言运行时会将其加载到内存中,然后由即时编译器(JIT)来识别IL文件,然后由CPU去完成相应的操作。

所以我们可以这样理解.NET程序跨平台,因为IL文件是一个和平台无关、和CPU无关的、跨平台的文件结构,所以我们只需要在不同的平台上实现这样一个公共语言运行时(CLR)就可以实现在不同的平台上运行同一个程序。但这个过程中,需要有一个C#编译器负责将C#代码转换为IL代码,然后需要有一个公共语言运行时(CLR)来解析IL代码。与此同时,我们在.NET框架下使用了大量的基础类库,这些类库在Windows以外的平台是没有的,所以除了C#编译器和公共语言运行时以外,我们还需要基础类库。现在大家是不是对Mono有了更清楚的认识了呢?没错,Mono所做的事情其实就是我们在讨论的这些事情。这里博主想说说即时编译(JIT)和静态编译(AOT),这两种编译方式我们可以按照”解释型”和”编译型”来理解,为什么Unity3D在iOS平台上做热更新的时候会出现问题呢?这是因为iOS平台考虑到安全性禁止使用JIT即时编译,所以像C#这种需要编译的语言在这里就无计可施了。

好了,既然我们有Mono这样的工具能够帮助我们实现跨平台开发。那么我们现在就来考虑将这个程序移植到Linux平台,这里以Linux Deepin为例,我们按照C#程序编译的过程来完成这个移植过程:
1、将C#程序编译为IL文件:在.NET下我们使用csc.exe这个程序来完成编译,在Mono下我们使用mcs.exe这个程序来完成编译,这个程序在安装完Mono以后在其安装目录内可以找到。我们在命令行下输入命令:

mcs D:\项目管理\CSharp\MonoApplication\MonoApplication\Main.cs

2、这样将生成Main.exe这样一个IL文件,现在我们需要一个运行时来解析它,在.NET下我们使用CLR来完成这个步骤,在Mono下我们使用mono.exe这个文件来完成这个步骤。我们在命令行下输入下列命令:

mono D:\项目管理\CSharp\MonoApplication\MonoApplication\Main.exe

在这里插入图片描述

我们可以看到命令行下输出了我们期望的Hello World,这意味着我们编写的程序现在运行在Mono中了,实际上在Windows下由Mono提供的C#编译器mcs.exe编译的IL文件双击是可以直接运行的,因为我们的计算机上安装了CLR,它作为.NET的一部分内置在我们的计算机中。由此我们会发现一个问题,我们这里的跨平台实际上是编译器、运行时和基础类库这三部分的跨平台,这意味着我们在Linux下运行.NET程序是需要Mono提供支持的。因为在这里我无法在Linux离线安装Mono,所以Linux下运行.NET程序的验证需要等博主以后有时间再来更新啦!可是我们可以想象到,通过C#编译器编译得到的可执行文件在Linux下是无法正常运行的,因为通常情况下Windows程序在Linux下运行是需要虚拟机环境或者Wine这样的软件来支持的,显然让这样一个Windows程序运行在Linux环境下是因为我们在Linux下安装了Mono。

谈谈Mono跨平台以后
  好了,到现在为止我们基本理清了Mono跨平台的原理。我们知道微软的技术体系在发展过程中因为某些历史遗留问题,.NET程序在不同的Windows版本中的兼容性有时候会出现问题,虽然微软宣布Windows XP停止维护,我们编写Windows应用程序的时候可以忽略对Windows XP版本的支持,可是因为国内用户不喜欢在线更新补丁的这种普遍现状,所以假如让用户在安装程序的时候先去安装.NET框架一定会降低用户体验,其次.NET框架会增加应用程序安装包的大小,所以我们需要一种能够让我们开发的.NET应用程序在脱离微软的这套技术体系时,同时能够安全、稳定的运行,所以我们这里考虑借助Mono让.NET程序脱离.NET框架运行。

首先,我们来说说.NET程序为什么能够脱离.NET框架运行,我们注意到Mono提供了一个Mono运行时,所以我们可以借助这样一个运行时来运行编译器生成的IL代码。我们继续以Hello World为例,我们在使用Mono编译出IL代码以后需要使用Mono运行时来解析IL代码,所以假如我们可以编写一个程序来调用Mono运行时就可以解决这个问题。在这个问题中,其实精简应用程序安装包的大小从本质上来讲就是解决基础类库的依赖问题,因为Mono实现了.NET框架中大部分的基础类库,所以移植.NET应用程序的关键是基础类库的移植,比如WinForm在Linux下的解决方案是GTK,这些细节在考虑跨平台的时候都是非常重要的问题。

小结
  本文从Mono跨平台的原理说起,探讨了.NET应用程序跨平台的可能性和具体实现。跨平台是一个涉及到非常多内容的话题,我个人理解的跨平台是要编写跨平台的代码,这意味着我们在编写程序的时候需要考虑减少对平台特性的移植,比如说Linq是一个非常棒的特性,可是这个特性离开了Windows、离开了.NET就没有办法得到保证,所以如果要让使用了Linq的应用程序跨平台就会是一件非常麻烦的事情!在不同的平台间保持相同的体验很难,就像我们编写的Web程序在不同的浏览器间都有着不一样的表现,所以跨平台这个问题我们就抱着学习的态度来研究吧!

相关文章

当我们远离最新的 iOS 16 更新版本时,我们听到了困扰 Apple...
欧版/美版 特别说一下,美版选错了 可能会永久丧失4G,不过只...
一般在接外包的时候, 通常第三方需要安装你的app进行测...
前言为了让更多的人永远记住12月13日,各大厂都在这一天将应...