iOS / 架构演进 2026-09-20

iOS 模块化架构演进:从 CocoaPods 到 Swift Package Manager 的工程路径

iOS 工程的模块化不是"拆几个 Pod"那么简单。从 CocoaPods 的中心化依赖管理到 Swift Package Manager 的去中心化模块声明,背后是组件边界、构建系统、路由方案、版本管理四大架构决策的重构。本篇以百万日活 App 的 60+ 模块演进为蓝本,给出从单体到组件壳再到 SPM-native 的完整模块化路径与踩坑实录。

iOS 模块化 Swift Package Manager CocoaPods 组件化 路由 组件壳 Tuist

一、为什么 iOS 工程必须模块化

iOS 工程从单体走向模块化是被逼出来的——App 复杂度爆炸后,单体工程编译慢、协作难、复用性差三大问题让团队几乎无法继续工作。一个 50 万行代码的 iOS App,全量编译需要 8-12 分钟,意味着工程师每天有 1-2 小时在等编译。这种效率损耗不是"加几台 Mac Pro"能解决的,必须从架构上拆。

模块化的核心价值是编译时间指数级下降 + 团队并行化 + 复用性提升。一个 60 个模块的工程,单模块编译 30 秒,增量编译 5 秒内,工程师每天节省 1.5 小时。同时不同团队可以并行开发不同模块,团队规模从 10 人扩展到 50 人也不会撞车。

graph TB
    A[单体 App] --> B[编译 12min]
    A --> C[代码冲突频繁]
    A --> D[复用靠复制]
    E[60 模块] --> F[增量编译 5s]
    E --> G[团队并行开发]
    E --> H[模块复用]
架构哲学:模块化不是"为拆而拆",而是业务复杂度倒逼的架构演进。单体工程在 10 万行代码以下是甜蜜期,超过 30 万行必须模块化。判断标准简单——全量编译超过 5 分钟就值得拆。

二、模块化的三个核心问题

iOS 模块化必须解决三个独立但相互依赖的问题:模块怎么拆、模块怎么连、模块怎么管。拆错架构会引出更大麻烦,连接错会引发循环依赖,管理错会让版本地狱重现。

graph TB
    A[模块化核心问题] --> B[拆·边界设计]
    A --> C[连·依赖管理]
    A --> D[管·版本策略]
    B --> E[业务边界]
    B --> F[技术边界]
    C --> G[CocoaPods]
    C --> H[SPM]
    C --> I[Carthage]
    D --> J[语义化版本]
    D --> K[依赖锁定]

2.1 拆:模块边界设计

模块边界设计是模块化最难的部分。原则:按业务能力拆分,不按技术层次拆分。
反例:把网络层、Model 层、View 层拆成模块(Common/Network/Model),结果是任何业务变更都要改 5 个模块,协作反而更糟。
正例:按业务领域拆分(Order/User/Payment),每个模块内部自己组织 MVC,技术层次隐藏在模块内部。

2.2 连:依赖管理

模块之间的依赖关系必须单向、无循环、可视化。CocoaPods 通过 Podfile 中心化声明依赖,SPM 通过 Package.swift 去中心化声明。CocoaPods 简单但耦合重,SPM 灵活但管理复杂,选择哪个看团队规模和工程成熟度。

2.3 管:版本策略

模块的版本管理有两种模式:Git Tag 模式(每个模块独立仓库)和 Monorepo 模式(所有模块一个仓库)。Git Tag 模式适合开源项目和跨团队复用,Monorepo 模式适合内部业务模块。我们的实践:核心业务模块用 Monorepo,通用工具模块用 Git Tag。

维度CocoaPodsSPMCarthage
依赖声明中心化 Podfile去中心化 Package.swift中心化 Cartfile
构建方式动态/静态 frameworkSwift 静态库binary framework
依赖锁Podfile.lockPackage.resolvedCartfile.resolved
Apple 官方支持否(社区)是(官方)否(社区)
适用场景历史项目稳定新项目首选二进制分发

三、CocoaPods 时代:中心化依赖的黄金年代

CocoaPods 是 iOS 模块化的奠基者。2011 年发布后,几乎所有 iOS 项目都用它管理依赖。它解决了"如何让团队共享第三方库"的根本问题——在 CocoaPods 之前,iOS 工程共享库靠手动拖文件,是真正的"复制粘贴编程"。

graph LR
    A[Podfile] --> B[pod install]
    B --> C[生成 .xcworkspace]
    C --> D[集成所有 Pods]
    D --> E[App 工程引用]

3.1 CocoaPods 的工作原理

Podfile 声明依赖:pod 'Alamofire', '~> 5.0'。pod install 会下载源码、解析依赖、生成 .xcworkspace、配置工程引用。整个机制是中心化的——所有依赖在 Podfile 声明,构建时统一处理。

3.2 CocoaPods 的局限性

三个根本性问题:
(1)构建慢——Pod 数量超过 50 后,Pod install 时间指数级增长;
(2)耦合重——所有 Pod 在同一个 workspace,无法真正隔离;
(3)官方不支持——Apple 始终没有把 CocoaPods 集成进 Xcode,构建过程是"外挂"。

