WWDC Quick Look 💓 By SwiftGGTeam
Improve app size and runtime performance

Improve app size and runtime performance

观看原视频

Highlight

用 Xcode 14 重新编译 App,并在 iOS 16 上运行,你的 App 体积最多可缩小 4%,启动速度也会提升——所有改进都不需要修改一行代码。

核心内容

协议检查拖慢启动

Swift 开发中经常用到协议(Protocol)。一个函数接收 CustomLoggable 参数时,运行时要用 asis 检查传入对象是否符合协议。

编译器能在编译期优化的检查已经做了。但遇到泛型等场景,大量检查只能在启动时完成。Apple 在真实 App 中发现,协议检查有时占掉启动时间的一半。

Xcode 14 和 iOS 16 的四项透明优化

Apple 从编译器和运行时两个层面做了改进:

  1. Swift 协议检查预计算:把协议检查元数据提前算好,放进 dyld 启动闭包里。现有 App 在 iOS 16 上直接受益。
  2. Objective-C message send 体积缩小:每个 objc_msgSend 调用从 12 字节减到 8 字节(ARM64),整体二进制体积最多减少 2%。
  3. retain/release 调用更小:ARC 自动插入的 objc_retain / objc_release 从 8 字节减到 4 字节(ARM64),再省 2% 体积。
  4. 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_retainobjc_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 上效率不高。

新实现改用指针比较:

  1. autorelease 时保存返回地址指针
  2. retain 时获取当前返回地址指针
  3. 两个指针相同,说明是匹配的 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

评论

GitHub Issues · utterances