Highlight
用 Xcode 14 重新编译 App,并在 iOS 16 上运行,你的 App 体积最多可缩小 4%,启动速度也会提升——所有改进都不需要修改一行代码。
核心内容
协议检查拖慢启动
Swift 开发中经常用到协议(Protocol)。一个函数接收 CustomLoggable 参数时,运行时要用 as 或 is 检查传入对象是否符合协议。
编译器能在编译期优化的检查已经做了。但遇到泛型等场景,大量检查只能在启动时完成。Apple 在真实 App 中发现,协议检查有时占掉启动时间的一半。
Xcode 14 和 iOS 16 的四项透明优化
Apple 从编译器和运行时两个层面做了改进:
- Swift 协议检查预计算:把协议检查元数据提前算好,放进 dyld 启动闭包里。现有 App 在 iOS 16 上直接受益。
- Objective-C message send 体积缩小:每个
objc_msgSend调用从 12 字节减到 8 字节(ARM64),整体二进制体积最多减少 2%。 - retain/release 调用更小:ARC 自动插入的
objc_retain/objc_release从 8 字节减到 4 字节(ARM64),再省 2% 体积。 - autorelease elision 更快更小:运行时改用指针比较替代指令比较,速度提升的同时还省掉了标记指令的代码体积。
详细内容
协议检查:从启动时计算到预计算
(01:23)
Swift 的协议检查需要元数据支持。一部分元数据编译期就能确定,但泛型相关的只能在启动时构建。
protocol CustomLoggable {
var customLogString: String { get }
}
struct Event: CustomLoggable {
let name: String
let date: Date
var customLogString: String {
"\(name) at \(date)"
}
}
func log(_ value: Any) {
if let loggable = value as? CustomLoggable {
print(loggable.customLogString)
}
}
let event = Event(name: "WWDC", date: Date())
log(event) // 运行时执行 as? CustomLoggable 检查
关键点:
as? CustomLoggable在编译期无法完全确定时,会交给运行时处理- 运行时依赖协议检查元数据来判断对象是否符合协议
- 泛型场景下,大量元数据只能在启动时构建,耗时可达数百毫秒
- iOS 16 的 Swift 运行时把这些计算提前到 dyld 闭包中,启动时直接读取结果
Message Send:selector stub 共享代码
(03:10)
Objective-C 的每个方法调用最终都变成 objc_msgSend。在 ARM64 上,一个完整的调用需要 12 字节:4 字节调用指令 + 8 字节准备 selector。
// 这段代码会产生大量 objc_msgSend 调用
NSCalendar *cal = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
NSDateComponents *components = [[NSDateComponents alloc] init];
components.year = 2022;
components.month = 6;
components.day = 6;
NSDate *theDate = [cal dateFromComponents:components];
关键点:
- 每个
objc_msgSend调用中,8 字节用于准备 selector - 同一个 selector 的准备代码完全相同,可以提取成共享的 stub 函数
- Xcode 14 的编译器和链接器自动做这个优化
- 默认模式平衡体积和性能,也可用
-objc_stubs_small链接器标志只优化体积
Retain/Release:自定义调用约定
(06:57)
ARC 自动插入的 objc_retain 和 objc_release 是普通 C 函数,必须遵守 C 调用约定。这意味着编译器要生成额外指令把对象指针放到正确的寄存器。
Apple 为 retain/release 设计了自定义调用约定。编译器根据对象指针当前所在位置,选择最合适的调用变体,省去不必要的 move 指令。
关键点:
objc_retain/objc_release在 ARM64 上从 8 字节减到 4 字节- 需要运行时支持,所以要把 deployment target 设到 iOS 16 / tvOS 16 / watchOS 9
- 与 message send 优化叠加,整体体积收益更明显
Autorelease Elision:指针比较替代指令比较
(09:26)
函数返回临时对象时,ARC 会用 autorelease 延迟释放。如果调用方立即 retain,这对 autorelease/retain 可以互相抵消,这就是 autorelease elision。
旧实现中,运行时把代码指令当数据加载,与一个特殊标记值比较来判断是否可以 elide。这个操作在 CPU 上效率不高。
新实现改用指针比较:
autorelease时保存返回地址指针retain时获取当前返回地址指针- 两个指针相同,说明是匹配的 autorelease/retain 对,可以 elide
关键点:
- 指针比较比加载指令当数据快得多
- 不需要特殊标记指令,代码体积进一步缩小
- iOS 16 运行时自动生效;代码体积收益需要 deployment target 升级到 iOS 16
核心启发
-
立即重新编译现有 App 用 Xcode 14 重新编译,不需要改代码,App 体积就能减少约 2%。这是零成本收益,应该尽快执行。
-
把 deployment target 升级到 iOS 16 升级后 retain/release 优化和更小的 autorelease elision 序列再省约 2% 体积。评估用户设备分布后,可以考虑推进这个升级。
-
检查启动耗时中的协议检查占比 如果 App 启动慢,用 Instruments 的 Time Profiler 看看启动阶段有没有大量协议检查。iOS 16 上这个问题会自动缓解,但也可以审视代码中过度使用协议检查的模式。
-
对体积敏感的应用使用
-objc_stubs_small如果 App 体积是核心指标(比如 watchOS App),可以在链接器设置里加上-objc_stubs_small,把 message send stub 的体积优化推到最大。
关联 Session
- App Startup Time: Past, Present, and Future — 深入了解 dyld 闭包和启动优化机制
- What’s new in Swift — Swift 语言层面的新特性
- What’s new in Xcode 14 — Xcode 14 编译器和链接器的改进是本次优化的基础
- Diagnose Power and Performance regressions in your app — 学会用 Instruments 定位性能问题
- Swift concurrency: Update a sample app — 并发优化与运行时性能提升结合
评论
GitHub Issues · utterances