用预编译 XCFramework 加速 iOS 构建

Expo SDK 56 在 iOS 上默认启用预编译 XCFrameworks,构建开销大幅降低,并开启从 CocoaPods 转向 SwiftPM 的转型。

中文
复制
Faster iOS Builds with Precompiled XCFrameworks

Expo SDK 56 开始支持在 iOS 上以 XCFrameworks 形式使用预编译的 Expo 模块。对许多项目来说,这能大幅降低原生构建的开销。

预编译 XCFrameworks 在本地和 EAS Build 上默认启用,无需任何配置。

在这背后,这也是 Expo 生态一次更大转型的开端:从 CocoaPods 转向 Swift Package Manager(SwiftPM),也就是 Apple 用于依赖管理和原生构建的现代方案。

本文将展开讲讲:

  • 为什么要做这次转型

  • 预编译 Expo 模块是如何工作的

  • 其中涉及的架构挑战

  • 以及这为 Expo 在 iOS 上的未来打开了什么

不改动应用,也能更快构建

过去,iOS 上每一个 Expo 和 React Native 库都要在你的应用工程里从源码编译。这意味着每一次本地构建、每一次 EAS Build,都要从头重新编译下面这些内容:

  • React Native

  • Expo 模块

  • 带原生代码的第三方库

https://expo.dev/changelog/sdk-56 中的预编译 XCFrameworks 改变了这一点。

现在许多 Expo 模块通过 npm 以预编译 XCFrameworks 的形式分发,可以直接链接,而不必在每次应用编译时重新构建。实际效果就是原生编译步骤更少、本地迭代更快、EAS Build 更快,原生构建环境也更可复现。

最省心的是,这一切自动发生,不需要任何迁移步骤。

💡 什么是 XCFrameworks? XCFramework 是 Apple 用来分发预编译原生库的格式。与其分发每个应用都要在本地编译的源码,框架可以直接以二进制产物的形式分发,并且已经针对 iOS 设备、模拟器以及多种架构编译完成。每个 XCFramework 都包含针对特定平台和架构的独立框架“切片”。React Native 内部已经在使用 XCFrameworks,而 Expo SDK 56 把这一模式扩展到了整个 Expo 生态。

为什么预编译 XCFramework 很重要

这项工作解决了 React Native 生态中两个长期存在的重大问题。

1. CocoaPods 正在变成遗留基础设施

Expo、React Native 以及大多数 React Native 库目前都严重依赖 CocoaPods。

CocoaPods 历来负责依赖解析、原生自动链接、项目集成和构建编排。但它是一套基于 Ruby 的老系统,终将被弃用,并会在 2026 年 12 月进入只读状态。

Swift Package Manager 已成为 Apple 在原生依赖、构建、包分发和构建扩展性方面的标准工具。

React Native 自身已经在内部开始向 SwiftPM 支持迁移,包括将 React Native 的部分内容以预编译 XCFramework 的形式分发。

Expo SDK 56 正是沿着这个方向推进。

2. 原生构建成本高昂

随着应用规模变大,原生构建时间持续增长。

这在 CI 环境、EAS Build 以及拥有大量原生依赖的大型 monorepo 中尤为明显。预编译 XCFramework 让我们可以把其中大部分工作提前到流水线的更早阶段。Framework 只编译一次,打包后在各次构建中重复使用。

这大幅减少了重复的原生编译工作。

为什么这件事比听起来更难

React Native 和 Expo 最初是围绕 CocoaPods 和基于源码的编译构建的。

那套环境极其宽松:头文件全局可用,源文件几乎可以导入任何东西,pod target 之间的隔离很松散,模块边界也很模糊。

XCFramework 施加的规则要严格得多。

每个 framework 都必须:完全模块化、自包含,并且与其模块边界之外的实现细节隔离。

许多在 CocoaPods 下完全成立的假设,在构建可分发的 XCFramework 时会直接失效。

从零重写整个原生架构并不现实,所以我们转而专注于渐进式的兼容层和构建基础设施。

构建耗时影响

以下数据在 Apple M4 Max(64 GB 内存)上测得,测试对象是原版 Expo 应用的干净 iOS 构建,逐层启用预编译 XCFramework。

在 SDK 55 中,我们已经看到了使用预编译 React Native 二进制文件带来的提升。下表展示了预编译 Expo Modules 和第三方模块能为默认 Expo 项目带来的额外收益:

构建配置相比上一行相比全源码构建
全部从源码构建
+ React Native 核心~44%~44%
+ Expo 模块预编译~10%~50%
+ 第三方库预编译~30%~65%

项目越大,收益越取决于覆盖率:React Native 和 Expo 模块始终预编译,但第三方库只有最常用的那些才预编译。因此,如果项目依赖的是比较冷门的原生依赖,这些依赖仍要从源码编译,整体耗时的降幅可能更小。