3.3 CocoaPods 在企业内部模块化

很多企业用 CocoaPods 管理内部私有模块——每个业务模块打成 podspec,主工程通过 CocoaPods 引入。优势是简单:业务方熟悉、配置直观;劣势是构建慢:60 个 Pod 的 pod install 需要 3-5 分钟,每次版本切换都重新生成 workspace。

四、组件壳工程:路由 + 容器 + 资源

组件壳(Component Shell)是中大型 App 模块化的核心架构模式——每个业务模块是一个独立的壳工程,包含 UI、路由、资源、对外接口。主工程只负责壳工程的组装和壳之间的通信。

graph TB
    subgraph 主工程
        A[App Shell] --> B[路由中心]
        A --> C[容器服务]
    end

    subgraph 业务模块
        D[User 壳] --> E[路由注册]
        D --> F[对外接口]
        D --> G[资源]
        H[Order 壳] --> E
        H --> F
        H --> G
        I[Payment 壳] --> E
        I --> F
        I --> G
    end

    B -.路由协议.-> D
    B -.路由协议.-> H
    B -.路由协议.-> I

4.1 路由设计

路由是模块间通信的核心——URL-based 路由是最常见的实现:业务方通过 URL 跳转到目标模块,路由中心解析 URL 并分发给对应模块。
优势:模块间解耦(不直接 import),支持远程调试和延迟加载;
劣势:字符串硬编码、类型不安全,需要配合协议化路由(Protocol-based)才能工业级。

4.2 容器服务

模块间除了 UI 跳转,还需要获取服务(如 UserService、PaymentService)。容器服务(Container)解决依赖注入问题:每个模块提供注册接口,主工程启动时注册所有服务,业务方通过协议获取服务实例。
我们的实践:用 Swinject 或手工容器,模块间只依赖协议不依赖实现。

4.3 资源隔离

每个壳工程有自己的 Asset Catalog、字符串文件、图片资源。主工程的 Asset Catalog 不直接引用模块资源——模块资源打包进 framework bundle,运行时按 bundle 路径加载。这避免资源冲突。

架构哲学:组件壳是 iOS 模块化的"工业标准"。它解决了模块化最难的三个问题:UI 跳转、服务获取、资源隔离。没有组件壳的模块化只是"形式上的拆分",不是真正的架构演进。

五、Swift Package Manager:去中心化的范式转移

Swift Package Manager(SPM)是 Apple 官方推出的依赖管理和模块化工具,Swift 3.0 起逐步成熟。它是 iOS 模块化的未来——Xcode 14+ 已经深度集成,App Clip、Swift Playground 都原生支持 SPM。

graph LR
    A[Package.swift] --> B[声明模块]
    B --> C[products: library/executable]
    B --> D[targets: source code]
    B --> E[dependencies: other packages]
    A --> F[swift build]
    F --> G[生成 .build 目录]
    G --> H[Xcode 自动集成]

5.1 Package.swift 的核心结构

SPM 用 Package.swift 声明模块(包),每个包包含多个 target(子模块),target 之间通过 dependencies 声明依赖关系。这是真正的"代码即配置"——Package.swift 本身是 Swift 代码,可以用 Swift 的逻辑控制依赖版本。

5.2 SPM vs CocoaPods 的本质差异

CocoaPods 是中心化的:所有依赖在 Podfile 集中声明,Pod install 后统一生成 workspace;
SPM 是去中心化的:每个 Package 自带 Package.swift,Xcode 自动发现并集成。
这意味着 SPM 支持本地 Package 直接开发,改一个 Package 的代码立即在主工程生效,构建时间从 CocoaPods 的 3-5 分钟压到 30 秒以内。

5.3 SPM 的限制

SPM 不是银弹——三个局限:
(1)不支持 resource bundle 的复杂管理,需要手工打包;
(2)不支持 pre-build script 的复杂配置,某些工程化需求(Code Signing)需要 workaround;
(3)iOS 资源加载机制限制,SPM 模块的资源 bundle 需要手动加载,配置繁琐。

六、Tuist:声明式项目生成

Tuist 是 iOS 模块化的"瑞士军刀"——它通过 Swift DSL 声明工程结构,自动生成 .xcodeproj,从根本上解决 .xcodeproj 冲突问题。

graph LR
    A[Project.swift] --> B[Tuist generate]
    B --> C[生成 .xcodeproj]
    B --> D[生成 .xcworkspace]
    C --> E[多 target 工程]
    D --> F[自动模块化]

6.1 Tuist 解决的核心问题

.xcodeproj 是iOS 工程最大的协作痛点——XML 文件冲突频繁,合并地狱让 PR 经常卡在工程文件上。Tuist 用 Swift 代码声明工程结构,.xcodeproj 由工具生成,不进入 Git 冲突。这是企业级 iOS 模块化的关键基础设施。

6.2 Tuist + SPM 的最佳实践

我们的架构:Tuist 负责工程结构、SPM 负责模块依赖、CocoaPods 仅保留历史第三方库。这种混合架构既享受 Tuist 的工程化能力,又利用 SPM 的模块化能力,同时兼容遗留依赖。

