Windows 应用的单文件分发 - 单一可执行文件与系统依赖的界限

· 更新日期: · · Windows, 分发, 单一可执行文件, .NET, C++, WebView2, WinUI

更新记录(2 条,最后更新 2026年09月03日)

本文的修改记录。已保存的更新前版本,可通过带有 DOI 的永久链接阅读。

本文此前是日文原文的节译,缺少大量章节、表格、Mermaid 图、图题、脚注与 FAQ。现已改写为日文原文的完整译文,技术主张与日文版一致,并补上了此前缺失的图与表,同时统一了全篇的术语译法。 查看更新前的版本 (DOI: 10.5281/zenodo.22276903)
补充了日文原文中已有的咨询引导(consultation_services)。正文内容没有改动。 查看更新前的版本 (DOI: 10.5281/zenodo.21615517)
首次发布
引用本文(DOI: 10.5281/zenodo.21615516)

本文保存于 Zenodo。以下同时提供始终指向最新版本的 DOI,以及固定于您正在阅读版本的 DOI。

小村 豪(2026)。《Windows 应用的单文件分发 - 单一可执行文件与系统依赖的界限》。小村软件有限公司。https://doi.org/10.5281/zenodo.21615516 https://comcomponent.com/zh-CN/blog/2026/03/19/003-windows-single-binary-and-os-dependencies/

DOI(最新版本)
10.5281/zenodo.21615516
DOI(此版本)
10.5281/zenodo.22282037

这篇文章源自一场闲聊,话题是“在 Windows 上,到底能把哪些情况称为单一可执行文件”。引出这个话题的帖子如下。即使跳过这一段,后面的内容也可以单独阅读。

在 Windows 上,“如果可以,希望只用一个文件分发”是相当普遍的需求。对于内部工具、设备联动工具、监控端、离线环境,以及尽量想避开安装程序的现场,做成单一可执行文件都很有吸引力。

不过,这个话题如果不先分清楚,讨论多半会在中途就对不上。因为在 Windows 上说“想做成单一二进制文件”时,其实很容易混进 4 种不同的需求。

  • 想让发布物只有 1 个
  • 想不用事先安装 .NET 或 Visual C++ 的运行时
  • 想不用安装程序、不需要管理员权限,放着就能跑
  • 想不受目标 Windows 差异的影响

这 4 件事并不相同。从实务角度看,这样理解最不容易跑偏。

把发布物收拢成 1 个 EXE,是相当可行的。 但要把对目标 Windows 的依赖降到零,是做不到的。

本文就从 Windows 应用实务的角度,把这条界线梳理清楚。

即使做成1个EXE也无法消除系统依赖想做成单一可执行文件这一需求中,容易混杂分发层面的诉求和依赖层面的诉求;把发布物收拢成1个EXE相当可行,但把对目标Windows的依赖降到零做不到。“想做成单一可执行文件”分发层面:想只有1个、想放着就能跑依赖层面:不需要运行时、不依赖系统收拢成1个EXE相当可行系统依赖无法降到零

图1:“想只用一个文件分发”里,混杂着能做到的事和做不到的事。

1. 先说结论

先把结论列出来。

  • 普通的桌面 EXE,可以做到相当高程度的 single binary 化
  • 但能做成 1 个 EXE和不依赖目标 Windows,是两件不同的事
  • Shell 扩展、Windows 服务、驱动、WebView2、部分 WinUI 3 场景,重点往往不在文件数量,而在于要向系统注册什么、以什么为前提
  • 实务上最重要的是分开决定:到底想做 single binary、想去掉安装程序,还是想降低系统依赖

换句话说,在 Windows 上这条界线是这样的。

  • 把发布物收拢成 1 个:相当可行
  • 把额外运行时一并带上:相当可行
  • 做到 xcopy 分发:取决于应用类型
  • 消除目标 Windows 侧的依赖:不可能
