众所周知,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 程序在不同的浏览器间都有着不一样的表现,所以跨平台这个问题我们就抱着学习的态度来研究吧!