让 Expo Modules Core 兼容 Swift Package Manager

第一个重要步骤是改造 expo-modules-core

几乎所有 Expo 模块都依赖它,所以它位于依赖图的根部。如果它无法构建为模块化的 XCFramework,其他任何东西都无从谈起

为此需要解决几个架构问题。

移除非法的头文件导出

过去,Expo Modules Core 的一些公开头文件会直接暴露 React Native 的头文件:

#import <React/RCTView.h>

在 CocoaPods 下这没问题,因为编译期间所有头文件全局可见。

Framework 的接口不允许这样做。

💡除非依赖本身已正确模块化,否则一个 framework 不能公开暴露另一个 framework 的头文件。

这些问题大多通过重构公开接口、隔离 React Native 内部实现、调整 API 结构来解决,使非模块化依赖不再泄漏到导出的接口中。

打破 Swift ↔ Objective-C 循环依赖

在混合语言 target 上,Swift Package Manager 比 CocoaPods 严格得多。

Expo Modules Core 长期存在循环依赖:Objective-C 引用 Swift 类型,Swift 又引用 Objective-C 类型。

SwiftPM 要求 target 之间有明确的依赖方向。为此,我们引入了新的接口抽象,拆分实现层,并重构了若干内部 API。

少数情况下,我们还借助 Objective-C 运行时反射动态调用 Swift 实现,从而避免引入不合法的编译期依赖。

为 SwiftPM 拆分源码树

Swift Package Manager 对源码归属同样严格。同一个源文件不能属于同一 package graph 中的多个 target。

Expo 的仓库结构从来不是按这个前提设计的。我们没有永久性地重组仓库,而是在构建期间通过符号链接、生成目录和构建期源码分离,生成临时的隔离源码结构。

这样既保留了现有的仓库布局,又满足了 SwiftPM 的隔离要求。

React Native 与 Virtual File System overlay

另一个主要难题是 React Native 的头文件结构。

React Native 目前的 XCFramework 支持仍然高度依赖 CocoaPods 生成的旧式头文件布局。这种布局在 Swift Package Manager 构建中并不天然存在。

为弥合这一差距,我们着手在 React Native 内部加入对 Clang Virtual File System(VFS)overlay 的支持。

VFS overlay 让编译器“看到”一套与物理文件系统结构不同的虚拟头文件布局。

由此我们可以保留现有的 include 路径,避免大规模重构源码,并在不物理重组 React Native 源码树的前提下,向编译器呈现模块化结构。

把庞大的遗留原生代码库现代化为可分发的模块化 framework 时,这是常用手法。

自动生成 Package.swift 文件

Swift Package Manager 用 Package.swift manifest 定义 target、依赖、平台和构建设置。要在 Expo 的各个 package 中手工维护这些文件,很快就会变得困难且容易出错。

为此,我们在 expo-tools 中引入了新的工具链,可自动生成 Package.swift manifest、隔离的源码结构、依赖图以及 XCFramework 打包步骤。

这套基础设施被设计为完全在 CI 中运行,最终用于支撑大规模的预编译包分发。

同时支持 CocoaPods 和 SwiftPM

这一过渡需要时间。

React Native 生态仍然严重依赖 CocoaPods,许多库尚未兼容 Swift Package Manager。因此,SDK 56 的重点是共存,而非替换。

Expo 模块目前可以在从源码构建和直接使用预编译 XCFramework 之间切换。

这样,现有应用可以继续正常工作,CocoaPods 仍然得到支持,同时生态在底层逐步现代化。

如有需要,也可以关闭预编译模块:

EXPO_USE_PRECOMPILED_MODULES=0

长期来看,我们预计 Expo 的 autolinking、原生集成和构建工具会更多地迁移到 Swift Package Manager 本身。

更大规模现代化工作的一部分

预编译 XCFramework 是 Expo SDK 56 整体现代化工作的一部分,同期还引入了内联原生模块、持续推进的 React Native 现代化工作,以及为未来原生工具链准备的基础设施改进。

这些改动共同让 Expo 更接近更快的原生构建、更清晰的模块化架构,以及与 Apple 现代开发生态更深入的整合。

展望

我们当前的重点是稳定兼容性、扩大包覆盖范围、验证构建性能提升,并继续与 React Native 上游协作。

这是我们在 Expo iOS 侧进行过的最大规模基础设施迁移之一。

但它为 React Native 在 Apple 平台上的开发开启了更具扩展性的未来:更快的构建、更好的工具、更清晰的原生边界,最终走向一个不再需要 CocoaPods 的世界。

SDK 56 是这一过渡的起点。

来源: Expo Blog← 返回首页