Windows上的界线把发布物收拢成1个以及随附额外运行时都相当可行,能否做到xcopy分发取决于应用类型,而消除目标Windows侧的依赖则不可能。把发布物收拢成1个相当可行随附额外运行时做到xcopy分发取决于应用类型消除系统侧的依赖不可能

图2:能做到的、取决于类型的、做不到的,三者的界线。

1.1 本文使用的术语

先把后文反复出现的词整理一下。

术语 读法与全称 本文中的含义
UCRT Universal C Runtime Visual Studio 2015 拆分 C 运行时后形成的标准 C 库部分。在 Windows 10 及以后,它作为系统的组成部分随系统提供
VC++ 可再发行组件包 Visual C++ Redistributable 用于把 UCRT 之外的 Visual C++ 运行时 DLL 装到目标机上的安装程序
framework-dependent 依赖目标环境 .NET 的发布 以目标机上已安装 .NET 运行时为前提的发布形态
self-contained 随附 .NET 运行时 由应用侧把整套 .NET 运行时一并带上分发的形态
single-file 单文件发布 把发布物合并成 1 个 EXE 的 .NET 发布选项
Native AOT Native Ahead-Of-Time 在发布时把 IL 预先编译为本机代码的发布形态。运行时不使用 JIT
app-local 分发 应用相邻分发 把 DLL 放在与 EXE 相同的文件夹中,只供该应用使用的分发方式
xcopy 分发 只需复制的分发 不使用安装程序也不需要管理员权限,整个文件夹复制过去就能运行的分发方式
SCM Service Control Manager,服务控制管理器 管理 Windows 服务注册、启动、停止的系统机制
UAC User Account Control,用户账户控制 控制提升到管理员权限的 Windows 安全机制
Shell 扩展 外壳扩展 被加载进资源管理器等进程中运行的 COM 组件
Evergreen 持续更新方式 把 WebView2 Runtime 交给一份自动更新的共享实例的分发模式
Fixed Version 版本固定方式 把特定版本的 WebView2 Runtime 随附到自己应用中的分发模式
arch architecture,CPU 架构 指 x86 / x64 / Arm64

图中实线表示始终成立的关系,虚线表示带条件的关系(成立条件写在详情页各关系的说明中)。关系的完整列表(共 22 条,附依据与确信度)以及主要概念的定义,汇总在知识地图详情页(日文)。数据:JSON-LD / Turtle

2. 把“单一可执行文件”拆成 4 个层次

先把这几个层次画成图,是这样的。自下而上越来越难,最上面一层在 Windows 上无法达到。

层次 A:发布物只有 1 个相当可行层次 B:无需预先安装语言运行时相当可行层次 C:无需安装或注册取决于应用类型层次 D:不依赖目标 Windows在 Windows 上无法达到

图3:单一可执行文件的4个层次。自下而上越来越难,层次D无法达到。

“想做成单一可执行文件”这句话,指的是这 4 层中的哪一层,需要做的工作完全不同。

2.1 层次 A:发布物只有 1 个

最表面的一层是这样的。

  • 用邮件发 1 个附件就能寄出
  • U 盘里放 1 个文件就够
  • 目标位置上只放一个 app.exe

这是发布单位外观层面的事。实际上,即使启动时会临时解压,即使依赖系统侧的 DLL,仅就这一条件而言也是能满足的。

2.2 层次 B:无需预先安装语言运行时

接下来是无需事先在目标机器上安装 .NET 运行时或 VC++ 可再发行组件包就能运行的状态。

  • C/C++ 的静态链接
  • .NET 的 self-contained
  • .NET 的 single-file
  • .NET Native AOT

到了这一层,“可以单独带走”的感觉就会明显强很多。

2.3 层次 C:无需安装或注册

从这里开始会突然变难。

单纯的 EXE 可能放着就能跑。但下面这些属于另一类。

  • Shell 扩展
  • Windows 服务
  • 自定义 URL scheme 或文件关联
  • 驱动
  • 会被资源管理器、Office 等其他进程加载的组件

这一类光靠把文件放上去是不够的,需要向系统注册,或者与宿主侧对接。

