Swift 全面深入指南
本文从底层原理、横向对比、纵向深度、性能优化、难点问题、高难度原理六大维度,对 Swift 语言进行全面、细致、深入的梳理。
第一部分:Swift 基础与底层原理
1. 值类型 vs 引用类型
1.1 核心区别
| 维度 | 值类型 (Value Type) | 引用类型 (Reference Type) |
|---|---|---|
| 代表 | struct, enum, tuple | class, closure |
| 存储 | 栈(小对象)/ 堆(大对象或含引用) | 堆 |
| 赋值语义 | 拷贝(Copy-on-Write 优化) | 共享引用 |
| 线程安全 | 天然线程安全(独立副本) | 需要同步机制 |
| 引用计数 | 无 | 有 ARC |
| 继承 | 不支持(enum/struct) | 支持(class) |
| deinit | 不支持 | 支持 |
| Identity | 无 === 操作 | 有 === 操作 |
1.2 底层内存布局
struct 布局:
- struct 的成员按声明顺序存储(有对齐填充)
- 小 struct(通常 ≤ 3 个 word,即 24 字节 on 64-bit)直接在栈上分配
- 大 struct 或含引用类型成员时,可能会被编译器优化到堆上(间接存储)
- 作为协议的 existential container 时,超过 3 word 会触发堆分配
class 布局:
- 堆上分配,包含:
- isa 指针(8 字节):指向类的元数据(metadata),用于动态派发
- 引用计数(8 字节):strong count + unowned count + weak count(打包在一个 64-bit InlineRefCounts 中)
- 实例变量:按声明顺序排列,有对齐
- 总 overhead 至少 16 字节(isa + refcount),加上 malloc 的 16 字节对齐 overhead
1.3 Copy-on-Write (COW) 深入
标准库 COW 实现(Array/Dictionary/Set/String):
- 内部持有一个引用类型的 buffer(如
_ArrayBuffer) - 赋值时只复制 buffer 的引用(引用计数 +1),O(1)
- 写入前检查
isKnownUniquelyReferenced(&buffer)- 如果引用计数 == 1,直接修改(无拷贝)
- 如果引用计数 > 1,先深拷贝 buffer,再修改
- 注意:自定义 struct 不会自动获得 COW,需要手动实现
自定义 COW 模式:
final class Storage<T> {
var value: T
init(_ value: T) { self.value = value }
}
struct COWWrapper<T> {
private var storage: Storage<T>
init(_ value: T) { storage = Storage(value) }
var value: T {
get { storage.value }
set {
if !isKnownUniquelyReferenced(&storage) {
storage = Storage(newValue)
} else {
storage.value = newValue
}
}
}
}
1.4 struct 中包含引用类型的代价
- struct 拷贝时,内部引用类型成员的引用计数也要 +1
- 如果 struct 有 N 个引用类型成员,一次拷贝就有 N 次 retain
- 这就是为什么大量包含引用类型的 struct 拷贝比纯 class 更慢
2. 内存管理 — ARC 深入
2.1 ARC 的本质
- ARC 是编译器在编译期自动插入
retain/release调用 - 不是 GC(垃圾回收),没有 stop-the-world
- 引用计数操作是原子的(使用
atomic_fetch_add等),保证线程安全 - 每次 retain/release 有 CPU 开销(原子操作 + 内存屏障)
2.2 引用计数的存储结构(Swift 5+)
InlineRefCounts (8 bytes):
┌─────────────────────────────────────────────────┐
│ strong RC (32 bit) │ unowned RC (31 bit) │ flags │
└─────────────────────────────────────────────────┘
- strong count:强引用计数。变为 0 时触发 deinit,释放实例内存(如果无 unowned/weak 引用)
- unowned count:unowned 引用计数 + 1(自身占 1)。变为 0 时释放 side table / 对象内存
- weak count:存储在 side table 中。当有 weak 引用时,对象会创建 side table
- Side Table:当需要 weak 引用或引用计数溢出时,从 InlineRefCounts 切换到指向 side table 的指针
2.3 strong / weak / unowned 深入对比
| 维度 | strong | weak | unowned |
|---|---|---|---|
| 引用计数 | +1 strong RC | 不增加 strong RC,增加 weak RC(side table) | +1 unowned RC |
| 解引用速度 | 最快(直接访问) | 较慢(需要检查 side table) | 快(直接访问,但有运行时检查) |
| 置 nil | 不会 | 对象释放后自动置 nil | 不会(对象释放后访问触发 fatal error) |
| Optional | 不要求 | 必须 Optional | 不要求 Optional |
| 内存释放时机 | strong RC = 0 时 deinit | 不影响释放 | 不影响 deinit,但影响内存回收 |
| 适用场景 | 默认所有权 | delegate、可能为 nil 的反向引用 | 生命周期确定不短于自身的引用 |
unowned 的危险与底层:
- unowned 引用在对象 deinit 后,内存不会立即释放(因为 unowned count > 0)
- 访问已 deinit 的 unowned 引用会触发 runtime trap(不是野指针,是确定性崩溃)
-
unowned(unsafe)可以跳过检查,行为类似 C 的悬垂指针,性能最高但最危险
weak 的底层机制:
- weak 引用不直接指向对象,而是通过 side table 间接引用
- 对象 deinit 时,runtime 遍历 side table 将所有 weak 引用置 nil
- 这就是为什么 weak 必须是 Optional —— 因为可能被置 nil
- weak 的读取需要加锁(原子操作),有性能开销
2.4 循环引用的三种场景与解决
场景一:两个对象互相持有
class A { var b: B? }
class B { var a: A? } // 循环引用!
// 解决:B 中用 weak var a: A?
场景二:闭包捕获 self
class ViewController {
var handler: (() -> Void)?
func setup() {
handler = { self.doSomething() } // self 持有 handler,handler 捕获 self
}
}
// 解决:handler = { [weak self] in self?.doSomething() }
场景三:嵌套闭包中的 capture list
handler = { [weak self] in
guard let self = self else { return }
// 这里 self 是 strong 的局部变量,闭包执行期间不会释放
someAsyncCall {
self.doSomething() // 安全,因为外层已经 guard 了
}
}
2.5 Autorelease Pool 在 Swift 中的角色
- Swift 原生对象不使用 autorelease(ARC 直接管理)
- 但与 ObjC 交互时(调用返回 ObjC 对象的方法),仍可能进入 autorelease pool
-
autoreleasepool { }在 Swift 中仍然可用,用于循环中大量创建临时 ObjC 对象时控制内存峰值
3. 协议 (Protocol) 底层原理
3.1 协议的两种使用方式
作为泛型约束(Static Dispatch):
func process<T: MyProtocol>(_ value: T) { value.doSomething() }
- 编译期确定类型,静态派发
- 编译器为每个具体类型生成特化版本(monomorphization / specialization)
- 性能最优,等同于直接调用
作为存在类型(Dynamic Dispatch):
func process(_ value: MyProtocol) { value.doSomething() }
// Swift 5.6+ 显式写法:func process(_ value: any MyProtocol)
- 运行时通过 Existential Container 动态派发
- 有性能开销
3.2 Existential Container 详细结构
Existential Container (5 words = 40 bytes on 64-bit):
┌──────────────────────────────────────────┐
│ Value Buffer (3 words = 24 bytes) │ ← 存储值或指向堆的指针
│ Metadata Pointer (1 word = 8 bytes) │ ← 指向类型元数据
│ PWT Pointer (1 word = 8 bytes) │ ← Protocol Witness Table 指针
└──────────────────────────────────────────┘
Value Buffer 策略:
- 值 ≤ 24 字节:inline 存储,直接放在 buffer 中(无堆分配)
- 值 > 24 字节:buffer 中存指向堆分配内存的指针
- 这就是为什么小 struct 遵循协议时没有额外堆分配
Protocol Witness Table (PWT):
- 每个「类型 + 协议」组合有一张 PWT
- PWT 是一个函数指针数组,每个协议方法对应一个条目
- 调用协议方法时:从 existential container 取出 PWT → 查表 → 间接调用
- 类似于 C++ 的 vtable,但针对的是协议而非类继承
Value Witness Table (VWT):
- 每个类型有一张 VWT,描述该类型的内存操作
- 包含:size、alignment、copy、move、destroy 等函数指针
- 存在类型赋值/拷贝时,通过 VWT 执行正确的内存操作
3.3 协议组合与多协议 existential
func process(_ value: ProtocolA & ProtocolB) { ... }
- existential container 会包含多个 PWT 指针(每个协议一个)
- 容器大小 = 24(buffer)+ 8(metadata)+ 8 × N(N 个协议的 PWT)
3.4 Class-Only Protocol 的优化
protocol MyDelegate: AnyObject { ... }
- 编译器知道遵循者一定是 class(引用类型)
- existential container 退化为:1 个 word 的引用 + metadata + PWT
- 不需要 24 字节的 value buffer
- 可以使用 weak/unowned 修饰
3.5 协议扩展 vs 协议要求方法的派发差异
protocol Greetable {
func greet() // 协议要求:PWT 动态派发
}
extension Greetable {
func greet() { print("Hello") } // 默认实现
func farewell() { print("Bye") } // 扩展方法:静态派发!
}
struct Person: Greetable {
func greet() { print("Hi, I'm a person") }
func farewell() { print("See you") }
}
let p: Greetable = Person()
p.greet() // "Hi, I'm a person" —— 动态派发,走 PWT
p.farewell() // "Bye" —— 静态派发!走协议扩展的默认实现
关键区别:
- 协议要求中声明的方法 → 在 PWT 中有条目 → 动态派发 → 能被遵循者重写
- 仅在协议扩展中定义的方法 → PWT 中无条目 → 静态派发 → 根据编译期类型决定
4. 泛型底层原理
4.1 泛型的实现方式
Swift 泛型采用类型擦除 + 运行时传递元数据的策略(不同于 C++ 的完全模板实例化):
- 编译器生成一份泛型函数的代码(不是每个类型一份)
- 运行时通过隐藏参数传递 type metadata 和 witness table
- 但在开启优化(-O)时,编译器会进行泛型特化(specialization),为常用类型生成直接调用的版本
4.2 泛型特化 (Specialization)
func swap<T>(_ a: inout T, _ b: inout T) { ... }
// 编译器优化后可能生成:
// swap_Int(...) ← 针对 Int 的特化版本
// swap_String(...) ← 针对 String 的特化版本
// swap_generic(...) ← 通用版本(需要 metadata)
特化条件:
- 编译器能看到泛型函数的实现(同一模块 或
@inlinable) - 能确定具体类型
- 优化级别 -O 或 -Osize
跨模块特化:
- 默认不能跨模块特化(泛型函数实现不可见)
-
@inlinable将函数体暴露给其他模块,允许跨模块特化 -
@frozen将 struct 布局暴露给其他模块
4.3 Type Erasure(类型擦除)模式
问题: 带 associatedtype 的协议不能直接作为存在类型
protocol Iterator {
associatedtype Element
func next() -> Element?
}
// let iter: Iterator ← 编译错误(Swift 5.6 以前)
// let iter: any Iterator ← Swift 5.7+ 部分支持
经典手动类型擦除:
struct AnyIterator<Element>: IteratorProtocol {
private let _next: () -> Element?
init<I: IteratorProtocol>(_ iterator: I) where I.Element == Element {
var iter = iterator
_next = { iter.next() }
}
func next() -> Element? { _next() }
}
原理: 用闭包捕获具体类型实例,对外暴露统一的泛型接口,擦除了具体类型信息。
4.4 some vs any(Swift 5.7+)
| 维度 |
some Protocol (Opaque Type) |
any Protocol (Existential Type) |
|---|---|---|
| 底层 | 编译期确定的固定类型(对调用者隐藏) | 运行时动态类型(existential container) |
| 派发 | 静态派发 | 动态派发(PWT) |
| 性能 | 高(无间接开销) | 低(堆分配 + 间接调用) |
| 类型一致性 | 同一函数返回的 some P 保证是同一类型 | 不保证 |
| 适用 | 返回值、属性 | 参数、集合元素 |
5. 方法派发 (Method Dispatch) 全面解析
5.1 四种派发方式
| 派发方式 | 速度 | 机制 | 适用场景 |
|---|---|---|---|
| 内联 (Inline) | 最快 | 编译器将函数体直接插入调用点 | 小函数、@inline(__always)
|
| 静态派发 (Static/Direct) | 快 | 编译期确定函数地址,直接 call | struct 方法、final 方法、private 方法 |
| 虚表派发 (V-Table) | 中 | 通过类的虚函数表间接调用 | class 的非 final 方法 |
| 消息派发 (Message) | 慢 | ObjC runtime 的 objc_msgSend | @objc dynamic 方法 |
5.2 Swift class 的虚函数表
Class Metadata:
┌──────────────────────┐
│ isa (指向 metaclass) │
│ superclass pointer │
│ cache (ObjC 兼容) │
│ data (ObjC 兼容) │
│ ... │
│ V-Table: │
│ [0] → method1() │
│ [1] → method2() │
│ [2] → method3() │
│ ... │
└──────────────────────┘
- 子类的 vtable 包含父类的所有方法条目 + 自己新增的
- override 时,子类 vtable 中对应位置替换为子类的函数指针
- 调用时:metadata → vtable[index] → 间接跳转
5.3 各场景的派发方式总结
| 声明位置 | 修饰符 | 派发方式 |
|---|---|---|
| struct 方法 | — | 静态 |
| enum 方法 | — | 静态 |
| class 方法 | — | 虚表 (V-Table) |
| class 方法 | final |
静态 |
| class 方法 | private |
静态(隐式 final) |
| class 方法 | @objc dynamic |
消息 (objc_msgSend) |
| protocol 要求方法 | 泛型约束 <T: P>
|
静态(特化后)/ Witness Table |
| protocol 要求方法 | 存在类型 any P
|
PWT 动态派发 |
| protocol 扩展方法 | — | 静态 |
| extension of class | — | 静态(不在 vtable 中!) |
重要陷阱:class 的 extension 中定义的方法是静态派发!
class Base {
func inVTable() { print("Base") } // vtable
}
extension Base {
func notInVTable() { print("Base ext") } // 静态派发!
}
class Sub: Base {
override func inVTable() { print("Sub") } // OK
// override func notInVTable() { } // 编译错误!不能 override
}
let obj: Base = Sub()
obj.inVTable() // "Sub" —— 动态派发
obj.notInVTable() // "Base ext" —— 静态派发
5.4 @objc 与 dynamic 的区别
| 修饰符 | 作用 | 派发方式 |
|---|---|---|
@objc |
将方法暴露给 ObjC runtime | 仍然是 vtable(Swift 侧) |
dynamic |
使用 ObjC 消息派发 | objc_msgSend |
@objc dynamic |
暴露给 ObjC 且使用消息派发 | objc_msgSend(可被 KVO/method swizzling) |
6. 闭包 (Closure) 底层原理
6.1 闭包的内存结构
闭包在 Swift 中是一个引用类型,底层结构:
Closure = 函数指针 + 上下文 (Context)
┌─────────────────────────┐
│ Function Pointer │ → 指向闭包体的代码
│ Context (Capture List) │ → 堆上分配的捕获变量
└─────────────────────────┘
- 如果闭包不捕获任何变量,退化为普通函数指针(无堆分配)
- 捕获变量时,编译器创建堆上的 context 对象,存储捕获的变量
6.2 捕获语义
默认捕获:引用捕获(变量)
var x = 10
let closure = { print(x) }
x = 20
closure() // 输出 20 —— 捕获的是变量本身(引用)
底层:编译器将 x 从栈上提升到堆上的一个 Box 中,闭包和外部代码共享同一个 Box。
capture list 捕获:值捕获
var x = 10
let closure = { [x] in print(x) }
x = 20
closure() // 输出 10 —— 捕获的是值的拷贝
6.3 逃逸闭包 vs 非逃逸闭包
| 维度 | @escaping |
非逃逸(默认) |
|---|---|---|
| 生命周期 | 超出函数作用域 | 函数返回前执行完毕 |
| 堆分配 | 必须堆分配 context | 编译器可能优化到栈上 |
| 捕获 self | 需要显式 self.
|
不需要 |
| 性能 | 有堆分配开销 | 可能零开销 |
withoutActuallyEscaping: 允许将非逃逸闭包临时当作逃逸闭包使用(高级场景)。
6.4 @autoclosure
- 将表达式自动包装为闭包,延迟求值
- 常用于
assert、??等需要短路求值的场景 - 底层就是一个无参闭包
() -> T
func logIfTrue(_ condition: @autoclosure () -> Bool) {
if condition() { print("True") }
}
logIfTrue(2 > 1) // 2 > 1 被自动包装为 { 2 > 1 }
7. 枚举 (Enum) 底层原理
7.1 简单枚举的内存布局
enum Direction { case north, south, east, west }
// sizeof = 1 字节(只需要区分 4 个 case,1 字节足够 256 个)
- 底层就是一个整数 tag(鉴别器/discriminator)
- case 数量 ≤ 256 → 1 字节;≤ 65536 → 2 字节;依此类推
7.2 关联值枚举的内存布局
enum Result {
case success(Int) // payload: 8 字节
case failure(String) // payload: 16 字节
}
// sizeof = max(payload) + tag = 16 + 1 = 17,对齐到 8 → 24 字节
- 采用 tagged union 策略
- 大小 = max(所有 case 的 payload) + tag 的大小(可能利用 spare bits 优化)
7.3 Optional 的底层:枚举的极致优化
// Optional<T> 就是:
enum Optional<Wrapped> {
case none
case some(Wrapped)
}
指针类型的 Optional 优化:
-
Optional<AnyObject>只占 8 字节(和非 Optional 一样!) - 因为指针不可能为
0x0,所以none用全零表示,some用有效指针值 - 这叫 spare bit optimization —— 利用值中不可能出现的 bit pattern 作为 tag
- 同理
Optional<Bool>= 1 字节(Bool 只有 0/1,用 2 表示 none)
7.4 indirect enum
indirect enum Tree {
case leaf(Int)
case node(Tree, Tree)
}
- 没有
indirect时,Tree 大小会无限递归(编译错误) -
indirect让关联值通过堆上的 Box 间接引用 - 底层类似于
case node(Box<Tree>, Box<Tree>),Box 是引用类型
8. Struct vs Class 的性能深入对比
8.1 分配与释放
| 操作 | struct (栈) | class (堆) |
|---|---|---|
| 分配 | 移动栈指针(1条指令) | malloc 系统调用(涉及锁、空闲链表搜索) |
| 释放 | 移动栈指针 | free + 引用计数归零检查 |
| 速度比 | ~1ns | ~25-100ns |
8.2 引用计数开销
- class 每次传递都要 retain(原子操作 ~5ns)
- 多线程下原子操作可能导致 cache line bouncing
- struct 无引用计数,但含引用类型成员时有间接引用计数开销
8.3 缓存友好性
- struct 的数组
[MyStruct]:连续内存,cache 友好 - class 的数组
[MyClass]:数组存的是指针,实际对象分散在堆上,cache miss 率高 - 这在遍历大数据集时差距巨大
9. String 底层原理
9.1 Small String Optimization (SSO)
- Swift String 占 16 字节(2 个 word)
- 短字符串(≤ 15 字节 UTF-8)直接 inline 存储在这 16 字节中,无堆分配
- 长字符串在堆上分配 buffer,16 字节中存 buffer 指针 + 长度 + flags
- 判断标志位在最高位
9.2 String 的字符模型
- Swift 的
Character是扩展字形簇 (Extended Grapheme Cluster) - 一个
Character可能对应多个 Unicode 标量(如 emoji 👨👩👧👦 = 7 个标量) - 因此
String.count是 O(n) 复杂度(需要遍历确定字形簇边界) -
String.Index不是整数,是不透明的偏移量,因为字符宽度不固定
9.3 String 的多种视图
| 视图 | 元素 | 场景 |
|---|---|---|
string.utf8 |
UTF8.CodeUnit (UInt8) | 网络传输、C 交互 |
string.utf16 |
UTF16.CodeUnit (UInt16) | NSString 兼容 |
string.unicodeScalars |
Unicode.Scalar | Unicode 处理 |
string (默认) |
Character | 用户可见字符 |
9.4 String 与 NSString 的桥接
- Swift String 和 NSString 可以零成本桥接(toll-free bridging 的 Swift 版本)
- 但底层编码不同:Swift 用 UTF-8,NSString 用 UTF-16
- 桥接时可能触发转码,有性能开销
-
String被传给 ObjC API 时可能创建临时 NSString(autorelease 对象)
10. 属性 (Property) 的底层机制
10.1 存储属性 vs 计算属性
| 类型 | 内存 | 本质 |
|---|---|---|
| 存储属性 | 占实例内存 | 实际的内存字段 |
| 计算属性 | 不占内存 | getter/setter 方法 |
| lazy 属性 | Optional 存储 | 首次访问时初始化 |
10.2 属性观察器 (willSet/didSet) 底层
var name: String {
willSet { print("将变为 \(newValue)") }
didSet { print("已从 \(oldValue) 变为 \(name)") }
}
编译器展开为:
var _name: String
var name: String {
get { _name }
set {
let oldValue = _name
// willSet(newValue)
_name = newValue
// didSet(oldValue)
}
}
注意:init 中赋值不会触发 willSet/didSet。
10.3 Property Wrapper 底层
@propertyWrapper struct Clamped {
var wrappedValue: Int { ... }
var projectedValue: Clamped { self }
}
struct Config {
@Clamped var volume: Int
}
编译器展开为:
struct Config {
private var _volume: Clamped
var volume: Int {
get { _volume.wrappedValue }
set { _volume.wrappedValue = newValue }
}
var $volume: Clamped { _volume.projectedValue }
}
10.4 KeyPath 底层
- KeyPath 是一个类型安全的属性引用,底层是一个对象
- 编译器生成一系列 offset/accessor 信息
- 支持组合(
\Base.a.b.c)、运行时读写 -
WritableKeyPath、ReferenceWritableKeyPath等继承层级 - KeyPath 的读取性能接近直接属性访问(编译器优化后)
11. 并发 (Concurrency) — Swift Concurrency
11.1 Actor 模型
actor BankAccount {
var balance: Double = 0
func deposit(_ amount: Double) { balance += amount }
}
底层原理:
- Actor 内部有一个串行执行器 (Serial Executor)
- 所有对 actor 的方法调用被排列在执行器的队列中
- 保证同一时刻只有一个任务在执行 actor 的代码
- 跨 actor 调用需要
await(可能涉及线程切换)
Actor 隔离 (Isolation):
- Actor 的属性和方法默认是 isolated 的
- 外部访问需要
await(异步访问) -
nonisolated标记的方法可以不需要 await(不能访问 mutable 状态)
11.2 Structured Concurrency
async let result1 = fetchData1()
async let result2 = fetchData2()
let combined = await (result1, result2)
- 子任务的生命周期绑定到父作用域
- 父任务取消时,子任务自动取消
- 子任务完成前,父作用域不会退出
11.3 Task 与 TaskGroup
Task:
-
Task { }创建非结构化的顶级任务 -
Task.detached { }创建完全独立的任务(不继承 actor context) - Task 持有对结果的引用,可以
await task.value
TaskGroup:
await withTaskGroup(of: Int.self) { group in
for i in 0..<10 {
group.addTask { await compute(i) }
}
for await result in group { ... }
}
11.4 Sendable 协议
- 标记类型可以安全跨并发域传递
- 值类型自动满足 Sendable(如果所有成员都是 Sendable)
- class 需要满足:final + 所有属性 immutable (let) + Sendable
-
@Sendable标记闭包,禁止捕获可变状态 - 编译器在严格并发检查模式下会检查 Sendable 合规性
11.5 async/await 底层 —— Continuation
func fetchData() async -> Data { ... }
- 编译器将 async 函数转换为状态机
- 每个
await点是一个 suspension point(挂起点) - 函数被分割为多个 continuation(延续)
- 挂起时不阻塞线程,线程可以执行其他任务
- 恢复时通过 continuation 跳回正确的执行点
- 这就是 协程 (Coroutine) 的实现
与 GCD 的区别:
| 维度 | GCD | Swift Concurrency |
|---|---|---|
| 线程模型 | 每个 block 可能在不同线程 | 协程,挂起不占线程 |
| 线程爆炸 | 容易创建过多线程 | 协作式线程池(线程数 ≤ CPU 核心数) |
| 结构化 | 无 | Task 层级结构,自动取消传播 |
| 安全性 | 手动保证 | Actor 隔离,Sendable 检查 |
12. 类型系统高级特性
12.1 Phantom Type(幻影类型)
enum Kilometers {}
enum Miles {}
struct Distance<Unit> {
let value: Double
}
// Distance<Kilometers> 和 Distance<Miles> 是不同类型
// Unit 从未被使用为值,只在类型层面区分 → 零开销
12.2 Result Builder
@resultBuilder struct ArrayBuilder {
static func buildBlock(_ components: Int...) -> [Int] {
components
}
}
- 编译器将 DSL 块内的语句转换为
buildBlock/buildOptional/buildEither等方法调用 - SwiftUI 的
@ViewBuilder就是 result builder
12.3 Metatype(元类型)
let type: Int.Type = Int.self // Int 的元类型
let obj = type.init(42) // 用元类型创建实例
-
.self获取类型本身的值 -
.Type是类型的元类型 -
type(of: instance)获取运行时动态类型 - 对于 class,
type(of:)返回的可能是子类类型
第二部分:第三方常用库原理
1. Alamofire
1.1 架构分层
Request → SessionManager → URLSession → URLSessionTask
↑ ↑ ↑
Encoding ServerTrust Interceptor
1.2 核心原理
- 基于
URLSession封装,使用URLSessionDelegate统一管理回调 -
请求拦截器 (RequestInterceptor):
adapt修改请求(如添加 token),retry处理重试 -
响应序列化:通过
ResponseSerializer协议将Data转为目标类型 - 请求链:请求排队 → 适配 → 发送 → 验证 → 序列化 → 回调
-
证书锁定 (Certificate Pinning):通过
ServerTrustManager实现,防中间人攻击
1.3 重要设计模式
-
Builder 模式:链式调用
.validate().responseDecodable(of:) -
命令模式:
Request封装了一次完整请求的所有信息 -
策略模式:
ParameterEncoding协议的不同实现(URL/JSON/Custom)
2. Kingfisher
2.1 核心架构
KingfisherManager
├── ImageDownloader(网络下载)
└── ImageCache
├── MemoryCache(NSCache)
└── DiskCache(FileManager)
2.2 缓存策略
-
内存缓存:基于
NSCache,系统内存紧张时自动清理 - 磁盘缓存:文件名 = URL 的 MD5 哈希,支持过期时间和大小限制
- 查找顺序:内存 → 磁盘 → 网络下载
-
缓存键:默认为 URL 字符串,可自定义
CacheKeyFilter
2.3 图片处理管线
- Processor:下载后/缓存前进行图片处理(裁剪、模糊、圆角等)
- 处理后的图片以
原始key + processor标识为 key 缓存 - 支持渐进式 JPEG 加载、GIF 动画、SVG
2.4 性能优化细节
- 下载使用
URLSession,支持 HTTP/2 多路复用 - 图片解码在后台线程,避免阻塞主线程
- 支持下载优先级和取消(cell 复用时取消旧请求)
-
ImagePrefetcher预加载机制
3. SnapKit
3.1 底层原理
- 本质是对 Auto Layout 的
NSLayoutConstraint的 DSL 封装 -
链式调用:每个方法返回
ConstraintMaker/ConstraintDescription -
make.top.equalTo(view).offset(10)最终等价于创建一个NSLayoutConstraint -
约束更新:
snp.updateConstraints找到已有约束修改 constant,比重新创建高效 -
约束引用:可以保存约束引用后续修改
constraint.update(offset: 20)
3.2 与原生 API 的性能对比
- SnapKit 在约束创建阶段有少量 wrapper 开销(可忽略)
- 约束解算性能完全等同于原生 Auto Layout(最终都走同一个 Cassowary 算法引擎)
- 主要价值是可读性和维护性
4. RxSwift / Combine 响应式框架
4.1 核心概念对比
| 概念 | RxSwift | Combine |
|---|---|---|
| 数据流 | Observable | Publisher |
| 消费者 | Observer | Subscriber |
| 取消 | Disposable / DisposeBag | AnyCancellable |
| 背压 | 无原生支持 | Demand 机制 |
| 调度器 | Scheduler | Scheduler |
| Subject | PublishSubject/BehaviorSubject | PassthroughSubject/CurrentValueSubject |
4.2 RxSwift 底层原理
-
Observable是一个持有subscribe闭包的结构 -
subscribe时创建Sink(桥梁),连接 Observable 和 Observer - 操作符(map/filter 等)创建新的 Observable,形成链式管道
-
DisposeBag在 deinit 时调用所有 Disposable 的 dispose,断开链条
4.3 Combine 底层原理
- 基于 Publisher-Subscriber 协议
-
背压 (Backpressure):Subscriber 通过
Demand控制接收速率 - Publisher 是值类型(struct),Subscriber 是引用类型(class)
-
sink/assign等返回AnyCancellable,释放即取消订阅
5. SwiftUI 数据流原理
5.1 属性包装器对比
| Property Wrapper | 所有权 | 触发刷新 | 适用场景 |
|---|---|---|---|
@State |
View 拥有 | 值变化时 | View 内部简单状态 |
@Binding |
不拥有(引用) | 值变化时 | 父子 View 双向绑定 |
@ObservedObject |
不拥有 | objectWillChange 时 | 外部注入的 ObservableObject |
@StateObject |
View 拥有 | objectWillChange 时 | View 创建的 ObservableObject |
@EnvironmentObject |
不拥有 | objectWillChange 时 | 跨层级的 ObservableObject |
@Environment |
不拥有 | 值变化时 | 系统环境值 |
5.2 View 的 diff 更新机制
- SwiftUI View 是值类型 struct,每次状态变化创建新的 View 值
- SwiftUI 通过 attribute graph 追踪依赖关系
- 只有依赖的数据发生变化的 View 才会被重新 body 求值
- body 返回的新旧 View tree 做 structural diff,只更新差异部分
第三部分:开发难点与解决方案
1. 内存泄漏排查
1.1 常见泄漏场景
| 场景 | 根因 | 解决 |
|---|---|---|
| 闭包捕获 self | 循环引用 |
[weak self] / [unowned self]
|
| delegate 强引用 | 循环引用 | delegate 用 weak 声明 |
| Timer 持有 target | Timer → self → Timer |
Timer.scheduledTimer(withTimeInterval:repeats:block:) + [weak self]
|
| NotificationCenter addObserver | iOS 8 以下需手动 remove | block-based API + [weak self]
|
| DispatchWorkItem 捕获 | 闭包内持有 self | 取消 workItem 或 [weak self]
|
| WKWebView 与 JS 交互 | WKScriptMessageHandler 被 WKUserContentController 强持有 | 使用中间代理对象弱引用 self |
1.2 排查工具链
- Xcode Memory Graph Debugger:可视化对象引用关系,直接定位循环引用
- Instruments - Leaks:运行时检测内存泄漏
- Instruments - Allocations:查看对象生命周期和内存分配
- deinit 打印:在 deinit 中加 print 确认对象释放
- MLeaksFinder(第三方):自动检测 ViewController 泄漏
1.3 难点:隐蔽的循环引用
// 难以发现的泄漏:闭包嵌套
class ViewModel {
var onUpdate: (() -> Void)?
func start() {
NetworkManager.shared.request { [weak self] data in
self?.onUpdate = {
// 这里隐式捕获了 self(strong),因为 onUpdate 是 self 的属性
// 而 self?.onUpdate = ... 外层已经是 weak self
// 但 inner closure 没有 weak!
self?.process(data) // 如果这里 self 已经 unwrap 为 strong...
}
}
}
}
2. 多线程数据竞争
2.1 经典问题
var array = [Int]()
DispatchQueue.concurrentPerform(iterations: 1000) { i in
array.append(i) // 崩溃!Array 非线程安全
}
2.2 解决方案对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| Serial DispatchQueue | 简单直观 | 完全串行,性能差 |
| Concurrent Queue + Barrier | 读并发,写独占 | 代码稍复杂 |
| NSLock / pthread_mutex | 最轻量 | 需要手动 lock/unlock |
| os_unfair_lock | 最快的互斥锁 | 不支持递归 |
| Actor (Swift 5.5+) | 编译器保证安全 | 异步调用 |
| @Atomic property wrapper | 属性级别保护 | 单次操作安全,复合操作不安全 |
2.3 Barrier 读写锁模式
class ThreadSafeArray<T> {
private var array = [T]()
private let queue = DispatchQueue(label: "safe", attributes: .concurrent)
func read<R>(_ block: ([T]) -> R) -> R {
queue.sync { block(array) } // 并发读
}
func write(_ block: @escaping (inout [T]) -> Void) {
queue.async(flags: .barrier) { block(&self.array) } // 独占写
}
}
3. 大量数据的列表性能
3.1 问题
- 大量 cell 导致滚动卡顿
- 图片加载闪烁
- 内存暴涨
3.2 解决方案
| 问题 | 解决方案 |
|---|---|
| cell 创建开销 | 复用机制 dequeueReusableCell
|
| 图片解码卡主线程 | 异步解码 + 缓存解码后的 bitmap |
| 复杂 cell 布局 | 预计算 cell 高度,缓存布局结果 |
| 透明度 / 离屏渲染 | 避免 cornerRadius + masksToBounds,用 CAShapeLayer 或预渲染圆角图 |
| 大量图片内存 | Kingfisher/SDWebImage 的缩略图 + downsampling |
| Diff 更新 | DiffableDataSource / IGListKit / 手动 diff 只更新变化的 cell |
4. 启动优化
4.1 启动阶段拆解
冷启动:
1. 内核创建进程
2. dyld 加载 → 动态库绑定 → rebase/bind
3. +load / __attribute__((constructor))
4. Runtime 初始化(ObjC class 注册、category attach)
5. main() 函数
6. AppDelegate → UIWindow → 首屏渲染
4.2 优化手段
| 阶段 | 优化方式 |
|---|---|
| dyld | 减少动态库数量(合并为 1 个);使用静态库 |
| +load | 移到 +initialize 或懒加载 |
| 二进制 | 二进制重排(Profile-Guided Optimization),减少 Page Fault |
| main 后 | 延迟非必要初始化,首屏数据预加载/缓存 |
| 渲染 | 简化首屏 UI,避免首屏大量 Auto Layout |
5. 崩溃治理
5.1 常见崩溃类型
| 崩溃 | 原因 | Swift 中的表现 |
|---|---|---|
| EXC_BAD_ACCESS | 野指针 / 访问已释放内存 | 极少(ARC + 值类型),除非 unowned(unsafe) 或 Unsafe 指针 |
| EXC_BREAKPOINT | trap 指令 |
fatalError、force unwrap nil、数组越界 |
| SIGABRT | abort() | 断言失败、unrecognized selector(ObjC 交互) |
| OOM | 内存超限 | 无 crash log(Jetsam),需要 MetricKit |
5.2 Swift 特有崩溃
-
Force unwrap nil:
let x: Int = optional!—— 最常见 - Array index out of range:下标越界
- Unowned reference after dealloc:访问已释放的 unowned 对象
- Exhaustive switch:enum 新增 case 但 switch 未覆盖(@unknown default)
第四部分:性能优化深入细节
1. 编译器优化
1.1 关键编译选项
| 选项 | 含义 | 效果 |
|---|---|---|
-Onone |
无优化(Debug) | 保留所有调试信息 |
-O |
标准优化(Release) | 内联、泛型特化、死代码消除 |
-Osize |
优化体积 | 减少内联,优先选择小代码 |
-Ounchecked |
移除安全检查 | 数组越界、溢出检查被移除,危险但最快 |
| WMO (Whole Module Optimization) | 全模块优化 | 跨文件内联/特化/去虚拟化 |
1.2 WMO 的重要性
- 非 WMO 模式下,每个文件单独编译,看不到其他文件的实现
- WMO 允许编译器将非 public/非 open 的 class 方法去虚拟化(直接调用)
- 将
internal方法从 vtable 派发降级为静态派发 - 自动推断
final(如果子类在整个模块中不存在)
1.3 帮助编译器优化的编码技巧
| 技巧 | 原因 |
|---|---|
用 final 修饰不需要继承的 class |
静态派发 |
用 private / fileprivate
|
编译器可推断 final,静态派发 |
| 用 struct 而非 class | 无引用计数,栈分配 |
| 避免过大的协议 existential | 减少堆分配 |
用 @inlinable 暴露关键路径 |
跨模块内联优化 |
用 @frozen 标记稳定的 struct/enum |
允许编译器直接操作内存布局 |
| 减少不必要的 Optional | 减少分支和 unwrap 开销 |
2. 内存优化
2.1 减少堆分配
| 场景 | 优化 |
|---|---|
| 小对象 | 用 struct 替代 class |
| 协议类型 | 用泛型约束替代 existential |
| 闭包 | 非逃逸闭包(编译器可栈分配) |
| String | 短字符串利用 SSO |
| 数组 |
Array.reserveCapacity(_:) 预分配,避免多次扩容拷贝 |
2.2 减少引用计数操作
- 减少 class 实例的传递次数
- struct 中减少引用类型成员数量
- 用
let代替var(编译器可以省略某些 retain/release) - 考虑
Unmanaged<T>手动管理引用计数(高性能场景)
2.3 内存对齐与布局优化
// 不好:padding 浪费
struct Bad {
let a: Bool // 1 byte + 7 padding
let b: Int64 // 8 bytes
let c: Bool // 1 byte + 7 padding
} // 总共 24 bytes
// 好:重排成员减少 padding
struct Good {
let b: Int64 // 8 bytes
let a: Bool // 1 byte
let c: Bool // 1 byte + 6 padding
} // 总共 16 bytes
Swift 编译器不会自动重排 struct 成员(为了保持 ABI 兼容),需要手动优化。
3. 集合操作优化
3.1 Lazy Collection
// 非 lazy:创建 3 个中间数组
let result = array.filter { $0 > 0 }.map { $0 * 2 }.prefix(5)
// lazy:单次遍历,按需计算,无中间数组
let result = array.lazy.filter { $0 > 0 }.map { $0 * 2 }.prefix(5)
-
lazy将操作转为惰性求值 - 只遍历一次,遇到满足条件的前 5 个就停止
- 适合大数组 + 链式操作 + 只取部分结果
3.2 Dictionary 性能
-
Dictionary使用开放寻址 + 线性探测哈希表 - 负载因子超过 75% 自动扩容(容量翻倍 + rehash)
-
Dictionary.reserveCapacity(_:)可预分配 - 自定义 Hashable 时注意 hash 分布均匀性
-
Dictionary(grouping:by:)比手动 for 循环分组更高效
3.3 ContiguousArray vs Array
-
Array需要兼容 NSArray 桥接,有额外判断开销 -
ContiguousArray保证连续内存存储,不支持 NSArray 桥接 - 存储非 class、非 @objc 类型时两者等效
- 存储 class 类型且确定不需要 ObjC 桥接时,
ContiguousArray更快
4. 字符串性能
4.1 避免频繁拼接
// 差:每次 += 可能触发拷贝和堆分配
var s = ""
for i in 0..<1000 { s += "\(i)" }
// 好:预分配
var s = ""
s.reserveCapacity(4000)
for i in 0..<1000 { s += "\(i)" }
// 更好:用数组 join
let s = (0..<1000).map(String.init).joined()
4.2 子串 Substring
-
Substring与原String共享底层 buffer(COW) - 长期持有
Substring会阻止原 String buffer 释放 - 短期使用
Substring,长期存储时转为String(substring)
5. 减少动态派发
5.1 性能对比数据
| 派发方式 | 相对开销 |
|---|---|
| 内联 | 0(最快) |
| 静态派发 | 1x |
| vtable 派发 | ~1.1x - 1.5x(间接跳转 + 可能的 cache miss) |
| PWT 派发 | ~1.5x - 2x(多一次间接寻址) |
| objc_msgSend | ~3x - 5x(查找 IMP 缓存) |
5.2 优化方法
- struct > class(天然静态派发)
- final class / final method(静态派发)
- private / fileprivate method(隐式 final)
-
泛型约束
<T: P>> 存在类型any P(可特化为静态派发) - WMO 开启(自动去虚拟化)
第五部分:八股文中的横向对比
1. 值类型 vs 引用类型(深度对比)
| 对比维度 | 值类型 | 引用类型 |
|---|---|---|
| 拷贝语义 | 深拷贝(COW 优化后延迟拷贝) | 浅拷贝(共享引用) |
| 身份判断 | 无法判断「同一个」(只有值相等) |
=== 判断同一实例 |
| 多态 | 协议实现 + 泛型 | 继承 + 协议 |
| 线程安全 | 天然安全 | 需同步 |
| 析构 | 无 deinit | 有 deinit |
| 内存位置 | 栈/内联(优先) | 堆 |
| 引用计数 | 无 | 有(ARC) |
| 适用场景 | 数据模型、算法、并发安全 | 共享状态、标识语义、继承层级 |
选择原则: 默认用 struct,只在需要共享状态、继承、deinit、identity 时用 class。
2. struct vs class vs enum vs actor
| 特性 | struct | class | enum | actor |
|---|---|---|---|---|
| 类型 | 值 | 引用 | 值 | 引用 |
| 继承 | 不支持 | 支持 | 不支持 | 不支持 |
| 协议遵循 | 支持 | 支持 | 支持 | 支持 |
| deinit | 无 | 有 | 无 | 有 |
| 可变性 | mutating | 自由修改 | mutating | 隔离保护 |
| 线程安全 | 拷贝安全 | 需手动 | 拷贝安全 | 编译器保证 |
| 引用计数 | 无 | 有 | 无 | 有 |
| 内存 | 栈优先 | 堆 | 栈优先 | 堆 |
3. let vs var(底层差异)
| 维度 | let |
var |
|---|---|---|
| 可变性 | 不可变 | 可变 |
| 编译器优化 | 更多(常量折叠、省略 retain/release) | 较少 |
| 线程安全 | 安全(不可变) | 不安全 |
| 引用类型 | 引用不可变(属性仍可变) | 引用可变 |
4. map vs flatMap vs compactMap
| 方法 | 签名 | 作用 |
|---|---|---|
map |
(T) -> U |
1:1 转换 |
flatMap |
(T) -> [U] |
1:N 转换后展平 |
compactMap |
(T) -> U? |
1:1 转换,自动过滤 nil |
let a = [[1,2],[3,4]]
a.map { $0 } // [[1,2],[3,4]]
a.flatMap { $0 } // [1,2,3,4]
let b = ["1","a","3"]
b.compactMap { Int($0) } // [1, 3]
Optional 上的 flatMap:
let x: Int? = 5
x.flatMap { $0 > 3 ? $0 : nil } // Optional(5)
x.map { $0 > 3 ? $0 : nil } // Optional(Optional(5)) → Int??
5. GCD vs Operation vs Swift Concurrency
| 维度 | GCD | Operation | Swift Concurrency |
|---|---|---|---|
| 抽象层级 | 低(C API) | 中(ObjC 对象) | 高(语言级别) |
| 取消 | 手动检查 |
isCancelled 属性 |
结构化自动传播 |
| 依赖管理 | 手动 dispatch_group/barrier | addDependency |
async let / TaskGroup |
| 线程控制 | QoS + target queue | maxConcurrentOperationCount |
协作式线程池 |
| 线程爆炸 | 容易 | 容易 | 不会(线程数 ≤ 核心数) |
| 错误处理 | 无内建 | 无内建 | throws + try await |
| 安全保证 | 无 | 无 | Actor + Sendable |
6. weak vs unowned vs unowned(unsafe)
| 维度 | weak | unowned | unowned(unsafe) |
|---|---|---|---|
| 类型 | Optional | 非 Optional | 非 Optional |
| 对象释放后 | 自动 nil | trap 崩溃 | 野指针(UB) |
| 性能开销 | Side table + 原子操作 | 较少 | 零额外开销 |
| 安全性 | 最安全 | 安全(确定性崩溃) | 最危险 |
| 适用场景 | delegate、不确定生命周期 | 确定不会先于 self 释放 | 极致性能,生命周期绝对保证 |
7. Any vs AnyObject vs any Protocol vs some Protocol
| 类型 | 含义 | 底层 |
|---|---|---|
Any |
任意类型(值/引用) | existential container (32 bytes) |
AnyObject |
任意引用类型 | 单指针 (8 bytes) |
any Protocol |
任意遵循 P 的类型 | existential container |
some Protocol |
某个特定的遵循 P 的类型(编译期确定) | 无 container,直接值 |
8. 访问控制对比
| 级别 | 可见范围 | 编译器优化影响 |
|---|---|---|
open |
任何模块可继承和 override | 不能优化派发 |
public |
任何模块可访问,不可继承 override | 不能优化派发(外部可能做协议遵循等) |
internal |
同一模块(默认) | WMO 下可推断 final |
fileprivate |
同一文件 | 可推断 final |
private |
同一声明作用域 | 隐式 final,静态派发 |
9. 闭包 vs 函数 vs 方法
| 维度 | 全局函数 | 实例方法 | 闭包 |
|---|---|---|---|
| 类型 | (Args) -> Return |
(Self) -> (Args) -> Return(柯里化) |
(Args) -> Return |
| 捕获 | 无 | 隐式捕获 self | 显式/隐式捕获环境 |
| 堆分配 | 无 | 无(作为闭包传递时有) | 有(逃逸时) |
10. throws vs Result vs Optional
| 方式 | 适用场景 | 性能 | 链式处理 |
|---|---|---|---|
throws |
同步错误处理 | 正常路径零开销(Swift 使用 error return) | do-catch |
Result<T, E> |
异步回调 / 存储结果 | enum 开销(极小) | map/flatMap |
Optional<T> |
值可能不存在 | 最小 | map/flatMap/?? |
第六部分:高难度深底层原理
1. Swift Runtime 与 Metadata 系统
1.1 类型元数据 (Type Metadata)
每个 Swift 类型在运行时都有一个元数据 (Metadata) 记录:
Struct Metadata:
┌─────────────────────────┐
│ Kind (标识类型种类) │ ← struct/class/enum/optional/tuple...
│ Type Descriptor │ → 指向类型描述符(名称、字段、泛型参数等)
│ Value Witness Table Ptr │ → VWT(size/alignment/copy/destroy 等操作)
└─────────────────────────┘
Class Metadata (ISA):
┌─────────────────────────┐
│ Kind │
│ SuperClass Pointer │ → 父类元数据
│ Cache / Data (ObjC兼容) │
│ Flags │
│ Instance Size │
│ Instance Alignment │
│ Type Descriptor │
│ V-Table entries... │ → 虚函数表
└─────────────────────────┘
1.2 泛型 Metadata 的懒创建
- 泛型类型如
Array<Int>的 metadata 是运行时按需创建的 - 首次使用
Array<Int>时,runtime 用模板 +Int.self的 metadata 组合生成 - 生成后缓存在全局表中(线程安全的 concurrent hash map)
- 这就是为什么泛型类型的首次使用可能比后续使用略慢
1.3 Mirror 反射的底层
let mirror = Mirror(reflecting: someInstance)
for child in mirror.children { ... }
- Mirror 通过 Type Descriptor 中的字段描述信息获取属性名和偏移量
- 通过 Value Witness Table 中的操作函数读取字段值
- 属于「有限反射」—— 只能读取,不能修改(不像 Java/ObjC 的完全运行时反射)
- Release 模式下如果类型信息被 strip,反射能力会受限
2. SIL (Swift Intermediate Language)
2.1 编译流程
Swift Source → AST → SIL (raw) → SIL (canonical) → SIL (optimized) → LLVM IR → Machine Code
↑ ↑ ↑ ↑
解析/类型检查 SILGen 强制诊断/优化 LLVM 优化
2.2 SIL 的作用
- 类型检查之后、LLVM 之前的中间表示
- 比 LLVM IR 更高级,保留了 Swift 的类型信息
- 用于:
- ARC 优化:合并/消除冗余的 retain/release
- 泛型特化:生成具体类型的特化版本
- 去虚拟化:将 vtable 调用转为直接调用
- 内联:将小函数体直接插入调用点
- 诊断:检测未初始化变量、排他性访问违规等
2.3 查看 SIL
swiftc -emit-sil file.swift # 优化前的 SIL
swiftc -emit-sil -O file.swift # 优化后的 SIL
SIL 中可以直接看到 retain/release 的插入位置、dispatch 方式、内联决策等。
3. 排他性访问 (Exclusivity Enforcement)
3.1 原则
Swift 保证同一时刻不能同时存在对同一变量的读访问和写访问(Law of Exclusivity)。
3.2 静态检查
var x = 1
swap(&x, &x) // 编译错误!同时对 x 进行两个写访问
3.3 动态检查
var array = [1, 2, 3]
// 运行时可能崩溃:对 array 同时读 (subscript) 和写 (modifyElement)
extension Array {
mutating func modifyFirst(using: (inout Element) -> Void) {
using(&self[0]) // self 正在被修改,又通过 subscript 修改
}
}
3.4 底层实现
- 编译器在变量的访问开始/结束时插入
begin_access/end_access标记 - 栈上变量:编译器静态证明(大部分情况)
- 堆上变量/全局变量:运行时维护访问记录栈,检测冲突
- Debug 模式检查更严格,Release 中部分检查被优化掉
4. 内存安全与 Unsafe API
4.1 Swift 的安全保证
- 变量使用前必须初始化
- 数组下标自动检查越界
- 整数溢出自动检测(Debug 模式)
- Optional 强制解包检查
- 排他性访问检查
4.2 Unsafe 指针体系
| 类型 | 含义 | 等价 C 类型 |
|---|---|---|
UnsafePointer<T> |
只读指针 | const T* |
UnsafeMutablePointer<T> |
可变指针 | T* |
UnsafeRawPointer |
无类型只读指针 | const void* |
UnsafeMutableRawPointer |
无类型可变指针 | void* |
UnsafeBufferPointer<T> |
只读指针 + 长度 |
const T* + size_t
|
UnsafeMutableBufferPointer<T> |
可变指针 + 长度 |
T* + size_t
|
OpaquePointer |
不透明指针 | C 的 opaque struct pointer |
Unmanaged<T> |
手动管理引用计数的引用 | CFTypeRef |
4.3 使用场景
- C 库交互(Core Audio, Metal, 网络底层等)
- 高性能数据处理(避免 ARC / 边界检查开销)
- 内存映射文件操作
4.4 常见陷阱
// 危险:指针悬垂
var ptr: UnsafeMutablePointer<Int>?
do {
var x = 42
ptr = UnsafeMutablePointer(&x)
}
ptr?.pointee // 未定义行为!x 已超出作用域
// 正确:使用 withUnsafe 系列方法
withUnsafePointer(to: &x) { ptr in
// ptr 仅在此闭包内有效
}
5. ABI 稳定性 (Swift 5+)
5.1 什么是 ABI 稳定
- ABI (Application Binary Interface):二进制层面的接口约定
- 包括:函数调用约定、类型内存布局、name mangling、元数据格式、runtime 接口
- Swift 5 之后 ABI 稳定 → Swift runtime 嵌入 OS → App 不再需要内嵌 Swift runtime → 包体积减小
5.2 Library Evolution
-
@frozen:向编译器承诺 struct/enum 的布局不会变化- 编译器可以直接根据偏移量访问成员(更快)
- 不加
@frozen时,编译器通过间接方式访问(支持未来布局变化)
-
@inlinable:向编译器暴露函数体,允许跨模块内联 - 这些是标准库和系统框架使用的属性
5.3 Module Stability
- Swift 5.1+ 模块稳定:
.swiftinterface文件替代.swiftmodule - 不同编译器版本编译的模块可以互相兼容
6. 类型转换的底层机制
6.1 as / as? / as! 的区别
| 操作 | 检查时机 | 失败行为 | 底层 |
|---|---|---|---|
as |
编译期 | 编译错误 | 无运行时开销(类型已知) |
as? |
运行时 | 返回 nil | metadata 比较 |
as! |
运行时 | trap 崩溃 | metadata 比较 + 强制 |
6.2 is 检查的底层
if value is MyClass { ... }
- 值类型:编译期确定(静态检查)
- 引用类型:运行时检查 isa 指针链(遍历继承链)
- 协议类型:检查 type metadata 中的 protocol conformance 表
6.3 Protocol Conformance 查找
- Swift runtime 维护一个全局的 Protocol Conformance Table
- 表项格式:
(TypeDescriptor, ProtocolDescriptor) → WitnessTable -
as? SomeProtocol时,runtime 在表中查找当前类型是否遵循该协议 - 查找结果会被缓存
7. Swift 与 Objective-C 互操作底层
7.1 桥接机制
| Swift 类型 | ObjC 类型 | 桥接方式 |
|---|---|---|
| String | NSString | 按需转换(UTF-8 ↔ UTF-16) |
| Array | NSArray | 包装/拆包 |
| Dictionary | NSDictionary | 包装/拆包 |
| Int/Double | NSNumber | 装箱/拆箱 |
| struct | 不可桥接 | 需要手动封装为 class |
7.2 @objc 的代价
- 标记为
@objc的方法会生成 ObjC 兼容的调用入口 - Swift class 继承 NSObject 时,会注册到 ObjC runtime
- ObjC 方法调用走
objc_msgSend,比 Swift vtable 慢 3-5 倍 - 每个
@objc方法增加约 100 字节的二进制体积
7.3 Dynamic Member Lookup
@dynamicMemberLookup
struct JSON {
subscript(dynamicMember member: String) -> JSON { ... }
}
let value = json.user.name // 编译器转换为 subscript 调用
- 编译期将
.member语法转为 subscript 调用 - 不涉及 ObjC runtime,纯 Swift 实现
- 用于 DSL、动态语言桥接等
8. Move Semantics 与 Ownership(Swift 5.9+)
8.1 consuming / borrowing 参数
func process(_ value: consuming MyStruct) {
// value 的所有权被转移到此函数,调用方不能再使用
}
func inspect(_ value: borrowing MyStruct) {
// 只读借用,不拷贝,不转移所有权
}
8.2 ~Copyable(不可拷贝类型)
struct FileHandle: ~Copyable {
let fd: Int32
deinit { close(fd) } // struct 有了 deinit!
}
- 不可拷贝类型保证唯一所有权
- 赋值 = 移动(原变量失效)
- 可以为 struct 添加
deinit(资源清理) - 类似 Rust 的 ownership 模型
- 消除不必要的引用计数开销
8.3 意义
- 零成本抽象的资源管理(RAII)
- 编译器保证资源不会被重复释放或遗忘释放
- 为 Swift 引入更精细的内存控制能力
9. Result Type 与 Error Handling 底层
9.1 throws 的底层实现
Swift 的 throws 不使用异常表(不同于 C++/Java):
// 函数签名实际上是:
func foo() throws -> Int
// 底层等价于:
func foo() -> (Int, Error?)
- 通过隐藏的返回值寄存器传递 Error
- 正常路径零开销(no error → 直接返回结果)
- 错误路径有 Error 对象创建的开销
- 这就是为什么
try的正常路径性能很好
9.2 typed throws (Swift 5.9+)
func parse() throws(ParseError) -> AST { ... }
- 限定了错误类型,避免 existential Error 的开销
- 调用方可以直接 catch 具体类型,无需
as?转换
10. @dynamicCallable 与语言扩展能力
@dynamicCallable
struct PythonObject {
func dynamicallyCall(withArguments args: [Any]) -> PythonObject { ... }
func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, Any>) -> PythonObject { ... }
}
let result = pythonObj(1, 2, name: "test") // 编译器转为 dynamicallyCall
- 编译器将函数调用语法重写为
dynamicallyCall方法调用 - 用于与 Python/Ruby 等动态语言桥接
- TensorFlow for Swift 等项目大量使用
11. Opaque Return Type 与 Reverse Generics
11.1 some 的底层
func makeShape() -> some Shape {
Circle()
}
- 编译器知道返回类型是
Circle,但对调用方隐藏 - 不使用 existential container,直接返回 Circle 的值
- 零额外开销(等同于直接返回 Circle)
- 但保持了 API 的抽象性(future-proof)
11.2 与 existential 的根本差异
// some:编译期确定类型,运行时无开销
func a() -> some Collection { [1,2,3] }
// a() 和 a() 保证是同一类型(Int Array)
// any:运行时动态类型,有 container 开销
func b() -> any Collection { Bool.random() ? [1] : Set([1]) }
// b() 每次可能不同类型
12. Memory Layout 工具
MemoryLayout<Int>.size // 8(实际占用字节)
MemoryLayout<Int>.stride // 8(数组中相邻元素的间距)
MemoryLayout<Int>.alignment // 8(对齐要求)
MemoryLayout<Bool>.size // 1
MemoryLayout<Bool>.stride // 1
MemoryLayout<Bool>.alignment // 1
MemoryLayout<Optional<Int>>.size // 9(8 + 1 tag)
MemoryLayout<Optional<Int>>.stride // 16(对齐到 8 的倍数)
MemoryLayout<String>.size // 16(SSO 结构)
MemoryLayout<String>.stride // 16
这些在需要与 C 交互、手动管理内存、优化内存布局时非常关键。
附录:高频面试题速查
| # | 问题 | 核心关键词 |
|---|---|---|
| 1 | struct 和 class 的区别? | 值/引用、栈/堆、COW、ARC、继承 |
| 2 | Swift 的方法派发有几种? | 静态、vtable、PWT、objc_msgSend |
| 3 | ARC 和 GC 的区别? | 编译期插入 vs 运行时扫描、确定性 vs 非确定性、无停顿 vs STW |
| 4 | weak 和 unowned 的区别? | Optional/非Optional、side table、释放后行为 |
| 5 | 什么是 Existential Container? | 5 words、value buffer、metadata、PWT |
| 6 | 什么是 COW? | isKnownUniquelyReferenced、延迟拷贝 |
| 7 | 泛型约束和存在类型的区别? | 静态/动态派发、特化、性能差异 |
| 8 | 闭包是值类型还是引用类型? | 引用类型、函数指针+context、堆分配 |
| 9 | Swift 的 String 为什么不能用 Int 下标? | 变长 UTF-8、扩展字形簇、O(n) 遍历 |
| 10 | Optional 底层是什么? | 枚举 .none/.some、spare bit 优化 |
| 11 | some 和 any 的区别? | opaque type vs existential、静态/动态、性能 |
| 12 | Actor 怎么保证线程安全? | 串行执行器、isolation、await |
| 13 | async/await 底层原理? | 协程、状态机、continuation、不阻塞线程 |
| 14 | throws 的性能开销? | 正常路径零开销、隐藏返回寄存器 |
| 15 | @frozen 和 @inlinable 的作用? | ABI 稳定、跨模块优化、库演进 |
| 16 | 什么是 WMO? | 全模块优化、去虚拟化、跨文件内联 |
| 17 | ~Copyable 是什么? | 不可拷贝类型、唯一所有权、move semantics |
| 18 | 协议扩展方法为什么不能多态? | PWT 无条目、静态派发 |
| 19 | class extension 的方法能 override 吗? | 不能、不在 vtable 中、静态派发 |
| 20 | 排他性访问是什么? | Law of Exclusivity、begin/end_access、读写冲突检测 |