软件设计

[模块化 #4] 使用 SPM 开始 iOS 模块化(本地包、static、dynamic 全面总结)

在前面的模块化系列中,我们梳理了边界、依赖方向和分层结构等基本原理。现在该把它们应用到实际的 iOS 项目中了。

4 分钟阅读
[模块化 #4] 使用 SPM 开始 iOS 模块化(本地包、static、dynamic 全面总结) 封面图

在前面的模块化系列中,我们梳理了边界、依赖方向和分层结构等基本原理。现在该把它们应用到实际的 iOS 项目中了。

在 iOS 中拆分模块,实际上有两种工具:Xcode framework target,以及 Swift Package(SPM,Swift Package Manager)。

先说结论:以 2026 年为准,重新开始模块化时,SPM 是默认选择。只需使用文件夹和 Package.swift,就能创建模块,不会产生项目文件(pbxproj)冲突。

本文将介绍如何使用 SPM 创建本地模块、它与 framework target 的区别,以及选择 static 或 dynamic 链接方式的标准。


为什么选择 SPM,而不是 framework target?

也可以在 Xcode 中通过 File → New → Target → Framework 创建模块。很长一段时间里,这都是标准做法。

问题在于这种方式留下的痕迹。每添加一个 target,project.pbxproj 就会改动几十行;如果两名团队成员同时操作 target,就会出现合并冲突。

SPM 本地包则不同。

对比 Framework target SPM 本地包
模块定义位置 project.pbxproj Package.swift(声明式代码)
合并冲突 频繁 少见(文件是人类可读的代码)
添加模块的成本 操作 Xcode GUI 文件夹 + 添加几行代码
精细的构建设置 自由 有限

模块越多,“添加一个模块的成本”越会决定整体结构。添加模块如果令人畏惧,就不会有人愿意拆分。

除非必须非常精细地控制构建设置,否则本地 SPM 包在维护成本方面更有优势。


如何创建本地 SPM 包?

在应用项目旁创建一个文件夹,再放入 Package.swift,就完成了。我们直接搬用上一篇文章的分层结构。

// Modules/Package.swift
let package = Package(
    name: "Modules",
    products: [
        .library(name: "FeatureSearch", targets: ["FeatureSearch"]),
        .library(name: "CoreNetwork", targets: ["CoreNetwork"]),
    ],
    targets: [
        .target(name: "FeatureSearch", dependencies: ["CoreNetwork"]),
        .target(name: "CoreNetwork"),
        .testTarget(name: "FeatureSearchTests", dependencies: ["FeatureSearch"]),
    ]
)

将这个包拖入 Xcode 项目,并把库连接到应用 target,之后 import FeatureSearch 就能正常工作。

关键在于,依赖关系会以代码形式明确写在 dependencies 数组中。如果 FeatureSearch 不应使用 CoreNetwork,就从数组中删除;偷偷 import 会产生编译错误。

上一篇文章所说的“由编译器强制边界”,就是这样建立的。

dependencies 数组会直接成为依赖图
dependencies 数组会直接成为依赖图

除了像上面这样在一个包中放置多个 target,也可以为每个模块分别使用一个包。模块较少时,单包多 target 更易管理;达到几十个规模后,许多团队会按领域拆分包。


static 和 dynamic,该选哪个?

声明 .library 时可以指定链接方式。默认值是由 Xcode 自动决定的 automatic,而且大多数情况下会按 static 处理。

  • static:模块代码会复制并合并到应用二进制文件中。应用启动时无需额外加载
  • dynamic:模块以独立的 framework 文件存在,并在运行时加载

选择标准如下。

情况 选择
一般功能或核心模块 static(保持默认值)
应用、组件和扩展共享同一个模块 dynamic(消除二进制重复)
应用启动时间敏感,但有几十个 dynamic 模块 考虑整合为 static

dynamic framework 越多,应用启动时的加载成本越高。Apple 在 WWDC session 中一直建议这样做,因此除非有特殊理由,保持 automatic(实际上是 static)通常更稳妥。


什么时候只用 SPM 会不够?

从 SPM 模块化开始并顺利使用后,团队变大时仍会遇到一些限制。

  • 应用 target 本身的设置(签名、scheme 和构建设置)仍保留在 pbxproj 中,因此仍可能发生冲突
  • 模块达到几十个后,管理 Package.swift 和统一模块模板会变得麻烦
  • 希望在组织层面共享构建缓存

这时会出现的工具就是 Tuist。这将是系列最后一篇的主题。

面试时会这样问

Q. iOS 模块化中为什么要使用 SPM 本地包?

模块定义由声明式的 Package.swift 代码而不是 pbxproj 管理,因此可以减少合并冲突并降低添加模块的成本。此外,target 之间的依赖关系会明确写在 dependencies 中,未获允许的 import 会被编译错误拦截,从而由编译器强制执行模块边界。

Q. 请说明 static 库和 dynamic framework 的区别,以及选择标准。

static 会在链接时复制到应用二进制文件中,因此运行时没有加载成本;dynamic 作为独立文件存在,并在运行时加载。默认使用 static;当应用、扩展和组件共享代码、需要减少二进制重复时,选择 dynamic。

看着包文件夹一个个增加,多少有点乐趣
看着包文件夹一个个增加,多少有点乐趣

下一篇将介绍许多团队开始模块化的真正契机:Xcode 构建速度。

我们将整理构建变慢的原因、模块化能改善哪些地方以及改善多少,还会介绍如何用数字衡量改善效果。