光放文件不够的领域Shell扩展、Windows服务、文件关联、驱动、会被其他进程加载的组件,光把文件放上去不够,需要向系统注册或与宿主侧对接。Shell扩展、服务光放文件不够文件关联、驱动会被其他进程加载的组件需要向系统注册、与宿主对接

图4:层次C之所以突然变难,是因为它属于需要向系统注册的领域。

2.4 层次 D:不依赖目标 Windows

这一层在 Windows 上做不到。

因为 Windows 应用最终仍然运行在 Windows 的 API、加载器、安全模型、设备栈之上。single binary 化能覆盖的,只到应用自身的责任范围,并不是把操作系统本身也一起带走。

3. 相当容易收拢成 1 个 EXE 的领域

在 Windows 上,也有相对容易收拢成 1 个 EXE 的应用。

  • 单独启动的桌面工具
  • EXE 自身同时承担 UI 和处理逻辑的业务应用
  • 通信、文件处理、日志收集、监控、设备控制这类工具
  • 不需要与资源管理器或 Office 做宿主整合的应用
  • 不以 Web 运行时为前提的 UI

这一类应用中,容易纳入应用本体的内容很多。

  • 自有代码
  • 资源文件
  • manifest
  • 默认配置
  • 模板数据
  • 部分第三方库
  • 语言运行时本体

此外,即使不把 DLL 完全嵌入 EXE 内部,把 DLL 放在 EXE 旁边的 app-local 分发在 Windows 上也是相当实用的常规做法。实务中,

  • 单独一个 app.exe
  • 或者 app.exe 加上少量相邻 DLL
  • 但无需安装程序、无需管理员权限,可以直接 xcopy 分发

这种形式往往比硬压缩成 1 个 EXE 更容易维护。

app-local分发这个折中点即使不把DLL完全嵌入EXE,只要做到把DLL放在EXE旁边、无需安装程序、无需管理员权限、可以xcopy分发,往往比硬压缩成1个EXE更容易维护。app.exe加上少量相邻DLL无需安装程序、无需管理员权限可以直接xcopy分发有时比硬压缩成1个EXE更容易维护

图5:不必执着于1个EXE,用app-local分发常常也能满足目的。

4. 即使做成 1 个 EXE 也无法消除的 Windows 依赖

如果以为“只要 EXE 只有 1 个,就不依赖目标 Windows”,就会在这里出问题。实际上,即使做成 1 个 EXE,仍有依赖会留下来。

4.1 操作系统版本依赖

Windows API 各自都有最低支持的系统版本。x64 与 Arm64 之间也有差异。也就是说,即使做成单个 EXE,下面这些也需要一开始就定下来。

  • 是否要支持到 Windows 10
  • 是否以 Windows 11 为前提
  • 是否也要在 Windows Server 上运行
  • 目标是 x86 / x64 / Arm64 中的哪一种

4.2 系统 DLL 依赖

即便自己觉得已经做成了 1 个 EXE,运行时当然仍在使用系统提供的组件。

  • kernel32.dll
  • user32.dll
  • advapi32.dll
  • COM 基础设施
  • 服务控制基础设施

这些属于 Windows 侧的责任范围。

4.3 安全模型依赖

  • UAC
  • 文件 ACL
  • 服务控制管理器
  • 注册表
  • 驱动签名策略

这些东西,应用无法独自承担。

做成1个EXE后仍然留下的依赖即使做成1个EXE,对Windows API最低支持系统版本与架构、kernel32.dll等系统DLL与COM基础设施、UAC与ACL、驱动签名策略等安全模型的依赖仍然存在。1个EXE的应用系统版本与arch系统DLL、COM基础设施安全模型这些都不是应用侧能承担的

图6:即使把发布物合并成一个,对Windows侧责任范围的依赖仍然留下。

4.4 宿主与运行时依赖