6.3 Tuist 的学习曲线

Tuist 的 Swift DSL 需要团队学习 Swift Package Manager 的概念(target、product、dependency),新手 2-4 周才能上手。但一旦掌握,工程的复杂度变成可管理的 Swift 代码,长期收益远超学习成本。

七、模块边界设计:依赖倒置与接口隔离

模块化最难的不是"用什么工具",而是"模块边界怎么画"。边界画错会导致循环依赖、过度耦合、复用性差——所有模块化努力前功尽弃。

graph TB
    A[User 模块] -->|接口| P[UserProtocol]
    B[Order 模块] -->|依赖| P
    P -.实现.-> A

7.1 依赖倒置原则

高层模块不依赖低层模块,二者都依赖抽象。Order 模块需要"获取当前用户信息",不能直接 import User 模块,应该依赖 UserProtocol 协议,User 模块实现协议。
这避免了循环依赖——User 模块可以依赖 Order 模块的回调,但不需要 import Order 模块。

7.2 接口隔离

模块对外暴露的接口要最小化。User 模块可能有 100 个方法,但对外暴露的"获取当前用户"只需要 1 个。接口臃肿会让模块耦合,其他模块会不自觉依赖"看起来有用"的方法。

7.3 业务边界 vs 技术边界

模块划分应该按业务能力,不按技术层次。
反例:Common / Network / Model / View 四个模块,任何业务变更都要跨模块修改;
正例:Order / User / Payment / Marketing 四个业务模块,每个模块内部自组织 MVC,技术层次隐藏在模块内部。

划分方式模块数耦合度团队规模
技术层次(反例)5-10极高≤ 10 人
业务领域(正例)20-60低30-100 人
DDD 限界上下文30-100极低100+ 人

八、生产踩坑:模块化三大陷阱

模块化是看上去简单、做起来极难的工程。下面三个坑我们每个都花了 2-4 周才定位修复。

8.1 坑一:循环依赖引发主工程编译失败

Order 模块需要 User 模块的 UserService,User 模块的注册流程需要 Order 模块的回调——形成循环依赖。CocoaPods 检测到循环会报错,但 SPM 检测到的是隐式循环(Protocol 化解后仍然存在)。
修复:提取公共协议到独立的 Interface 模块,让 User 和 Order 都依赖 Interface 模块,避免直接耦合。

8.2 坑二:模块化后体积反而变大

我们最初以为模块化会让 App 体积变小,实测模块化后 App 体积增加了 8MB。原因是每个 framework 都有自己的 dyld 加载开销和 metadata。
修复:用 Static Framework 替代 Dynamic Framework,链接时合并到主二进制,体积压回原来大小。代价是启动时间略增(无 dyld 共享缓存),但对一般 App 可接受。

8.3 坑三:模块间跳转的性能损耗

组件壳用 URL 路由跳转,每次跳转涉及 URL 解析、路由查找、模块加载,P99 跳转延迟从 50ms 升到 200ms。
修复:核心跳转路径用 protocol 化路由(直接函数调用),仅外部 URL(远程推送、深链)用 URL 路由。混合路由策略让性能与解耦兼得。

工程哲学:模块化是"收益与代价"的工程权衡。不是所有 App 都应该模块化——10 万行以下的单体工程,模块化反而是负担。判断标准简单:编译时间超过 5 分钟、团队超过 20 人、业务超过 5 个独立域,这三个条件满足两个就该模块化。

九、演进方向:Binary 框架与 Swift 6 宏

iOS 模块化的下一个演进方向有三个:Binary Framework 加速、Swift 6 宏系统、Swift Macros 驱动的新模块化范式。

graph LR
    A[iOS 模块化演进] --> B[Binary Framework]
    A --> C[Swift 6 宏]
    A --> D[Macro-driven 模块化]
    B --> E[XCFramework]
    B --> F[预编译 framework]
    C --> G[编译期元编程]
    D --> H[路由代码自动生成]
    D --> I[接口自动生成]

Binary Framework 是企业级模块化的下一站:把核心模块预编译成 XCFramework,主工程只链接二进制,编译时间从分钟级压到秒级。代价是源码调试需要额外配置。

Swift 6 宏系统带来全新模块化可能:用宏在编译期生成路由代码、生成接口 Stub、生成协议适配层,大量手写模板代码消失。我们的实验项目用宏自动生成模块间跳转代码,减少了 40% 模板代码。

另一个方向是Swift Distributed Actors:跨进程、跨设备的模块化通信框架,未来模块间通信可能从 URL 路由走向 Actor 调用,这是 iOS 模块化的下一个十年。

最后一个方向是Swift Macros 驱动的依赖注入。目前依赖注入靠手工注册或运行时反射,宏可以在编译期生成依赖注册代码,让模块化架构的样板代码减半。这是 iOS 模块化的未来杀手锏。

未来已来:iOS 模块化正在从"手工拆分"走向"宏驱动的自动化"。Swift 6 + Binary Framework + Macros 会重新定义 iOS 模块化的范式。提前布局的团队,会在 Swift 生态的新一轮演进中占据架构制高点。