众所周知,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以外的库:

1
2
3
4
5
6
7
8
9
10
11
12
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代码:

1
2
3
4
5
6
7
8
9
10
11
.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以后在其安装目录内可以找到。我们在命令行下输入命令:
    1
    mcs D:\项目管理\CSharp\MonoApplication\MonoApplication\Main.cs
  • 2、这样将生成Main.exe这样一个IL文件,现在我们需要一个运行时来解析它,在.NET下我们使用CLR来完成这个步骤,在Mono下我们使用mono.exe这个文件来完成这个步骤。我们在命令行下输入下列命令:
    1
    mono D:\项目管理\CSharp\MonoApplication\MonoApplication\Main.exe
在Mono中运行.NET程序
在Mono中运行.NET程序

我们可以看到命令行下输出了我们期望的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程序在不同的浏览器间都有着不一样的表现,所以跨平台这个问题我们就抱着学习的态度来研究吧!