基于 Swift 5.10 / 部分 Swift 6 特性 与现代 Objective-C(ARC 已开启)。 文中代码片段取自某大型 iOS 客户端的 IM 模块,仅作为论点的最小佐证。
核心要点
| 维度 | Objective-C | Swift |
|---|---|---|
| 类型系统 | 弱(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 的可空性是类型系统的一部分:String 与 String? 是不同类型,使用 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,并手动管理 @property、alloc/init、内存语义。
1.3 泛型
- OC 的"泛型"(
NSArray<NSString *> *)是 lightweight generics,仅供编译器类型检查,运行时仍是NSArray。 - Swift 是真泛型,可参与重载并通过 specialization 在编译期单态化(monomorphization),调用零开销。
2. 内存与生命周期
两者都基于 ARC,但模型不同:
| 项 | Objective-C | Swift |
|---|---|---|
| 作用对象 | 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 dynamic | objc_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 完全靠开发者自觉:
@synchronized、NSLock、GCD barrier、os_unfair_lock。 - Swift 5 之前与 OC 无异;Swift 5.5 引入
async/await、actor;Swift 6 起的Sendable与严格并发模式可由编译器静态检查数据竞争——这是 OC 完全没有的能力。
5. Swift ↔ Objective-C 互操作(重点)
混编工程里这是真正"踩坑密集区",但绝大多数 Swift/OC 对比文章都没怎么讲。
5.1 Swift 想被 OC 调用的两个必要条件
- 类继承自
NSObject; - 公开成员加
@objc(或类整体@objcMembers)。
不满足其中任一条,OC 头文件里就看不到。final class + @objc static let shared 是常见的妥协写法:保留 Swift 内部静态派发,仅入口暴露给 OC。
5.2 容器桥接的隐形成本
Swift 的 String / Array / Dictionary 是值类型;当传给只接收 NSDictionary / NSArray 的 OC API 时会发生桥接:
- 桥接
Dictionary→NSDictionary时不会立刻物化为 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 |
| 跨语言调用 | OC | Swift→OC 需走 objc_msgSend + 容器桥接 |
| 编译速度 | OC | Swift 类型推断 / 泛型展开较慢 |
| 二进制体积 | OC | Swift 需要额外的 runtime 与元数据;不过 ABI 稳定后系统已内置 |
| 冷启动 | OC | 同上,加上 Swift 类型元数据注册成本 |
经验法则:纯 Swift 路径性能优于纯 OC;Swift 调 OC 的混合路径反而比纯 OC 慢,因为多了桥接。
7. 安全性对比
| 项目 | Objective-C | Swift |
|---|---|---|
| 空安全 | nil 发消息不崩,但默默吞返回值,掩盖 bug | Optional 强制处理,编译期发现问题 |
| 越界 | 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帮编译器做静态优化。