二十多年前,.NET 为 Windows 而诞生—— CLR、Winform、IIS,样样都和 Windows 深度绑定。今天,一个 .NET 应用可以毫无违和感地跑在 Ubuntu 容器、macOS 笔记本,甚至树莓派上。这篇文章简单介绍一下.NET跨平台的核心原理,以及微软为了“跨平台”对 .NET 做了哪些改造。
.NET 跨平台的底层逻辑:不要让应用代码直接对话操作系统,中间垫一层"翻译官"。
具体拆开看是两层隔离:
.NET 程序的运行逻辑:
App(IL) → BCL(基础类库) → CoreCLR/CLR(JIT + GC + 类型系统) → PAL(平台适配层) → OS
跨平台架构
几个关键角色:
System.IO、System.Threading、System.Net 等等,这是应用代码实际调用的 API 平面。BCL 内部会调用 CoreCLR 提供的运行时服务,但对外暴露的是统一、与操作系统无关的接口——这是"屏蔽平台差异"的第一道墙。dotnet/runtime 仓库里 src/coreclr/pal 目录下,Unix-like 系统上的 PAL 实现负责把这些"类 Win32"调用翻译成 pthreads、epoll/kqueue 之类的原生系统调用。一句话总结:BCL 负责让"上层 API 长得一样",PAL 负责让"底层实现能落地",JIT 负责让"同一份中间码能变成不同 CPU 能听懂的指令"。三者叠在一起,才是 .NET 跨平台的完整拼图。
.NET 应用最终交付给用户/服务器时,有三种打包策略,本质区别在于"运行时放哪儿、要不要提前编译成本机码"。
只发布应用自身的 IL 程序集,运行时依赖目标机器上已安装的共享 .NET Runtime。就是用这种方式做成来的程序,如果想要在电脑上执行,你就必须首先在你电脑(Windows, Linux或者MacOS)上从微软的官网下载并安装.NET Runtime。
dotnet publish -c Release -r linux-x64 --self-contained false
把整个 .NET Runtime(含 CoreCLR、BCL)连同应用一起打包,目标机器不需要装任何东西。
dotnet publish -c Release -r win-x64 --self-contained true
.NET 7 开始成熟的发布模式,编译期直接把 IL 编译成目标平台的单一本机可执行文件,运行时不再需要 JIT,只内嵌一个精简过的运行时(GC、最小反射支持等)。
dotnet publish -c Release -r linux-x64 -p:PublishAot=true
System.Reflection.Emit 不可用,动态 Type.GetType 受限),一些依赖运行时反射的库(早期版本的某些序列化框架)需要适配。三者不是互斥的,很多团队会用 SCD 兜底通用场景,同时给性能敏感的边缘服务单独做一版 AOT。
早期的 .NET Framework 本质上是"Windows 的一部分",跟 Win32、注册表、GDI+ 这些东西是长在一起的。真正意义上的跨平台,是靠一系列结构性重写完成的,远不止你列的三项,这里补充完整一点:
.NET Framework 的 BCL 里有大量假设自己运行在 Windows 上的代码(比如直接调用 Windows 注册表、依赖 GDI 做图形)。.NET Core 时代对 BCL 做了近乎重写:抽掉所有 Windows 专属实现,把平台差异下沉到 PAL / 各平台专属的 Native 库(System.Native、System.Security.Cryptography.Native 等)里。
.NET Standard 则是在 .NET Framework、.NET Core、Xamarin、Mono 生态并存的过渡期,用来解决"类库要不要跨平台"的规范问题——它定义了一套所有 .NET 实现都必须支持的 API 集合,类库作者只要面向 .NET Standard 编译,就能同时被 Framework 和 Core 引用。到 .NET 5 之后,随着各实现统一到一条主线,.NET Standard 的历史使命基本完成,现在推荐直接面向 net8.0/net9.0 这样的 TFM 开发。
.NET Framework 时代,ASP.NET 网站几乎离不开 IIS——请求管道、模块(HttpModule/HttpHandler)都是 IIS 概念的延伸,天然是 Windows-only。ASP.NET Core 引入了完全托管、跨平台的内置 Web 服务器 Kestrel,基于 libuv(早期)/后来切换到自研的跨平台 Socket 传输层,本身不依赖任何 Windows 特有组件。IIS/Nginx 这类服务器,现在的角色降级为反向代理(可选),而不是运行时的必需品。
.NET Framework 时代离开 Visual Studio 几乎无法构建项目(.csproj 格式冗长、依赖 VS 生成的 GUID 和大量隐式约定)。微软重写了 dotnet CLI(dotnet build/dotnet run/dotnet publish 等命令),并把 MSBuild 本身开源、跨平台化,配合 SDK 风格的精简 .csproj(几行就能描述一个项目),使得整个构建链路可以在纯命令行、任意编辑器(VS Code、Rider、Vim)下完成,彻底摆脱对 Visual Studio GUI 的依赖。
2014-2015 年前后,微软把整个 .NET Core 运行时(CoreCLR)、基础类库(CoreFX)、编译器(Roslyn)陆续开源到 GitHub,并接受社区 PR。这一步在工程层面看似"只是开源",实际意义重大:Linux/macOS 上的适配工作(PAL 的完善、各种 Native 库的移植)很大程度上是社区和微软工程师一起在 GitHub 上磨出来的,而不是微软内部闭门造出来再空投。
配合工具链重写,NuGet 包管理也做了对应升级,跨平台的包还原、PackageReference 取代旧的 packages.config,让同一个项目文件在 Windows/Linux/macOS 上用同一套命令就能还原依赖、构建、发布,不再依赖 Windows 特有的路径约定。
微软官方持续维护跨发行版的 Docker 基础镜像(Debian/Alpine/Ubuntu 变体),并持续压缩镜像体积、优化容器内 GC 行为(比如识别 cgroup 内存限制自动调整堆大小)。这一步严格说不算"跨平台原理",但从工程实践角度看,是让 .NET 真正被云原生生态接纳的关键推动力——没有这些优化,"能跑在 Linux 上"和"适合被大规模运维"完全是两回事。
.NET 6 把原本独立发展的 Mono 运行时并入统一的 .NET 主线,使得同一套 BCL 之上可以有两种运行时后端:CoreCLR(服务器/桌面)和 Mono(移动端 iOS/Android、浏览器 WASM、体积敏感场景)。这一步补上了 CoreCLR 天然覆盖不到的领域——浏览器内和移动设备。
.NET 和 Java 在"跨平台怎么做"这个问题上,答案表面上高度相似,都是"中间语言 + 托管运行时"的路数。放在一起看:
| 维度 | .NET | Java |
|---|---|---|
| 中间表示 | CIL(公共中间语言) | JVM 字节码 |
| 执行引擎 | CoreCLR(JIT,还有 Native AOT 可选) | JVM(HotSpot 等,JIT 为主,GraalVM 可 AOT) |
| 平台适配层 | PAL,源自 SSCLI/Silverlight | JVM 自身按平台分发原生实现,无独立命名的"PAL"概念,但思路等价 |
| 语言 | 多语言但事实标准是 C# | 多语言(Java/Kotlin/Scala/Groovy)但事实标准是 Java |
| 历史包袱 | 曾深度绑定 Windows,靠工程重写"脱钩" | 设计之初就以"Write Once, Run Anywhere"为目标 |
| 主导方 | 微软主导,逐步开源 | Oracle/OpenJDK 长期开源治理,多厂商共建(Eclipse Adoptium、Azul 等) |
似乎又没什么“本质上”的区别,但可以从历史起点和生态治理模式这点看一下:
dotnet/runtime),虽然开源、社区可以贡献,但架构决策权集中。.NET 的跨平台不是一句"支持 Linux 了"就能实现的营销话术,而是从 BCL 重写、PAL 抽象、JIT/AOT 双引擎,到工具链开源、服务端组件解耦这一整套系统工程堆出来的结果。
暂无评论,快来抢沙发吧!