如果不是单独启动的 EXE,而是要挂在某个宿主之上的设计,依赖会一下子增多。

  • 使用 WebView2:需要 WebView2 Runtime
  • 使用 WinUI 3 / Windows App SDK:需要理清分发模式
  • 制作 Shell 扩展:需要向资源管理器侧注册

也就是说,UI 与整合方式的选择,往往直接变成分发的难度。

UI与整合的选择直接变成分发难度使用WebView2就需要WebView2 Runtime,使用WinUI 3就需要理清分发模式,制作Shell扩展就需要向资源管理器侧注册,宿主与运行时的选择直接变成分发的难度。使用WebView2需要Runtime使用WinUI 3需要理清分发模式制作Shell扩展需要向资源管理器注册

图7:离单独启动的EXE越远,依赖增长得越快。

5. 按技术类型看现实的落地点

5.1 原生 C/C++

原生 C/C++ 在 single binary 化上自由度较高。它有选择静态链接的余地,如果是单独启动的 EXE,相当容易收拢。

不过,比起把所有内容都塞进 1 个文件,下面这些在实务上更重要。

  • UCRT 和 VC++ 运行时怎么处理
  • 第三方 DLL 是否放到 app-local
  • 目标 CPU / 系统要收窄到什么范围

入口:/MT 与 /MD 的区别

在 MSVC 中,决定“要不要把运行时一并带上”的,实际上就是这个编译器选项。

选项 链接的内容 目标机上需要的东西
/MD ucrt.lib 和 vcruntime.lib 这两个供 DLL 使用的导入库。运行时使用 ucrtbase.dll 和 vcruntime<版本>.dll UCRT 和 VC++ 运行时
/MT 静态链接 libucrt.lib、libvcruntime.lib、libcmt.lib 不需要额外运行时

最小的试法,在 Developer Command Prompt 里只要这一行。

cl /std:c++17 /EHsc /O2 /MT app.cpp /Fe:app.exe

这里有 3 点需要注意。

  • UCRT 是在 Visual Studio 2015 拆分 C 运行时时成为 Windows 的组成部分的,Windows 10 及以后作为系统的一部分随系统提供。如果要面向更早的 Windows,就需要通过 vcredist 再发行。
  • 不推荐用 /MT 构建 DLL。静态链接的 CRT 会把状态封闭在该 DLL 内部,于是 EXE 与 DLL 之间在内存分配、区域设置、_set_se_translator 的生效范围上会出现错位。如果笼统地统一成“EXE 用 /MT,随附的 DLL 也用 /MT”,跨边界的分配与释放就会出问题。
  • /clr 和 /MT 不能同时使用。如果混有 C++/CLI,就只能走 /MD 这一侧。
静态链接的3点注意UCRT在Windows 10及以后随系统提供但更早的Windows需要用vcredist再发行、用/MT构建DLL会把CRT状态封闭在DLL内导致跨边界分配与释放出问题、/clr与/MT不能同时使用。用/MT做静态链接较早的Windows需要vcredist不推荐DLL侧使用/MT不能与/clr同时使用跨边界的分配与释放会出问题

图8:/MT很强,但要注意UCRT的前提、DLL边界、C++/CLI这3点。

5.2 .NET

.NET 有 single-file、self-contained、Native AOT,表面上的发布单位可以做得相当小。

但还是要区分清楚。

  • framework-dependent:依赖目标环境的 .NET
  • self-contained:把 .NET 运行时一并带上
  • single-file:把发布物收拢成一个
  • Native AOT:能进一步减少启动期依赖,但也有功能限制

“因为是 single-file,所以系统依赖会变少”并不成立。变少的主要是应用发布物的集合数量。

single-file减少的是发布物的集合数量framework-dependent依赖目标环境的.NET,self-contained把运行时一并带上,single-file只是把发布物收拢成一个,并不是因为用了single-file系统依赖就变少。framework-dependent依赖目标环境的.NETself-contained把.NET运行时一并带上single-file把发布物收拢成一个并不因此减少系统依赖

图9:.NET的每种发布形态,减少的东西和留下的东西都不一样。

