Swift vs Objective-C:从语法到运行时的全面对比

8 阅读7分钟

基于 Swift 5.10 / 部分 Swift 6 特性 与现代 Objective-C(ARC 已开启)。 文中代码片段取自某大型 iOS 客户端的 IM 模块,仅作为论点的最小佐证。


核心要点

维度Objective-CSwift
类型系统弱(nullable 仅警告,id 万能)强(Optional 强制、值类型、真泛型)
默认派发全部 objc_msgSend 动态派发静态 / vtable / 动态 三档,按声明决定
内存模型一切对象在堆上,全 ARC值类型不参与 ARC;引用类型走原生 ARC
错误处理NSError ** 出参,靠人自查throws / try 编译期强制
并发安全自己加锁自己加锁 + Swift 6 Sendable / actor 编译期校验
互操作与 C/C++ 几乎零成本与 OC 需 @objc,容器有桥接代价
运行时反射强(swizzling、KVO、动态方法)弱(仅 @objc dynamic 才支持)
编译速度慢(类型推断 / 泛型展开开销大)
二进制 / 冷启动体积小、无额外 runtime大、需 Swift Runtime(iOS 12.2 后系统内置)

一句话:Swift 用更强的类型系统和静态优化换取安全与性能,但要为此承担与 OC 互通的成本更慢的编译;OC 在动态性、二进制体积、与 C/C++ 互通上仍有不可替代的优势。


1. 类型系统:空安全、值类型、泛型

1.1 空安全:注解 vs 类型系统

OC 的可空性依靠 nullable / _Nonnull 注解,仅是给编译器的提示,运行时不强制;漏标或写错只产生警告。

Swift 的可空性是类型系统的一部分StringString? 是不同类型,使用 Optional 必须显式解包,编译期就拦下大量"野 nil"问题:

guard let scene else { return }
if let error {
    log(.error, message: "scene creation failed: \(error)")
    return
}

1.2 值类型 vs 引用类型

OC 几乎所有对象都是 NSObject 子类、堆上分配、引用语义。Swift 鼓励用 struct 描述纯数据载体:

public struct ConversationTagLabelMetaData {
    public let conversationID: String
    public let inboxMode: String
    public let tabType: String
}

值类型按值复制、栈上传递、不参与 ARC,天然线程安全且不会出现循环引用。OC 中等价物必须写成 @interface ... : NSObject,并手动管理 @propertyalloc/init、内存语义。

1.3 泛型

  • OC 的"泛型"(NSArray<NSString *> *)是 lightweight generics,仅供编译器类型检查,运行时仍是 NSArray
  • Swift 是真泛型,可参与重载并通过 specialization 在编译期单态化(monomorphization),调用零开销。

2. 内存与生命周期

两者都基于 ARC,但模型不同:

Objective-CSwift
作用对象OC 对象(NSObject 子类)Swift 原生 class、闭包;struct/enum 不参与
retain/release 注入位置编译器在 OC 对象使用点插桩编译器在原生对象元数据上插桩
weak 实现runtime 维护一张 weak_table原生 side-table(仅作用于 class)
循环引用规避__weak/__strong 双层模板[weak self] in + guard let self else { return }

值类型不参与 ARC,意味着大量临时数据(struct、原生 Array/Dictionary)省去了原子 retain/release 的开销,这是 Swift 性能优势的来源之一;代价则是值类型的"按值复制"在大对象上需要 COW(Copy-on-Write)来摊销。


3. 方法派发与运行时

派发方式直接决定调用开销与可被 hook 的程度,是两者最深层的差异。

声明派发方式是否可 swizzle
OC 任意方法objc_msgSend(动态)
Swift final class / struct 方法静态派发,可内联
Swift class 普通方法vtable 派发
Swift @objc 或继承 NSObject 的方法objc_msgSend
Swift @objc dynamicobjc_msgSend是(且支持 KVO)

含义

  • OC 每次方法调用都要走 objc_msgSend 查 SEL→IMP 表,调用开销固定较高,但带来强大的运行时能力(Aspects、KVO、JSPatch 历史等)。
  • Swift 默认偏静态。在 final class + WMO(Whole Module Optimization)下,方法可被完全内联,调用近乎零成本;代价是丧失大部分运行时反射能力,Mirror 也比 OC runtime 弱得多。
  • 想给 OC 用的 Swift 类型必须手动加 @objc、继承 NSObject,等于把那部分接口拉回到 objc_msgSend 派发:
public final class InboxCellConversationTagLabelPSPService: NSObject {
    @objc public static let shared = InboxCellConversationTagLabelPSPService()
}

4. 错误处理与并发

4.1 错误处理

OC 走 NSError ** 出参 —— 编译器不强制检查,漏判直接吞错:

NSError *error = nil;
id result = [foo doSomething:&error];
// 不写 if (error) 也能编译

Swift 推荐 throws / try,由编译器强制处理:

do { try foo.doSomething() } catch { ... }

但当 Swift 调用 OC API 时仍要回退到出参风格:

var error: StrategyError?
let result = StrategyPlatform.executeScene(scene, ..., error: &error)
if let error { ... }

