Objective-C 运行时的新进展
Advancements in the Objective-C runtime
2020年6月25日
一句话判断
Objective-C 没死——它的运行时在持续进化以支撑 Swift 的底层机制,理解这些变化对写出高性能的 Swift/ObjC 混编代码至关重要。
这场 Session 讲了什么
这场 Session 深入探讨了 Objective-C 运行时(runtime)在 2020 年的技术演进。虽然 Objective-C 作为语言已经进入了维护模式,但它的运行时仍然是 iOS/macOS 应用执行的基础设施——Swift 代码与 Objective-C 框架的互操作全部依赖它。Session 聚焦于三个核心改进:tagged pointer 的扩展、方法查找的性能优化,以及新的 isa 优化策略。
Tagged pointer 机制得到了扩展。从 iOS 14 开始,更多的值类型被优化为 tagged pointer 形式存储——直接在指针中编码小整数、日期、URL 等常见值,跳过堆内存分配。这减少了内存压力和缓存未命中,对应用的内存占用和启动速度都有实际帮助。
方法查找(method lookup)也进行了优化。Session 介绍了新的 vtable 分发策略和改进后的方法缓存机制。对于频繁调用的热路径方法,运行时可以跳过完整的 objc_msgSend 查找流程,直接通过缓存命中。这些优化对 Swift 调用 Objective-C 代码的性能提升尤为明显。
值得深挖的点
Tagged Pointer 的新数据类型
经典场景中,NSNumber、NSDate、NSString(短字符串)已经使用了 tagged pointer。iOS 14 扩展了 tagged pointer 的覆盖范围,增加了对更多类型的支持。关键在于 tagged pointer 的判断极其快速——只需要检查指针的最低位是否为 1(arm64 上是最高位),不需要任何额外的内存访问。Session 提到这个机制每年都在扩大覆盖面,未来可能会有更多 Foundation 值类型受益。
Swift 与 ObjC 互操作的性能桥梁
当 Swift 代码调用 @objc 方法时,底层走的是 objc_msgSend。新的运行时优化减少了这个路径的开销:更快的缓存查找、更好的分支预测、以及对常见调用模式的特化处理。Session 建议:如果你在性能关键路径上频繁跨越 Swift/ObjC 边界,考虑将那部分 ObjC 代码用 Swift 重写,因为纯 Swift 调用路径的开销已经低于 ObjC message dispatch。
代码片段
// 理解 tagged pointer 对性能的影响
import Foundation
// NSNumber 使用 tagged pointer 优化
let smallInt = NSNumber(value: 42) // tagged pointer,无堆分配
let largeInt = NSNumber(value: Int64.max) // 需要堆分配
// 检查是否为 tagged pointer(仅用于调试理解)
// tagged pointer 在 arm64 上最高位为 1
// 这是运行时的内部机制,不应在生产代码中依赖
print("小整数 NSNumber 占用更少内存,因为使用了 tagged pointer")
// 短字符串也使用 tagged pointer
let shortString = "Hi" // tagged pointer(长度 <= 7 的 ASCII 字符串)
let longString = "这是一段很长的字符串" // 需要堆分配
// 日期值的 tagged pointer 优化
let recentDate = Date() // 大多数日期值可以用 tagged pointer 表示
// Swift 与 ObjC 互操作的性能考量
import Foundation
// ObjC 风格的方法调用(通过 message dispatch)
class ObjCCompatibleClass: NSObject {
@objc dynamic func processItem(_ item: String) -> String {
return "处理: \(item)"
}
}
// 纯 Swift 的方法调用(直接 dispatch)
struct SwiftStruct {
func processItem(_ item: String) -> String {
return "处理: \(item)"
}
}
// 性能对比(概念演示)
func benchmark() {
let obj = ObjCCompatibleClass()
let swift = SwiftStruct()
// ObjC dynamic dispatch 每次调用需要经过 objc_msgSend
// 但 iOS 14 的运行时优化让缓存命中率大幅提升
for _ in 0..<100000 {
_ = obj.processItem("测试")
}
// 纯 Swift 的 direct dispatch 没有额外开销
for _ in 0..<100000 {
_ = swift.processItem("测试")
}
}
// 利用运行时信息做调试和性能分析
import ObjectiveC
// 检查类的方法列表(调试用)
func inspectMethods(of cls: AnyClass) {
var methodCount: UInt32 = 0
guard let methods = class_copyMethodList(cls, &methodCount) else { return }
print("类 \(cls) 有 \(methodCount) 个方法:")
for i in 0..<Int(methodCount) {
let selector = method_getName(methods[i])
print(" - \(NSStringFromSelector(selector))")
}
free(methods)
}
// 检查实例大小(理解内存布局)
func printInstanceSize(of cls: AnyClass) {
let size = class_getInstanceSize(cls)
print("实例大小: \(size) bytes")
// 理解 isa 指针的开销
// isa 在 arm64 上占用 8 bytes(优化后可能共用 class pointer)
print("其中 isa 指针占用 8 bytes")
}
最佳实践
- 不要在性能敏感的热路径上频繁跨越 Swift/ObjC 边界,尽量在单一语言体系内完成高频调用
- 使用
final和private修饰符让 Swift 编译器使用 direct dispatch,避免不必要的虚表查找 - 理解
@objc dynamic的性能代价——它强制使用 ObjC message dispatch,只在需要方法替换(swizzling)或 KVO 时使用 - 定期用 Instruments 的 Time Profiler 检查
objc_msgSend的开销占比,超过 5% 就值得优化 - tagged pointer 是透明的优化,不需要代码层面的适配,但理解它有助于做内存性能分析
还有什么值得关注
- Session 提到 arm64e(Apple Silicon)上的 pointer authentication 进一步强化了 runtime 的安全性
- 新的 class data 结构优化了类元数据的存储布局,减少了脏内存页的数量
- Objective-C 运行时的改进对 Swift Concurrency(async/await)的底层实现也有影响——future Swift feature 的很多基础设施都在 runtime 层面准备着