入口:dotnet publish 的 3 种模式

如果想先跑起来看看区别,下面这 3 条命令就够了。

rem 1. framework-dependent + single-file: 目标机上需要 .NET 运行时
dotnet publish -c Release -r win-x64 --self-contained false -p:PublishSingleFile=true

rem 2. self-contained + single-file: 把 .NET 运行时一并带上
dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true

rem 3. Native AOT: 先在 csproj 中写上 PublishAot 再发布
dotnet publish -c Release -r win-x64

官方推荐把 PublishSingleFile 和 PublishAot 写在 csproj 里,而不是写在命令行上。这样构建过程中的兼容性分析会生效,减少发布之后才发现问题的情况。

<PropertyGroup>
  <PublishSingleFile>true</PublishSingleFile>
  <PublishAot>true</PublishAot>
</PropertyGroup>

另外,指定 RuntimeIdentifier 后 SelfContained 默认变为 true。想要 framework-dependent 时,请显式写上 --self-contained false。要在 Windows 上发布 Native AOT,需要安装 Visual Studio 的使用 C++ 的桌面开发工作负载。

权衡:“合并成 1 个”不是免费的

这里如果只写“能做到”就会出问题,所以把代价也一并列出来。

single-file 的启动时解压

  • 默认情况下,被打包进去的只有托管 DLL。它们在启动时被读入内存,不会解压到文件夹。运行时本体的本机二进制文件仍然是独立文件。
  • 如果想把本机二进制文件也一并放进 1 个文件,用 IncludeNativeLibrariesForSelfExtract;如果想让全部内容在运行前解压出来,用 IncludeAllContentForSelfExtract。两者都会变成“先解压再启动”的行为,在 Windows 上会解压到 %TEMP%\.net 之下。可以用 DOTNET_BUNDLE_EXTRACT_BASE_DIR 更改位置,但不要把它设成权限不同的用户或服务可以写入的位置。
  • 启用 EnableCompressionInSingleFile 后 EXE 会小很多,但启动时要在内存中解压,启动会变慢。官方的说法也是“使用之前请同时测量体积和启动开销”。影响程度因应用而异,差别很大。

single-file 的 API 不兼容

把发布物合并成一个之后,以文件路径为前提的代码会悄悄失效。

API 在 single-file 下的行为
Assembly.Location 返回空字符串
Assembly.CodeBase PlatformNotSupportedException
Assembly.GetFile IOException
Module.Name 返回 <Unknown> 这个字符串

如果要访问 EXE 旁边的文件,改用 AppContext.BaseDirectory;如果需要可执行文件的路径,改用 Environment.ProcessPath。第三方库内部也可能在用这些 API,所以做成 single-file 之后,至少要在真机上启动确认一次。

以路径为前提的代码会悄悄失效把发布物合并成一个之后Assembly.Location会返回空字符串等,以文件路径为前提的API行为会改变,因此EXE旁边的文件改用AppContext.BaseDirectory,可执行文件路径改用Environment.ProcessPath,并至少在真机上启动确认一次。做成single-file以路径为前提的API行为改变旁边的文件用AppContext.BaseDirectory可执行文件用Environment.ProcessPath至少在真机上启动确认一次

图10:做成single-file后,要替换取路径的方式并在真机上确认。

Native AOT 的功能限制

作为“启动快、不需要运行时”的代价,下面这些将无法使用。

  • Assembly.LoadFile 这类动态加载
  • System.Reflection.Emit 这类运行时代码生成
  • C++/CLI
  • 在 Windows 上是内置 COM
  • 由于必须做剪裁,剪裁本身的限制也会原样带上
  • 由于实质上就是 single-file,上面的 API 不兼容也会原样带上
  • System.Linq.Expressions 会始终以解释方式运行,比运行时生成的代码慢

目标平台也是写死的。在 .NET 8 中 Windows 支持 x64 和 Arm64,.NET 9 及以后加上了 x86。“用 AnyCPU 做成 1 个”这种想法从一开始就不成立。

