本文分享我如何通过 .NET 10 Native AOT 和交叉编译技术,将一个原本动辄 100MB 的应用压缩到 16MB,并在资源极度受限的环境中实现流畅运行的实战经验。

1. 背景
在嵌入式领域,C/C++ 一直是绝对的主角。但随着 .NET 的演进,Native AOT让 C# 开发者也能在资源极度受限的 SoC 上大展身手。
本文记录了如何将一个 ASP.NET Core 管理后台,完美塞进瑞芯微 RK3506 (224MB 内存 / 128MB Flash)仅有的 38MB 可用分区中。
2. 痛点:工业级硬件的"寸土寸金"
RK3506 是一款高性价比的国产工业芯片,但在我们的实战场景中,环境极其苛刻:
- 存储匮乏 :用户数据分区
/userdata仅剩 38MB。 - 内存敏感:仅有 224MB 物理内存。
- 架构差异:开发机为 x86_64,目标机为 ARM32 (armhf / ARMv7-a)。
常规的 .NET Self-contained 发布包动辄 60MB-100MB,显然是"超重"了。
3. 方案 A:Native AOT 与交叉编译沙盒
为了解决体积和跨平台编译的依赖冲突,我选择了 Docker + Native AOT 的方案。
在 WSL2 或 Linux 宿主机上配置 armhf 交叉编译链时,经常会遇到 glibc 版本冲突或 apt 源架构 404 的问题。使用官方的 .NET 10 SDK 镜像 可以获得一个纯净的编译沙盒。
以下便是核心的编译指令,这里有一个硬核的技巧:通过 -p:LinkerFlavor=lld 强制使用 LLVM 的链接器,解决 x64 宿主机对 ARM 链接模式不识别的问题。
bash
# 核心编译指令
docker run --rm -v "$(pwd):/app" -w /app mcr.microsoft.com/dotnet/sdk:10.0 bash -c " \
dpkg --add-architecture armhf && apt-get update -qq && \
apt-get install -y -qq clang gcc-arm-linux-gnueabihf zlib1g-dev:armhf lld && \
dotnet publish ./bweb/bweb.csproj -c Release -r linux-arm \
-p:PublishAot=true \
-p:InvariantGlobalization=true \
-p:LinkerFlavor=lld \
-o ./dist-aot"
4. 方案 B:WSL2 原生构建
第一个方案是避坑的首选,省的折腾环境了。
如果你是一名追求极致性能的"强迫症"开发者,不希望依赖 Docker,也可以直接在 WSL2 (这里用的是 Ubuntu 24.04) 中硬核出包。
当然,在开始前,需要先安装好交叉编译工具链:
bash
# 基础构建工具与 Clang
sudo apt install clang lld zlib1g-dev -y
# ARM32 交叉编译器 (提供库搜索路径)
sudo apt install gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf -y
# 安装目标架构的 C 库支持
sudo apt install libc6-dev-armhf-cross binutils-arm-linux-gnueabihf -y
由于 AOT 交叉链接需要手动"指路",我们需要显式指定交叉编译库的搜索路径。以下是核心的编译指令:
bash
dotnet publish ./bweb/bweb.csproj -c Release -r linux-arm \
-p:PublishAot=true \
-p:PublishTrimmed=true \
-p:InvariantGlobalization=true \
-p:CppCompilerAndLinker=clang \
-p:LinkerFlavor=lld \
-p:ObjCopyName=arm-linux-gnueabihf-objcopy \
-p:SysRoot=/ \
-p:CustomLinkerArgs="--target=armv7-linux-gnueabihf -L/usr/lib/gcc-cross/arm-linux-gnueabihf/$(ls /usr/lib/gcc-cross/arm-linux-gnueabihf/ | head -n 1) -L/usr/arm-linux-gnueabihf/lib" \
-o ./dist-aot
这里使用了 CustomLinkerArgs 来指定链接器的目标架构和库搜索路径,确保编译器能够正确找到 ARM 版本的 C 库。ls /usr/lib/gcc-cross/arm-linux-gnueabihf/ | head -n 1 是为了动态获取安装的交叉编译器版本,避免硬编码路径。一般来说,这个版本号会是 13。
5. 极致瘦身:压榨每一 KB 空间
为了让程序在 38MB 的分区里住得舒服,我又做了三层裁剪:
- 策略裁剪 :开启
InvariantGlobalization。在嵌入式 Web 后台场景中,我们通常不需要复杂的国际化 ICU 库,这一项就能省下约 25MB。 - 静态裁剪 :Native AOT 默认开启
Trimmed。它会扫描代码树,未被引用的库(如某些未使用的 Json 序列化程序)将不会被编译进二进制。 - 人工裁剪 :
- 移除
.dbg(调试符号):AOT 生成的符号文件往往比程序还大。 - 移除
appsettings.Development.json。 - 保留 Web 预压缩文件 :虽然占用了一点空间,但保留
.gz和.br文件可以让低频的 RK3506 避免实时压缩 CPU 损耗,也算是性能平衡的艺术吧。
- 移除
6. 战果总结
由于没有了 JIT,启动时的内存和CPU抖动消失了。通过 top 观察,程序的 RSS(实际驻留内存)非常稳定。
最终,我们的 ASP.NET Core 程序在 RK3506 上跑出了如下成绩:
- 部署包体积 :压缩后仅 16MB 左右。
- 启动时间 :从执行命令到监听成功,体感时间在 1秒以内。
- CPU 占用 :在 AOT 机器码加持下,Idle 空闲率从 82% 提升至 88%。
.NET 10 Native AOT 已经完全具备了在国产工业芯片上取代传统嵌入式开发语言的实力。它让我们可以用高效的 C# 语法,写出 C++ 级别的性能。更有强大的 LINQ、异步编程、完善的 NuGet 生态支持,真正实现了"高效开发,极致性能"的双赢。