一、Swift编译器的两个阶段

Swift编译器的工作分为两个核心阶段:Swift前端(解析、类型检查、SIL生成)和 LLVM后端(IR优化、目标代码生成)。理解两阶段的优化机会,是控制二进制体积的第一步。

Swift编译流程

Source Code → Swift Frontend → SIL (Swift Intermediate Language) → LLVM IR → Machine Code

  • 前端(Swift Compiler):语法解析 → 语义分析 → SIL生成 → SIL优化 → LLVM IR发射
  • 后端(LLVM):LLVM IR → Pass Manager(优化Pass) → 汇编生成 → 链接
  • 链接器(ld):静态链接多个 .o 文件,符号解析,重定位

1.1 编译优化级别

// Xcode Build Settings 中的优化级别
// 调试模式(Debug):
//   SWIFT_OPTIMIZATION_LEVEL = -Onone
//   LLVM_OPTIMIZATION = -O0
//   → 禁用优化,保留完整调试信息(DWARF)

// 发布模式(Release):
//   SWIFT_OPTIMIZATION_LEVEL = -O          // Swift全优化
//   LLVM_OPTIMIZATION = -O2                // LLVM O2优化
//   SWIFT_COMPILATION_MODE = wholemodule   // 跨文件优化
//   → 优化后代码体积通常比Debug小30-50%

// 关键:wholemodule 模式
// 每个文件作为同一模块编译,LLVM可跨文件内联,
// 减少冗余代码,但编译时间显著增加

二、二进制体积来源分析

2.1 Link Map 分析

Link Map 记录了最终二进制文件中每个符号的地址和大小,是体积分析的第一步。在 Build Settings 中启用:

// 启用 Link Map(Xcode Build Settings)
// WRITE_LINK_MAP_FILE = YES
// LINK_MAP_FILE = $(TARGET_BUILD_DIR)/$(PRODUCT_NAME).linkmap

// 解析 Link Map 的脚本工具
#!/usr/bin/env python3
import re

with open("MyApp.linkmap") as f:
    content = f.read()

# 提取目标文件大小
file_sizes = re.findall(r'([\w./]+)\s+([0-9a-f]+)\s+([0-9a-f]+)', content)
for path, addr, size in file_sizes:
    size_bytes = int(size, 16)
    print(f"{size_bytes/1024:.1f} KB\t{path}")

Link Map 分析可以发现:哪个静态库/Framework 占用了最多空间,常见的体积大户是大型第三方SDK(如防作弊SDK、地图SDK、支付SDK)。

2.2 二进制体积的主要来源

来源占比优化手段
应用代码(.text)40-60%编译优化、代码裁剪、LTO
动态库(.dylib/.so)20-40%静态链接、库裁剪、 Embedded Binaries
资源文件(.rsrc)10-30%图片压缩、资源优化、按需加载
符号表(.symbol)5-15%Strip Symbols、dSYM分离

三、代码级优化策略

3.1 方法内联(Inlining)

内联可以消除函数调用开销(压栈、跳转),同时为其他优化(常量传播、死代码消除)创造条件。Swift编译器在 -O 模式下会自动内联,但我们可以显式控制:

// Swift 强制内联(用于性能关键路径)
@inline(__always) func criticalPath() {
    // 编译器必须内联,即使增加了代码体积
}

// 永远不内联(用于代码体积控制)
@inline(never) func rarelyCalled() {
    // 编译器选择不内联
}

// 编译期条件:DEBUG下内联,Release下按需
@inline(__forward) func forwardable() {
    // hint:可在单模块内联
}

// 实际效果对比(汇编)
// @inline(__always) 强制内联后:函数体直接展开,无CALL指令
// 无内联:CALL swift_retain 等调用依然存在

3.2 泛型特化(Generic Specialization)

Swift的泛型是编译期多态,每个具体类型组合都会生成一份特化代码。这是Swift二进制体积偏大的主要原因之一:

// 泛型函数:编译器为每种类型组合生成一份实例化代码
func findMax(_ array: [T]) -> T? { ... }