Native AOT的代价Native AOT能减少启动期依赖,代价是无法使用动态加载和运行时代码生成、C++/CLI、Windows的内置COM,剪裁和single-file的限制也会带上,目标平台同样是写死的。Native AOT能减少启动期依赖无法动态加载、运行时生成无法用C++/CLI、内置COM剪裁和single-file的限制也带上目标系统与arch写死

图11:看Native AOT的好处时,要连同失去的功能一起看。

5.3 WebView2

一旦采用 WebView2,single binary 的难点会完全改变。这里真正的主题不是 EXE 的数量,而是如何处理 WebView2 Runtime。

比起“能否做成 1 个 EXE”,有些问题应该先想清楚。

  • 是否以目标环境已有 Runtime 为前提
  • 是否使用 Evergreen
  • 是否随附 Fixed Version
  • 离线分发的责任要扛到什么程度

Evergreen 与 Fixed Version 的区别,大致是这样。

  Evergreen Fixed Version
由谁更新 Microsoft,自动更新 自己,随应用更新一起替换
机器上的实体 所有应用共享一份 每个应用各自随附
分发体积 几乎不增加 超过 250 MB
安装方式 Bootstrapper 约 2 MB,按需下载所需内容。离线场景则随附 Standalone Installer 把解压出来的整套二进制文件与应用一起分发
既有约束 需要在启动前确认机器上是否已安装 无法从网络路径或 UNC 路径运行

Evergreen Runtime 在 Windows 11 上作为系统的一部分预装。但 Windows 10 那一侧仍有未安装的机器,因此 Microsoft 自己也写道“即使选择 Evergreen,也最好把 Runtime 一并分发”。也就是说,无论选哪一种,由应用侧确认是否已安装、不足就补装的流程都是必要的。Fixed Version 的代价是发布物会增加几百 MB,换来的是“更新时机由自己掌握”。这一点必须先于 1 个 EXE 的话题定下来。

WebView2要先决定分发方式Evergreen是由Microsoft自动更新的共享实例但机器上可能未安装,Fixed Version能自己掌握更新时机但发布物增加几百MB,无论选哪一种都需要由应用侧确认是否已安装、不足就补装。采用WebView2Evergreen:共享一份并自动更新Fixed Version:自己随附并更新确认是否已安装,不足就补装发布物增加几百MB

图12:WebView2的主题不是EXE的数量,而是Runtime的处理方式。

5.4 WinUI 3 / Windows App SDK

WinUI 3 同样在采用的那一刻就改变了分发要求。UI 技术的选择,本身就等同于分发方式的选择。

具体来说,要决定的有 2 个维度。

  • packaging:packaged(MSIX)、指向外部位置的 packaged,还是 unpackaged
  • runtime:framework-dependent 还是 self-contained

而从 single binary 的角度看,真正起作用的是下面这些组合约束。

  • 能使用 PublishSingleFile 的,只有 unpackaged 且 self-contained 的 WinUI 3 应用,并且需要 Windows App SDK 1.5 及以后。
  • packaged 的应用,以及指向外部位置的 packaged 应用,都无法使用 PublishSingleFile。
  • 一旦改为 unpackaged 就会失去 package identity,于是通知、后台任务、文件关联、上下文菜单扩展这些以 package identity 为前提的 Windows 功能都无法使用。

也就是说,在 WinUI 3 上,“做成 1 个 EXE”和“使用 Windows 的扩展功能”是正面冲突的。如果把 single binary 放在最优先,先重新审视 UI 技术的前提往往更快。

1个EXE与package identity的冲突在WinUI 3上能使用PublishSingleFile的只有unpackaged且self-contained的组合,而改为unpackaged就会失去package identity,通知和文件关联等功能都用不了,因此1个EXE与Windows的扩展功能正面冲突。想使用PublishSingleFile只有unpackaged加self-contained失去package identity通知、文件关联等无法使用1个EXE与扩展功能正面冲突