4.2 并发

  • OC 完全靠开发者自觉:@synchronizedNSLock、GCD barrier、os_unfair_lock
  • Swift 5 之前与 OC 无异;Swift 5.5 引入 async/awaitactor;Swift 6 起的 Sendable 与严格并发模式可由编译器静态检查数据竞争——这是 OC 完全没有的能力。

5. Swift ↔ Objective-C 互操作(重点)

混编工程里这是真正"踩坑密集区",但绝大多数 Swift/OC 对比文章都没怎么讲。

5.1 Swift 想被 OC 调用的两个必要条件

  1. 类继承自 NSObject
  2. 公开成员加 @objc(或类整体 @objcMembers)。

不满足其中任一条,OC 头文件里就看不到。final class + @objc static let shared 是常见的妥协写法:保留 Swift 内部静态派发,仅入口暴露给 OC。

5.2 容器桥接的隐形成本

Swift 的 String / Array / Dictionary 是值类型;当传给只接收 NSDictionary / NSArray 的 OC API 时会发生桥接

  • 桥接 DictionaryNSDictionary 时不会立刻物化为 CF 容器,而是包装成 _SwiftDeferredNSDictionary
  • 大多数 OC 代码无感知;但当对方是基于 runtime 反射的 VM / 序列化框架时,可能不识别这个类。

⚠️ 实战坑:把 [String: Any] 透传给一个只认 NSDictionary 的 OC/C++ 策略 VM,桥接后类型变成 _SwiftDeferredNSDictionary,部分 runtime 检测会拒收。解决办法是字段直接以 NSDictionary 持有:

/// Kept as `NSDictionary` to avoid Swift-to-ObjC bridging issues
/// (`_SwiftDeferredNSDictionary` is not recognized by the strategy VM).
public let coreInfoExt: NSDictionary?

5.3 头文件 vs 模块

  • OC 走 #import + 头文件 + @class 前向声明;
  • Swift 走 import 模块名,无头文件。
  • 跨语言时由 Xcode 自动生成 <Module>-Swift.h(OC 看 Swift)和 modulemap(Swift 看 OC)。

副作用:Swift 一次小改可能触发跨模块大范围重编,是 Swift 编译慢的重要原因之一。


6. 性能对比

项目谁占优说明
方法调用开销Swift(静态/vtable)objc_msgSend 永远多一层 SEL→IMP 查表
数据结构Swift值类型不计 ARC、Array/Dictionary COW
字符串接近Swift String 用 UTF-8 + Small String;OC 也有 NSTaggedPointerString,但仅限短 ASCII
跨语言调用OCSwift→OC 需走 objc_msgSend + 容器桥接
编译速度OCSwift 类型推断 / 泛型展开较慢
二进制体积OCSwift 需要额外的 runtime 与元数据;不过 ABI 稳定后系统已内置
冷启动OC同上,加上 Swift 类型元数据注册成本

经验法则:纯 Swift 路径性能优于纯 OCSwift 调 OC 的混合路径反而比纯 OC 慢,因为多了桥接。


7. 安全性对比

项目Objective-CSwift
空安全nil 发消息不崩,但默默吞返回值,掩盖 bugOptional 强制处理,编译期发现问题
越界NSArray 越界崩,定位较难Array 越界 trap,崩在第一现场
类型id 万能,几乎无约束Any 必须显式 as? 转换
不可变性NSArray * 可被强转回 NSMutableArray 修改let + struct 复制天然隔离
数据竞争完全靠开发者Swift 6 Sendable / actor 静态检查
反射可被滥用强 runtime 易于 swizzle / 调私 API,安全审计困难默认不暴露这些能力

总体:Swift 把"开发者自觉"前置成了"编译器强制",对大型团队和长期维护更友好;OC 的灵活性反过来意味着审计与稳定性成本。


8. 何时选 Swift,何时保留 OC

优先选 Swift 的场景

  • 新业务模块、纯 Swift 调用链;
  • 强类型建模(数据结构、状态机、协议网络层);
  • 需要 actor / async 控制并发安全的场景;
  • 团队希望编译期消除一类常见 bug(空、类型、并发)。

继续用 OC 的合理场景

  • 基础设施 / SDK,需要被全工程(含老 OC 模块)调用;
  • 与 C / C++ 深度互操作(音视频、加解密、底层数据结构);
  • 大量依赖 method swizzling、KVO、消息转发的"框架级"代码;
  • 冷启动 / 二进制体积极敏感的入口模块;
  • 需要保留 runtime 注入、热修复式能力的边角场景。

混编工程的最佳实践

  • 把 Swift 类型尽量做成纯 Swift:struct + enum + 内部 final class
  • 必须暴露给 OC 的接口集中在一个桥接层 / Facade,对外只露 NSObject + @objc
  • 注意容器桥接:跨语言传 Dictionary / Array 时优先持有 NSDictionary / NSArray,避免 _SwiftDeferredNSDictionary 类问题;
  • 接受"Swift 编译慢"的事实,把 Swift 模块切小、用 WMO + final 帮编译器做静态优化。