// 使用3种不同类型,编译后生成3份实例化代码:
let a = findMax([1, 2, 3])           // Int版本
let b = findMax(["a", "b"])          // String版本
let c = findMax([1.0, 2.0])          // Double版本

// Link Map验证:
// __TEXT.__text.__swift5_types     512 KB (3个实例化副本)
// 优化:使用 Existential Container(协议容器)避免特化:
// func findMax(_ array: [some Comparable]) -> Any? { ... }
// → 一份代码,但性能稍低(间接调用开销)

3.3 死代码消除(Dead Code Elimination)

整个模块一起编译(wholemodule)时,LLVM能识别未被调用的函数并将其移除。但dynamic dispatch(@objc、继承、方法重写)会阻止DCE:

// 被阻止DCE的情况:
class Animal {
    @objc dynamic func speak() {} // dynamic: 运行时查找,LLVM无法判断是否被调用
}

// 优化:使用 @inline(__always) + 非dynamic 的 final 方法
final class Cat {
    @inline(__always) func speak() {} // final保证不重写,LLVM可安全内联+消除
}

四、链接优化

4.1 链接时优化(LTO / Whole Program Optimization)

LTO 允许链接器在最终链接阶段重新进行全程序优化,打破编译单元边界:

// Xcode Build Settings:
// SWIFT_LTO_MODE = full                    // Swift LTO(完整跨模块优化)
// LLVM_LTO_MODE = full                     // LLVM LTO
// 或使用 thin LTO(编译时间更短,优化效果稍弱)
// SWIFT_LTO_MODE = thin

// LTO的核心效果:
// 1. 跨模块内联:Module A的函数被Module B调用时,可在链接时内联
// 2. 死代码消除更彻底:判断标准从"模块内未引用"扩展到"全程序未引用"
// 3. 虚函数表合并:同一类型的虚表只保留一份

// 实际收益:
// - 二进制体积减少:通常5-15%
// - 运行时性能提升:通常5-10%(减少间接调用)
// - 编译时间增加:通常30-50%(链接阶段更长)

4.2 Strip Symbols 减少符号表

// Xcode Build Settings:
// STRIP_INSTALLED_PRODUCT = YES
// STRIP_STYLE = Non-Global Symbols (调试用)
// 或 STRIP_STYLE = All Symbols (最终发布)

// Strip 后效果对比(Link Map分析):
// Strip前:
// __TEXT.__symbol_stub     2048 KB
// __DATA.__nl_symbol_ptr   512 KB
// __TEXT.__unwind_info     1024 KB

// Strip后(只保留必要符号):
// __TEXT.__symbol_stub     512 KB  (减少75%)
// 所有Strip的符号保存在 .dSYM 文件中(崩溃定位用,不影响体积)

五、资源优化

5.1 图片资源优化

  • HEIC格式:相同画质下比JPEG小40-50%,Xcode 15+原生支持
  • PDF矢量图:单个PDF替代多个分辨率的PNG/@1x/@2x/@3x
  • Asset Catalog:启用"App Thinning"后,按设备分发对应分辨率资源
  • ImageOptim:无损压缩PNG/JPEG,去除元数据

5.2 Swift版本与二进制体积

Swift版本的演进伴随着ABI稳定和标准库的解耦:

// Swift 5.0+:ABI稳定,runtime从app bundle中移除(节省约4MB)
// Swift 5.3+:新协议 @_fixedLayout、@inlinable 减少泛型膨胀
// Swift 5.6+:Type Parameter Packs 减少重载实例化

// Swift版本选择建议:
// - Xcode默认的Swift版本 = 最稳定选择
// - 升级Swift版本前:运行 App Thinning 分析体积变化
// - 不要使用过旧的Swift版本:运行时体积反而更大(runtime bug fix)

5.3 App Thinning 增量分发

App Thinning 确保用户设备只下载自己需要的那部分资源:

App Thinning 三个维度

  • Slicing:Xcode为每种目标设备生成独立包(不含其他设备的资源)
  • On-Demand Resources(ODR):资源按标签(Initial Install Tags)分发,非关键资源在首次使用时按需下载
  • Bitcode:已废弃,但原理是让Apple在服务端重新编译优化