图13:在WinUI 3上,选择1个EXE就意味着舍弃package identity。

6. 本质上就需要“注册与依赖”的领域

6.1 Shell 扩展

被资源管理器加载的 Shell 扩展,与单纯“放着就能跑的 EXE”是两回事。这里的主题不是文件数量,而是如何向资源管理器注册。

6.2 Windows 服务

即便服务本体的 exe 能做成一个文件,分发仍是另一个问题。需要考虑

  • 向 SCM 注册
  • 权限
  • 启动账户
  • 恢复设置

这些内容。也就是说,服务属于“如何安装”比“做成 1 个 EXE”更需要打磨的领域。

6.3 驱动

驱动更加明确。它要连同 INF、签名、安装流程一起才能成立,因此从一开始就很难站上 single binary 的擂台。

主题落在注册与签名上的领域Shell扩展需要向资源管理器注册,Windows服务需要向SCM注册并设计权限和启动账户,驱动要连同INF和签名一起才能成立,因此主题落在注册与依赖的设计上而不是文件数量。Shell扩展向资源管理器注册服务SCM注册、权限、账户驱动INF与签名比起文件数量,注册的设计才是主题

图14:在这3个领域里,要打磨的是“如何注册”而不是“做成1个EXE”。

7. 实务判断表

想做粗略判断时,这张表比较好用。

想做的东西 做成 1 个 EXE 的现实程度 应先考虑的事
单独启动的 Win32 / C++ 工具 高 静态链接、目标系统 / arch
单独启动的 WinForms / WPF 工具 高 self-contained、single-file、Native AOT 是否适用
WinUI 3 / Windows App SDK 应用 中 分发模式、额外依赖
基于 WebView2 的桌面 UI 低到中 Runtime 的分发方式
资源管理器右键扩展或预览 低 COM / 注册表注册
Windows 服务 中 SCM 注册、权限、更新流程
附带驱动的应用 低 INF、签名、安装

这张表里最重要的是让人明白:“二进制文件的数量”和“分发的责任范围”是两回事。

8. 分发设计中应先决定的事

想让 single binary 化成功,有些事情比实现更值得提前决定。

8.1 先决定想把什么合并成 1 个

  • 是想让发布物只有 1 个
  • 是想省掉运行时的预先安装
  • 是想去掉安装程序
  • 是想让离线更新更简单

答案不同,选择的技术也会不同。

8.2 一开始就固定最低支持的 Windows 版本和 arch

single-file 和 Native AOT 基本上都是 OS / architecture specific 的。如果这一点还含糊不清就一路推进“总之先做成一个文件”,最后往往会被 API 缺失或 runtime 不匹配卡住。

8.3 把“随应用一起分发的内容”和“交给 Windows 处理的内容”写清楚

实务中,只要把下面这张表提前写出来,出问题的情况就会明显减少。

  • 随应用一起分发的内容
    • 本体 exe
    • 自有 DLL
    • 配置模板
    • self-contained runtime
  • 交给 Windows 处理的内容
    • 系统 DLL
    • 系统 API
    • SCM / 注册表 / 资源管理器
    • 驱动基础设施
  • 另外作为前提的内容
    • WebView2 Runtime
    • VC++ Redistributable
    • Office / Excel
    • 专用驱动
把责任范围的3种分类写清楚只要把随应用一起分发的内容、交给Windows处理的内容、另外作为前提的内容这3种分类写下来,分发环节出问题的情况就会明显减少。发布物的责任范围随应用一起分发的内容交给Windows处理的内容另外作为前提的内容只要写下来问题就会减少

图15:把随附、交给系统、另外作为前提这3种分类提前写清楚。

8.4 如果优先考虑 single binary,就减少宿主整合

这一点相当有效。

  • 放弃 Shell 扩展,改用普通 EXE
  • 不做成服务,改用任务计划程序或显式启动
  • 不使用 WebView2,改用原生 UI
  • 把 COM 封闭在自己的进程内

归根结底,越是减少“让系统加载”“向系统注册”这类设计,就越接近 single binary。

越减少宿主整合就越接近放弃Shell扩展改用普通EXE、不做成服务改用任务计划程序或显式启动、不使用WebView2改用原生UI,越是减少让系统加载和向系统注册的设计,就越接近single binary。放弃Shell扩展改用普通EXE减少向系统注册的设计不做成服务改用显式启动不用WebView2改用原生UI越来越接近single binary

图16:如果优先考虑single binary,就要减少宿主整合本身。

9. 总结

在 Windows 上做 single binary 化,能做到的程度相当高。但最终落脚点就是这一句话。

可以把应用做成 1 个 EXE。 但这个应用所依赖的 Windows,无法一并做进这 1 个 EXE。

特别值得记住的有 5 点。

  • 单独启动的普通 EXE,可以相当接近单文件分发
  • C/C++ 的静态链接、.NET single-file、Native AOT 都是有力选项
  • 但对系统版本、arch、系统 DLL、安全模型的依赖不会消失
  • Shell 扩展、服务、驱动、WebView2、部分 WinUI 3 场景,主体会变成系统注册和额外运行时的问题
  • single binary 成败的关键,在于一开始就分清楚“想把什么合并成 1 个”

如果非常看重 single binary,在技术选型阶段就朝着降低与系统的耦合度的方向去设计,成功率会高得多。

成功的关键在于与系统的耦合度可以把应用做成1个EXE,但无法把它依赖的Windows也做进这1个EXE,因此如果非常看重single binary,就应在技术选型阶段朝着降低与系统耦合度的方向设计。把应用做成1个EXE做得到把依赖的Windows也做进去做不到所以在技术选型时降低与系统的耦合度

图17:能否做成1个EXE,取决于最初对与系统耦合度的设计。

10. 参考资料

共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。

与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。

本文与以下服务页面相关联,欢迎从最接近的入口查看。

常见问题

汇总了咨询这一主题时常见的问题。

Windows 应用可以只用一个 EXE 文件分发吗?
如果是单独启动的桌面工具,可以做到相当高的程度。使用 C/C++ 的静态链接、.NET 的 self-contained 或 single-file、Native AOT,都能把发布物合并成一个 EXE。但能做成 1 个 EXE 和不依赖目标 Windows 是两件不同的事,对系统版本、架构、系统 DLL、安全模型的依赖并不会消失。
.NET 的 single-file 是不是就意味着不再依赖系统?
不是。single-file 主要减少的是应用发布物的集合数量,而不是对系统的依赖。framework-dependent 依赖目标环境已安装的 .NET,self-contained 会把 .NET 运行时也一并带上,Native AOT 能进一步减少启动期依赖,但会带来功能上的限制。single-file 和 Native AOT 基本上都与系统版本和架构相关,因此需要先确定最低支持的 Windows 版本和目标架构。
哪些应用类型很难做成单个 EXE?
Shell 扩展、Windows 服务、驱动、基于 WebView2 的 UI、部分 WinUI 3 场景都比较困难。这些应用的重点不在文件数量,而在于需要向系统注册什么,或依赖哪些额外运行时:Shell 扩展需要向 Explorer 注册,服务需要向 SCM 注册并设计权限和启动账户,驱动需要 INF 文件和签名才能成立,因此只是把文件放上去并不成立。WebView2 则需要先决定 WebView2 Runtime 的分发方式。
有没有比硬塞进单个 EXE 更好的分发方式?
把 DLL 放在 EXE 旁边的 app-local 分发是很有力的选择。哪怕是 app.exe 加上少量相邻 DLL 这种形式,只要能做到无需安装程序、无需管理员权限、可以直接 xcopy 分发,往往比硬压缩成 1 个 EXE 更容易维护。关键是要先分清楚:到底是想把发布物合并成一个,还是想省掉运行时的预先安装,还是想去掉安装程序。

作者简介

本文作者的个人简介页面。

Go Komura

小村软件有限公司 代表

以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。

返回博客列表