iOS / Swift / Flutter 面试宝典(详尽版)
涵盖 OC、Swift、Flutter 三大部分,深入底层源码级别原理,全面覆盖行业内所有面试题型
每一个答案都力求做到:知其然,更知其所以然
本文档为 Swift 独立分册,包含 18 道面试题,涵盖值类型/CoW、Optional、Protocol、泛型、枚举、闭包、并发/Actor/Sendable、SwiftUI、Combine、Codable、混编、访问控制、PropertyWrapper、初始化、KeyPath、String/集合、内存安全等全模块。
第二部分:Swift
1. 值类型与引用类型
Q30: Swift 中 Struct 和 Class 的 15 项核心差异对照?
答案:
| # | 维度 | Struct | Class |
|---|---|---|---|
| 1 | 类型 | 值类型 | 引用类型 |
| 2 | 存储 | 栈(一般) | 堆 |
| 3 | 继承 | ❌ | ✅ 单继承 |
| 4 | deinit | ❌ | ✅ |
| 5 | 引用计数 | ❌ | ✅ ARC 管理 |
| 6 | 线程安全 | 天然(拷贝传递) | 需要同步 |
| 7 | 变量性 | let 后全不可变 | let 后对象内部可改 |
| 8 | mutating | 需要 | 不需要 |
| 9 | Equatable/Hashable | 自动生成(条件满足时) | 手动实现 |
| 10 | 构造函数 | 自动获得 memberwise init | 需手动实现 |
| 11 | OBJC 互操作 | ❌ | ✅ (需继承 NSObject) |
| 12 | 单例 | 不适合 | ✅ |
| 13 | 内存位置 | 栈(多数) | 堆 |
| 14 | 写时复制 | 标准库集合支持 | N/A |
| 15 | SwiftUI View | ✅ (推荐) | ❌ (View 必须 struct) |
Q31: Copy-on-Write 的完整实现及优化策略?
答案:
Swift 标准库中的 CoW:
Array, Dictionary, Set, String 都使用 CoW
当进行 var arr2 = arr1 时:
- 仅增加引用计数(不拷贝底层缓冲区)
- 当任一变量进行修改时:检查引用计数
- 引用计数 = 1 → 直接修改(唯一所有者)
- 引用计数 > 1 → 拷贝缓冲区,然后修改副本
验证 CoW 行为:
var arr1 = [1, 2, 3]
var arr2 = arr1
// 检查:arr1 和 arr2 共享同一内存
arr1.withUnsafeBufferPointer { p1 in
arr2.withUnsafeBufferPointer { p2 in
print(p1.baseAddress == p2.baseAddress) // true — 共享内存
}
}
arr2.append(4) // 触发 CoW → arr2 获得独立内存
arr1.withUnsafeBufferPointer { p1 in
arr2.withUnsafeBufferPointer { p2 in
print(p1.baseAddress == p2.baseAddress) // false — 已分离
}
}
自定义类型的 CoW 实现:
// 模式:外层是 struct(值语义),内部借用 class(引用共享存储)
final class Ref<T> {
var val: T
init(_ v: T) { self.val = v }
}
struct CowBox<T> {
private var ref: Ref<T>
init(_ value: T) {
ref = Ref(value)
}
var value: T {
get { ref.val }
set {
// 关键:检查是否唯一引用
if !isKnownUniquelyReferenced(&ref) {
// 有其他使用者 → 必须先拷贝
ref = Ref(newValue)
} else {
// 唯一引用 → 可以直接修改
ref.val = newValue
}
}
}
}
isKnownUniquelyReferenced 的原理:
- 检查引用对象的强引用计数是否为 1
- 如果 = 1 → 当前是唯一持有者 → 可以原地修改
- 如果 > 1 → 其他人也持有 → 必须拷贝
- ⚠️ 对于 ObjC 对象(NSObject 子类),需用
isKnownUniquelyReferenced(&ref)+ 确保引用是强引用
2. Optional 可选类型
Q32: Optional 的底层 enum 实现?if let / guard let / map / flatMap / ?? 的完整解析?
答案:
Optional 本质就是一个泛型枚举:
enum Optional<Wrapped>: ExpressibleByNilLiteral {
case none
case some(Wrapped)
init(nilLiteral: ()) { self = .none }
}
// 所以:
// let name: String? = "John" → .some("John")
// let name: String? = nil → .none
// nil 不是空指针!是 Optional.none 这个枚举值
所有解包方式及使用场景:
let opt: Int? = 42
// 1. if let — 条件解包(需在块内使用)
if let value = opt {
print(value) // value 仅在 if 块内有效
}
// 2. guard let — 提前退出(guard 之后可继续使用)
func process(_ opt: Int?) {
guard let value = opt else { return }
print(value) // value 在方法剩余部分有效
// 避免"金字塔嵌套"
}
// 3. switch — 枚举匹配(处理多层 Optional)
switch opt {
case .none: print("nil")
case .some(let v): print(v)
}
// 4. map — 对 Optional 内部值做变换(不展开 Optional)
let doubled = opt.map { $0 * 2 } // Int? = 84
let nilMapped: Int? = nil
nilMapped.map { $0 * 2 } // nil ← 不执行闭包
// 5. flatMap — 变换返回 Optional(自动展平)
func toStr(_ n: Int) -> String? { n > 0 ? "\(n)" : nil }
opt.flatMap(toStr) // String? = "42"
// 6. compactMap — 数组版 flatMap(过滤 nil)
["1", "abc", "3"].compactMap(Int.init) // [1, 3]
// 7. ?? 空合运算符
let result = opt ?? 0 // 42(opt 非 nil)
// 底层实现:
// func ??(optional: T?, defaultValue: @autoclosure () -> T) -> T
// @autoclosure 保证仅在 optional == nil 时计算默认值(惰性求值)
// 8. 可选链 Optional Chaining
let count = user?.address?.city?.count // Int?
// 任何中间环节为 nil → 整个链返回 nil
// 底层:编译为嵌套的 flatMap 调用
可选链的编译转换:
// 源码
let city = user?.address?.city
// 编译器转换为
let city = user.flatMap { $0.address }.flatMap { $0.city }
// 或等价于
let city: String?
if let u = user, let a = u.address {
city = a.city
} else {
city = nil
}
多层 Optional(面试陷阱):
let a: Int?? = 1 // Optional(.some(Optional(.some(1))))
let b: Int?? = nil // Optional(.none)
let c: Int?? = .some(nil) // Optional(.some(Optional(.none)))
print(a == b) // false — .some(1) ≠ .none
print(b == c) // false — .none ≠ .some(nil)
// nil 在多层 Optional 中语义不同!
3. Protocol 与面向协议编程
Q33: Protocol Extension 中的方法分派完整规则表?
答案:
这是 Swift 面试最高频陷阱题,必须彻底掌握。
protocol Drawable {
func draw() // 协议声明
}
extension Drawable {
func draw() { print("Default draw") } // 默认实现 → WT
func debugDraw() { print("Debug") } // 仅 Extension → 静态
}
struct Circle: Drawable {
func draw() { print("Circle draw") } // 覆盖 → WT
func debugDraw() { print("Circle debug") } // 覆盖 → 静态
}
let a: Circle = Circle()
a.draw() // "Circle draw" — Circle 实现覆盖了默认,WT 分派
a.debugDraw() // "Circle debug" — 静态分派,具体类型决定
let b: Drawable = Circle()
b.draw() // "Circle draw" — 协议声明 → WT → 找到 Circle 的实现
b.debugDraw() // "Debug" ⚠️⚠️⚠️ — 静态分派!协议类型→调了 Extension 的默认实现!
分派规则总结:
| 方法声明位置 | 类型声明方式 | 分派机制 | 能否被覆盖 |
|---|---|---|---|
| 协议中声明 | protocol P { func foo() } |
Witness Table | ✅ struct/class 可实现 |
| 仅 Extension | extension P { func bar() } |
静态分派 | ❌ 无法被协议类型调用 |
| 协议声明 + Extension 默认实现 | 两者都有 | Witness Table(优先查 WT) | ✅ struct/class 可覆盖 |
面向协议编程 (POP) vs OOP 的核心优势:
// OOP 问题:单继承限制
class Animal { func eat() {} }
class Flyable: Animal { func fly() {} }
// class Bird: Flyable, Swimmable {} // ❌ 多重继承不允许
// POP 方案:协议组合
protocol Eatable { func eat() }
protocol Flyable { func fly() }
protocol Swimmable { func swim() }
struct Duck: Eatable, Flyable, Swimmable { } // ✅ 任意组合
4. 泛型 Generics
Q34: Swift 泛型进阶 — 类型擦除 / Opaque Type / where 子句 / 泛型协议?
答案:
类型擦除 (Type Erasure) — 解决带 associatedtype 协议不能做类型的问题:
// 问题:带 associatedtype 的协议不能用作类型
protocol Repository {
associatedtype Item
func fetch(id: String) -> Item
}
// let repo: Repository // ❌ 编译错误!Protocol 'Repository' can only be used as a generic constraint
// 解决1:AnyRepository 类型擦除
struct AnyRepository<Item>: Repository {
private let _fetch: (String) -> Item
init<R: Repository>(_ repo: R) where R.Item == Item {
_fetch = repo.fetch
}
func fetch(id: String) -> Item { _fetch(id) }
}
// let repos: [AnyRepository<User>] // ✅ 可以了
// 解决2:Swift 5.7 的 any 关键字
// let repo: any Repository<User> // Swift 5.7+
Opaque Type (some) vs Existential (any):
// some — 不透明类型:返回一个固定的、确定的类型,但对外隐藏类型名
func makeView() -> some View {
Text("Hello") // 实际返回 Text,但调用者只知道是"某种 View"
}
// 编译器知道具体类型 → 静态分派 → 高性能
// 约束:所有代码路径必须返回同一具体类型
// any — 存在类型:返回任意遵守协议的类型
func makeView(flag: Bool) -> any View {
flag ? Text("Hello") : Image("icon") // 可以返回不同类型
}
// 编译器只知道是协议类型 → 存在容器 + 动态分派 → 有开销
// 可在函数体内返回不同类型
where 子句的进阶用法:
// 1. 约束泛型参数
func printArray<T>(_ arr: [T]) where T: CustomStringConvertible {
for item in arr { print(item.description) }
}
// 2. 约束 associatedtype
protocol Container {
associatedtype Item
var items: [Item] { get }
}
extension Container where Item: Equatable {
func contains(_ item: Item) -> Bool {
items.contains(item)
}
}
// 3. 约束协议扩展
extension Array where Element: Numeric {
func sum() -> Element { reduce(0, +) }
}
[1, 2, 3].sum() // 6
// ["a", "b"].sum() // ❌ String 不是 Numeric
// 4. 多个约束组合
func process<T>(_ item: T) where T: Codable & Hashable & Identifiable {
// 同时满足三个协议
}
5. 枚举 Enum 进阶
Q35: Swift 枚举的 Associated Value / Raw Value / indirect / 模式匹配完整解析?
答案:
// 1. Raw Value(原始值)— 编译时确定,所有 case 同类型
enum Planet: Int {
case mercury = 1, venus, earth // earth 自动 = 3
}
enum Direction: String {
case north = "N", south = "S" // 不设置时 = case 名
}
// 2. Associated Value(关联值)— 每个 case 可携带不同类型数据
enum APIResponse {
case success(data: Data)
case failure(error: Error, code: Int)
case loading(progress: Double)
}
let resp = APIResponse.failure(error: MyError.timeout, code: 408)
// 3. indirect — 递归枚举(如链表/表达式树)
indirect enum LinkedList<T> {
case empty
case node(value: T, next: LinkedList<T>)
}
// 或单 case 标记
enum Expression {
case number(Int)
indirect case add(Expression, Expression)
}
// 4. 模式匹配(5种方式)
let resp: APIResponse = .success(data: Data())
// 方式a:switch + let
switch resp {
case .success(let data): print("data: \(data.count)")
case .failure(let err, let code): print("\(code): \(err)")
case .loading(let progress): print("\(progress)%")
}
// 方式b:if case(单 case 匹配)
if case .success(let data) = resp { use(data) }
// 方式c:guard case
guard case .success(let data) = resp else { return }
// 方式d:for case(循环过滤)
let cases: [APIResponse] = [...]
for case .failure(let err, _) in cases { print(err) }
// 方式e:case let 语法糖
if case let .success(data) = resp { } // let 前置
// 5. Result 类型 — 本质就是 Enum
enum Result<Success, Failure: Error> {
case success(Success)
case failure(Failure)
}
6. 闭包 Closures 深入
Q36: @escaping / @autoclosure / 捕获列表的完整底层机制?
答案:
@escaping vs 非逃逸闭包:
// 非逃逸闭包(默认)— 函数返回前执行完毕
func perform(closure: () -> Void) {
closure() // 函数返回前执行 → 非逃逸
}
// 编译器优化:
// - 不需要堆分配(在栈上即可)
// - 内部不用写 self.(编译器知道没有循环引用风险)
// 逃逸闭包 — 函数返回后才执行
var storedClosures: [() -> Void] = []
func performEscaping(closure: @escaping () -> Void) {
storedClosures.append(closure) // 函数返回后 closure 才被调用
}
// 开销:
// - 必须在堆上分配(closure 需要超出函数作用域)
// - 内部必须显式写 self.(提醒循环引用风险)
// 典型逃逸场景:
// - 异步回调 (DispatchQueue.async)
// - 存储闭包
// - 返回闭包
@autoclosure — 自动包装为闭包(惰性求值):
// 不用 @autoclosure 时
func assert(_ condition: () -> Bool, _ message: String) {
if !condition() { print(message) }
}
assert({ 2 + 2 == 5 }, "Math error") // 啰嗦
// 使用 @autoclosure
func assert(_ condition: @autoclosure () -> Bool, _ message: String) {
if !condition() { print(message) }
}
assert(2 + 2 == 5, "Math error") // 简洁
// 2 + 2 == 5 被自动包装成闭包 → 仅在需要时才计算
// ?? 运算符就是依赖 @autoclosure 实现:
func ??<T>(optional: T?, defaultValue: @autoclosure () throws -> T) rethrows -> T
// defaultValue 只在 optional == nil 时才计算
let v = expensiveComputation() ?? defaultValue // defaultValue 惰性求值
闭包捕获列表面试深入:
class MyClass {
var name = "hello"
func setup() {
// 1. 默认:隐式强捕获 self
network.fetch { // 编译错误!需显式 self
print(self.name) // ⚠️ 循环引用风险
}
// 2. [weak self] — 弱捕获
network.fetch { [weak self] in
guard let self = self else { return }
print(self.name)
}
// 3. [unowned self] — 无主捕获(确定 self 存活时)
network.fetch { [unowned self] in
print(self.name) // self 已释放 → crash
}
// 4. 捕获多个变量
network.fetch { [weak self, unowned cache] in
self?.useCache(cache)
}
// 5. 值类型捕获 — 拷贝当前值
var count = 0
network.fetch { [count] in // 捕获当前 count 的拷贝
print(count) // 闭包内的 count 是独立的副本
}
count = 10 // 不影响闭包内的 count
}
}
7. 并发编程深入
Q37: Sendable / TaskGroup / AsyncSequence / 死锁预防 完整解析?
答案:
Sendable — 线程安全的数据类型标记:
// Sendable 标记可安全跨 Actor/并发域传递的类型
// 编译器自动推导的 Sendable 类型:
// - 值类型(所有属性都是 Sendable)
// - Actor 类型
// - 不可变 class(所有属性都是 let + Sendable)
struct User: Sendable { // ✅ 自动 Sendable
let id: Int // Sendable
let name: String // Sendable
}
// 手动 @unchecked Sendable — 你保证线程安全
final class Cache: @unchecked Sendable {
private let lock = NSLock()
private var dict: [String: Data] = [:]
func get(_ key: String) -> Data? {
lock.withLock { dict[key] }
}
}
// ⚠️ 非 Sendable 类型跨 Actor 传递 → 编译警告
var unsafe = NonSendableObj()
Task {
await actor.use(unsafe) // ⚠️ Sending 'unsafe' risks causing data races
}
TaskGroup — 并行执行多个子任务:
// 场景:并行下载多个图片
func downloadImages(urls: [URL]) async throws -> [UIImage] {
try await withThrowingTaskGroup(of: UIImage.self) { group in
for url in urls {
group.addTask {
let data = try await URLSession.shared.data(from: url).0
return UIImage(data: data)!
}
}
// 收集结果(按完成顺序,不是添加顺序)
var images: [UIImage] = []
for try await image in group {
images.append(image)
}
return images
}
}
// TaskGroup 关键特性:
// 1. 子任务并发执行
// 2. 所有子任务完成后 group 才返回
// 3. 任一子任务 throw → group 取消所有剩余子任务
// 4. 子任务可使用父 Task 的优先级
AsyncStream / AsyncSequence — 异步序列:
// 创建自定义 AsyncStream(替代 EventChannel 回调模式)
let locations = AsyncStream<CLLocation> { continuation in
let manager = CLLocationManager()
let delegate = LocationDelegate { location in
continuation.yield(location)
}
manager.delegate = delegate
manager.startUpdatingLocation()
continuation.onTermination = { @Sendable _ in
manager.stopUpdatingLocation()
}
}
// 消费 AsyncStream
for await location in locations {
print("Lat: \(location.coordinate.latitude)")
// 循环自动响应取消
}
// AsyncAlgorithms (Apple 开源) — 组合 AsyncSequence
import AsyncAlgorithms
for await (locA, locB) in zip(locationsA, locationsB) {
// 两个源数据配对
}
结构化并发 vs 非结构化并发:
// 结构化并发 — Task 嵌套,自动继承上下文、取消传播
await withTaskGroup { group in
group.addTask { await work() } // 默认继承 actor 隔离 + 优先级
}
// 非结构化并发 — 独立 Task
Task {
await work() // 继承当前 actor 上下文
}
Task.detached(priority: .background) {
await work() // 不继承任何上下文
}
// 取消传播:
let task = Task { await longWork() }
task.cancel() // 取消 task → 子 task 也会被取消(如果是结构化并发)
答案:
// Actor 的核心思想:将"互斥访问"内建到类型系统中
// 编译器保证:在任一时刻,最多只有一个任务在访问 actor 的状态
actor Counter {
private var value = 0
func increment() {
value += 1 // 在 actor 内部直接访问(不需要锁)
}
func getValue() -> Int {
return value
}
}
// 访问 actor:
let counter = Counter()
await counter.increment() // 必须 await — 可能需等待其他任务完成
let v = await counter.getValue() // 同样必须 await
Actor 与锁的关键区别:
| Actor | 互斥锁 | |
|---|---|---|
| 阻塞方式 | 不阻塞线程(挂起当前任务) | 阻塞线程(线程休眠) |
| 效率 | 线程可去执行其他任务 | 线程被占用,浪费资源 |
| 编译检查 | ✅ 不通过 await 访问 → 编译错误 | ❌ 忘记加锁 → 运行时问题 |
| 可重入性 | 天然支持(await 时释放隔离) | 递归需要递归锁 |
| 死锁 | 几乎不会(await 不阻塞线程) | 很容易(循环等待) |
Actor 的可重入性(重要但常被忽略):
actor ImageDownloader {
private var cache: [URL: Data] = [:]
func download(_ url: URL) async throws -> Data {
if let cached = cache[url] {
return cached
}
let data = try await URLSession.shared.data(from: url).0
cache[url] = data // ⚠️ 这里可能有数据竞争!
return data
}
}
// 问题:如果两个任务同时调用 download(sameURL)
// Task A: 检查 cache → 未命中 → await URLSession → 释放 actor 隔离
// Task B: 检查 cache → 未命中 → await URLSession → 释放 actor 隔离
// Task A: 恢复 → cache[url] = dataA
// Task B: 恢复 → cache[url] = dataB (覆盖了 A 的数据)
// 虽然不会 crash,但重复下载了同一资源
// 解决:在 await 之后重新检查状态
func download(_ url: URL) async throws -> Data {
if let cached = cache[url] { return cached }
let data = try await URLSession.shared.data(from: url).0
if cache[url] == nil { // 重新检查
cache[url] = data
}
return cache[url]!
}
7. Property Wrapper / 8. SwiftUI 核心机制
Q34: SwiftUI 的 View 更新机制和 diff 算法原理?
答案:
SwiftUI View 的本质:
// SwiftUI 的 View 是"描述 UI 的蓝图",不是真正的 UI 对象
// 每次状态变化,整个 body 都会重新执行,产生新的 View 树
// SwiftUI 内部比较新旧 View 树,只更新实际变化的部分
struct ContentView: View {
@State private var counter = 0
var body: some View {
VStack {
Text("Count: \(counter)")
Button("+1") { counter += 1 } // 点击 → body 重新执行
}
}
}
更新流程:
1. @State 的 counter 改变
↓
2. SwiftUI 标记 ContentView 为"dirty"
↓
3. RunLoop 下一帧 → 重新执行 body
↓
4. 生成新的 View 树 (新 Struct 实例)
↓
5. AttributeGraph 比较新旧 View 树
├── 类型相同 + 属性相同 → 不更新(截断)
├── 类型相同 + 属性不同 → 更新属性(不重建 View)
└── 类型不同 → 重建 View
↓
6. 将差异应用到实际渲染树 (CALayer / UIView)
AttributeGraph 的依赖追踪:
SwiftUI 使用 AttributeGraph(内部框架)管理依赖关系。
每个 View 读取 @State/@ObservedObject 属性时,
AttributeGraph 记录"View X 依赖属性 Y"。
当属性 Y 变化时:
→ 只重新计算"依赖 Y"的 View
→ 不依赖 Y 的 View 不会重新计算(即使它们在同层级)
9. Combine 响应式编程
Q35: Combine 的核心组件有哪些?Publisher/Subscriber/Subject/Scheduler 完整原理?
答案:
Combine 的完整数据流模型:
Publisher ────→ Operator ────→ Operator ────→ Subscriber
(发布数据) (转换/过滤) (合并/分支) (接收数据)
│ │
└──────────────── Cancellable ─────────────────┘
(订阅关系管理器,取消时断开整个链)
六大核心组件详解:
1. Publisher(发布者)
// Publisher 协议
protocol Publisher {
associatedtype Output // 发布的数据类型
associatedtype Failure: Error // 错误类型
func receive<S: Subscriber>(subscriber: S)
where S.Input == Output, S.Failure == Failure
}
// 内置 Publisher
Just(42) // 发布一次,立即完成
Future { promise in promise(.success(data)) } // 异步单次发布
Empty() // 立即完成,不发布数据
Fail(error: ...) // 立即失败
[1,2,3].publisher // Sequence 转 Publisher
URLSession.shared.dataTaskPublisher(for: url) // 网络请求
NotificationCenter.default.publisher(for: .NSPersistentStoreRemoteChange)
Timer.publish(every: 1.0, on: .main, in: .common)
// @Published — 属性包装器自动生成 Publisher
class ViewModel {
@Published var searchText = ""
// $searchText 类型为 Published<String>.Publisher
}
2. Subscriber(订阅者)
// 两种最常用 Subscriber
// sink — 提供闭包处理值 + 完成
let cancellable = publisher.sink(
receiveCompletion: { completion in
switch completion {
case .finished: print("完成")
case .failure(let error): print("错误: \(error)")
}
},
receiveValue: { value in
print("收到: \(value)")
}
)
// assign — 直接赋值给对象的属性
publisher.assign(to: \.text, on: label)
// 注意:assign 要求 Failure == Never(不能有错误)
3. Operator(操作符)
// 变换操作
.map { $0 * 2 } // 值变换
.mapError { _ in MyError() }
.flatMap { id in api.fetchUser(id) } // Publisher 展平(并发)
.switchToLatest() // 切换到一个新 Publisher 时取消旧的
// 过滤操作
.filter { $0 > 0 }
.removeDuplicates() // 连续重复值去重
.first() / .last() // 只取第一个/最后一个
.dropFirst(3) // 丢弃前 N 个
// 时间操作 (需要 Scheduler)
.debounce(for: .milliseconds(300), scheduler: RunLoop.main) // 防抖
.throttle(for: .seconds(1), scheduler: RunLoop.main, latest: true) // 节流
.delay(for: .seconds(2), scheduler: DispatchQueue.main)
// 合并操作
.combineLatest(pubB) { a, b in "\(a)-\(b)" } // 任意 source 变化就发新值
.zip(pubA, pubB) // 等配对来齐再发
.merge(with: pubB) // 任一 source 发就转发
// 错误处理
.catch { error in Just(fallbackValue) } // 捕获错误 → 替换 Publisher
.retry(3) // 失败后重试 3 次
.replaceError(with: defaultValue) // 错误替换为默认值
4. Subject(既是 Publisher 又是 Subscriber)
// PassthroughSubject — 无状态转发
let subject = PassthroughSubject<Int, Never>()
subject.send(1) // 向所有订阅者发送 1
subject.send(2) // 向所有订阅者发送 2
subject.send(completion: .finished) // 完成(之后不能再发送)
// CurrentValueSubject — 有状态,保持当前值
let subject = CurrentValueSubject<Int, Never>(0)
print(subject.value) // 0(可以随时读取当前值)
subject.send(1)
print(subject.value) // 1
// 新订阅者立即收到当前值
// 关键区别:
// PassthroughSubject: 新订阅者只收到订阅之后发送的值
// CurrentValueSubject: 新订阅者立即收到当前最新值 + 后续新值
5. Cancellable & AnyCancellable
// 订阅返回 Cancellable,取消时释放资源
let cancellable = publisher.sink { ... }
// AnyCancellable — 类型擦除的 Cancellable
var cancellables = Set<AnyCancellable>()
publisher
.sink { ... }
.store(in: &cancellables) // 自动管理生命周期
// 当 Set 被释放(deinit)时,所有存储的 subscription 自动 cancel
6. Scheduler(调度器)
// 控制代码在哪个线程/队列执行
.receive(on: DispatchQueue.main) // 下游在 main 线程
.subscribe(on: DispatchQueue.global()) // 上游在后台线程
// receive(on:) — 影响之后的链
// subscribe(on:) — 影响 Publisher 创建时所在的线程
实战:搜索防抖完整示例:
class SearchViewModel: ObservableObject {
@Published var query = ""
@Published var results: [SearchResult] = []
@Published var isLoading = false
private var cancellables = Set<AnyCancellable>()
init() {
$query
.debounce(for: .milliseconds(300), scheduler: RunLoop.main) // 1. 防抖
.removeDuplicates() // 2. 去重
.filter { !$0.isEmpty } // 3. 过滤空
.map { [weak self] query -> AnyPublisher<[SearchResult], Never> in
self?.isLoading = true
return SearchAPI.search(query)
.replaceError(with: [])
.eraseToAnyPublisher()
}
.switchToLatest() // 4. 新搜索取消旧搜索的订阅
.receive(on: DispatchQueue.main)
.sink { [weak self] results in
self?.isLoading = false
self?.results = results
}
.store(in: &cancellables)
}
}
10. Codable 序列化进阶
Q36: Codable 的完整自定义编解码策略?
答案:
Codable = Encodable + Decodable
编译器自动生成的三个条件:
- 所有存储属性都遵守 Codable
- enum 自动生成(不需要 raw value)
- CodingKeys 自动生成(属性名 = JSON key)
CodingKeys 枚举自定义 key 映射:
struct User: Codable {
let id: Int
let userName: String
let avatarURL: URL
let createdAt: Date // 日期类型!
let role: UserRole
enum UserRole: String, Codable {
case admin, member, guest
}
enum CodingKeys: String, CodingKey {
case id
case userName = "user_name"
case avatarURL = "avatar_url"
case createdAt = "created_at"
case role
}
}
完全自定义编解码(处理复杂场景):
struct Product: Codable {
let id: String
let name: String
let price: Double
let tags: [String]
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
// 1. 处理类型转换:Int → String
if let intId = try? container.decode(Int.self, forKey: .id) {
id = String(intId)
} else {
id = try container.decode(String.self, forKey: .id)
}
name = try container.decode(String.self, forKey: .name)
// 2. 处理价格单位转换:分 → 元
let priceInCents = try container.decode(Int.self, forKey: .price)
price = Double(priceInCents) / 100.0
// 3. 处理 JSON null 或缺失
tags = try container.decodeIfPresent([String].self, forKey: .tags) ?? []
}
}
JSONEncoder/JSONDecoder 全局配置策略:
// 日期编码策略
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601 // 最常用
decoder.dateDecodingStrategy = .secondsSince1970
decoder.dateDecodingStrategy = .millisecondsSince1970
decoder.dateDecodingStrategy = .formatted(myDateFormatter)
decoder.dateDecodingStrategy = .custom { decoder in
let container = try decoder.singleValueContainer()
let dateStr = try container.decode(String.self)
// 自定义解析逻辑
return parseDate(dateStr)
}
// Key 编码策略
decoder.keyDecodingStrategy = .convertFromSnakeCase
// user_name → userName
encoder.keyEncodingStrategy = .convertToSnakeCase
// userName → user_name
// Data 编码策略
encoder.dataEncodingStrategy = .base64
// 处理 null
decoder.dateDecodingStrategy = .deferredToDate // 默认
11. Swift 与 OC 混编完整机制
Q37: Swift ↔ OC 互调的完整原理?什么代码不能互调?
答案:
Swift 调用 OC —— 桥接文件机制:
编译流程:
1. Xcode 读取 "项目名-Bridging-Header.h" (桥接文件)
2. 将其中 #import 的所有 OC 头文件暴露给 Swift
3. 编译器自动将 OC API 转换为 Swift 风格
4. Swift 代码可直接使用(无需额外 import)
Bridging-Header.h 示例:
#import "MyOCHelper.h"
#import "MBProgressHUD.h" // 第三方 OC 库
#import <AFNetworking/AFNetworking.h>
// Swift 文件中直接使用:
let helper = MyOCHelper() // 无需 import
MBProgressHUD.showAdded(to: view, animated: true)
OC 调用 Swift —— 自动生成头文件:
编译流程:
1. Xcode 自动生成 "项目名-Swift.h" (隐藏文件)
2. OC 文件 #import "项目名-Swift.h"
3. 其中包含所有标记为 @objc 的 Swift 符号
限制:
- Swift 类必须继承 NSObject 或标记 @objc
- Struct/Enum 不能被 OC 访问
- 泛型类不能被 OC 访问
- 带关联值的 enum 不能被 OC 访问
- Protocol 需标记 @objc
完整互调规则表:
| Swift 特性 | OC 可访问? | 条件 |
|---|---|---|
| class 继承 NSObject | ✅ | 自动 |
| class 不继承 NSObject + @objc | ✅ | 手动标记 |
| struct | ❌ | 永远不能 |
| enum (无关联值) | ✅ | @objc enum MyEnum: Int {} |
| enum (有关联值) | ❌ | 永远不能 |
| protocol (简单) | ✅ | @objc protocol MyProto {} |
| protocol (带 associatedtype) | ❌ | 永远不能 |
| extension | ✅ (方法) | 标记 @objc
|
| 泛型 | ❌ | 类型擦除 |
| Actor | ❌ | 永远不能 |
12. 访问控制与错误处理
Q38: open / public / internal / fileprivate / private 的完整对照?
答案:
| 修饰符 | 同一文件 | 同一模块 | 外部模块访问 | 外部模块继承 | 外部模块 override |
|---|---|---|---|---|---|
| private | ✅ 仅当前 scope | ❌ | ❌ | ❌ | ❌ |
| fileprivate | ✅ | ❌ | ❌ | ❌ | ❌ |
| internal (默认) | ✅ | ✅ | ❌ | ❌ | ❌ |
| public | ✅ | ✅ | ✅ | ❌ | ❌ |
| open | ✅ | ✅ | ✅ | ✅ | ✅ |
关键场景:
// public vs open 的本质区别(框架开发必知)
// 模块 A (Framework)
public class PublicClass { } // 其他模块可以创建实例,不能继承
open class OpenClass { } // 其他模块可以继承
public func publicFunc() { } // 可以调用
open func openFunc() { } // 可以 override
// 模块 B (App)
// let obj = PublicClass() // ✅ 可以使用
// class Sub: PublicClass { } // ❌ 编译错误
// class Sub: OpenClass { } // ✅ 可以继承
错误处理对比:
// try vs try? vs try!
func fetchData() throws -> Data { ... }
do {
let data = try fetchData() // 需要 do-catch 包裹
} catch MyError.notFound {
// 处理特定错误
} catch {
print(error) // 捕获所有
}
let data = try? fetchData() // 失败返回 nil(丢失错误信息)
let data = try! fetchData() // 失败 crash
// rethrows — 转发错误
func map<T>(_ transform: (Element) throws -> T) rethrows -> [T]
// 如果 transform 不抛错 → map 也不抛错
// 如果 transform 抛错 → map 向上抛
13. Swift 进阶特性
Q39: Property Wrapper 底层原理与实战?
答案:
// Property Wrapper 本质:一个包装了 get/set 逻辑的结构体
@propertyWrapper
struct Clamped<Value: Comparable> {
private var value: Value
private let range: ClosedRange<Value>
var wrappedValue: Value {
get { value }
set { value = min(max(newValue, range.lowerBound), range.upperBound) }
}
// projectedValue 通过 $ 前缀访问
var projectedValue: Clamped<Value> { self }
init(wrappedValue: Value, _ range: ClosedRange<Value>) {
self.range = range
self.value = min(max(wrappedValue, range.lowerBound), range.upperBound)
}
}
// 自动编译为:
// private var _age: Clamped<Int> = Clamped(wrappedValue: 0, 0...150)
// var age: Int {
// get { _age.wrappedValue }
// set { _age.wrappedValue = newValue }
// }
// var $age: Clamped<Int> { _age.projectedValue }
@Clamped(0...150) var age: Int = 0
age = 200; print(age) // 150
// 常见 Property Wrapper:
// @UserDefault — UserDefaults 包装
// @Lazy — 懒加载
// @Atomic — 线程安全
// @Published — Combine 发布
// @State / @Binding — SwiftUI 状态
14. 初始化系统
Q40: Swift 的 designated / convenience / required / failable init / 两阶段初始化 完整解析?
答案:
Designated vs Convenience init:
class Vehicle {
var speed: Double
// Designated init — 必须调用 super 的 designated init
init(speed: Double) {
self.speed = speed // 先初始化自己的属性
// 不需要 super.init()(没有父类时)
}
}
class Car: Vehicle {
var brand: String
var color: UIColor
// 子类的 Designated init
init(speed: Double, brand: String, color: UIColor) {
self.brand = brand // 1. 先初始化子类属性
self.color = color
super.init(speed: speed) // 2. 再调用 super 的 designated init
}
// Convenience init — 委托给本类的 designated init
convenience init(brand: String) {
self.init(speed: 0, brand: brand, color: .black) // 必须调用 self.init
}
// Required init — 子类必须实现
required init?(coder: NSCoder) {
guard let brand = coder.decodeObject(forKey: "brand") as? String else {
return nil // Failable init 可以返回 nil
}
self.brand = brand
self.color = .black
super.init(speed: 0)
}
}
两阶段初始化流程:
Phase 1(从子类到父类):
├── 给子类的所有 stored properties 赋值
├── 调用 super.init(...)
├── 给父类的所有 stored properties 赋值
└── 直到所有类的所有属性都有值
Phase 2(从父类到子类):
├── 父类可以使用 self 了
├── 调用方法、访问属性、做额外配置
└── 子类可以用 self 了
// 安全规则:
// Phase 1 中不能:
// - 调用任何实例方法(属性还没初始化完)
// - 读取属性(可能还没值)
// - 使用 self(除了赋值给自己的属性)
init? 和 init! (Failable init):
struct User {
let name: String
init?(dict: [String: Any]) { // 可能返回 nil
guard let name = dict["name"] as? String else {
return nil // ✅ 可以返回 nil
}
self.name = name
}
}
// init? 也可以 override init (子类 failable 可以 override 父类 non-failable)
15. KeyPath 与反射
Q41: Swift KeyPath 的底层原理?#keyPath / 动态成员查找 / Mirror 反射?
答案:
KeyPath 的三种类型:
struct Person {
var name: String
var address: Address
}
struct Address {
var city: String
}
// 1. KeyPath<Root, Value> — 只读
let namePath = \Person.name // KeyPath<Person, String>
// 2. WritableKeyPath<Root, Value> — 可读可写(Root 是 struct)
let nameWP = \Person.name // WritableKeyPath<Person, String>
// 3. ReferenceWritableKeyPath<Root, Value> — 可读写(Root 是 class)
class PersonClass { var name = "" }
let nameRWP = \PersonClass.name // ReferenceWritableKeyPath<PersonClass, String>
// 使用方式
var person = Person(name: "John", address: Address(city: "NYC"))
// 读取
let name = person[keyPath: \Person.name] // "John"
let city = person[keyPath: \Person.address.city] // "NYC" — 链式路径!
// 修改
person[keyPath: \Person.name] = "Jane"
KeyPath 的应用场景:
// 1. 替代闭包做排序/map
users.sorted(by: \.name) // 替代 { $0.name < $1.name }
users.map(\.name) // 替代 { $0.name }
// 2. Combine 的 assign
publisher.assign(to: \.text, on: label)
// 3. SwiftUI Binding
TextField("Name", text: $person.name) // $person.name 底层就是 WritableKeyPath
@dynamicMemberLookup — 动态属性访问:
@dynamicMemberLookup
struct JSON {
private var dict: [String: Any]
subscript(dynamicMember key: String) -> Any? {
return dict[key]
}
}
let json = JSON(dict: ["name": "John", "age": 30])
let name = json.name // 像属性一样访问!实际调用 subscript(dynamicMember:)
// ⚠️ 编译时不做检查,如果 key 不存在会返回 nil
Mirror 反射:
let mirror = Mirror(reflecting: person)
for child in mirror.children {
print("\(child.label ?? "?"): \(child.value)")
}
// name: John
// address: Address(city: "NYC")
// Mirror 适合调试/序列化框架,不适合业务逻辑(性能差,仅只读)
16. String 与集合内部
Q42: Swift String 为什么那么复杂?Array/Dict/Set 的内部存储策略?
答案:
Swift String 的设计哲学:
// Swift String 做了大量权衡:正确性 > 性能 > 简洁的 API
// 1. Unicode 正确处理
let flag = "🇨🇳" // 2个 Unicode 标量,1个字符
let family = "👨👩👧👦" // 多个标量 + ZWJ,1个字符
let é = "e\u{0301}" // e + 组合重音,1个字符
print(flag.count) // 1
print(family.count) // 1
print(é.count) // 1
// String.count 是 O(n)!必须扫描全部字节才知道字符数
// 2. 内部使用 UTF-8 存储(Swift 5+)
// 但提供多种视图:
let str = "Hello世界"
str.utf8 // UTF-8 视图
str.utf16 // UTF-16 视图(兼容 NSString)
str.unicodeScalars // Unicode Scalar 视图
// 3. 与 NSString 的桥接
let nsStr: NSString = "Hello"
let swiftStr: String = nsStr as String // 可能拷贝(性能开销)
// Swift 5 中 String 内部直接存储 UTF-8
// 与 NSString 桥接时可能触发编码转换
Substring — 与原 String 共享内存:
let str = "Hello, World!"
let idx = str.firstIndex(of: ",")!
let sub = str[..<idx] // "Hello" — Substring 类型
// Substring 与原始 String 共享底层 Buffer → O(1) 切片
// ⚠️ 陷阱:Substring 长期持有会阻止原始 String 释放
// 解决:显式转 String
let stable = String(sub) // 独立拷贝
Array 内部存储策略:
Array 使用 Copy-on-Write:
- 修改时:检查 isKnownUniquelyReferenced → 唯一引用则原地修改
- 非唯一引用 → 拷贝 buffer 再修改
- 扩容策略:count > capacity → capacity *= 2(接近)
ContiguousArray → 元素为 Class 或 @objc 时用此优化
ArraySlice → 与 Array 共享内存(类似 Substring)
Dictionary 内部:
Dictionary 使用开放寻址哈希表:
- 负载因子 > 75% → 扩容
- Key 必须 Hashable
- 顺序不保证(但同一进程内顺序稳定,依赖此是 bug)
- 与 NSDictionary 桥接时,Key 必须是 NSObject 子类
Set 内部:
Set = Dictionary<Element, Void> 几乎相同的实现
- 同样基于哈希表
- 支持交集(intersection)、并集(union)、差集(subtracting)
17. 内存安全与独占访问
Q43: Swift 的内存安全(Memory Safety)和独占访问(Exclusive Access)是什么?
答案:
Swift 的核心安全保证:
Swift 内存安全的三个层次:
1. 类型安全 → 编译时类型检查,不会出现类型混淆
2. 初始化安全 → 所有存储属性在使用前必须初始化
3. 独占访问 → 同一变量不能同时被两个上下文修改
独占访问的三个条件:
// 条件:对同一变量的两次访问不能同时满足:
// 1. 至少一次是写访问
// 2. 访问同一内存位置
// 3. 访问时间重叠
// ❌ 违反独占访问的例子
var stepSize = 1
func increment(_ number: inout Int) {
number += stepSize // 读 stepSize(读) + 写 number(写)
}
increment(&stepSize)
// 冲突:stepSize 被当作 inout 参数(写)+ 函数体内读 stepSize
// 编译器检测到 → 编译错误
// ✅ 解决:拷贝一份
var copy = stepSize
increment(©)
stepSize = copy
inout 参数的底层实现:
// inout 不是"传引用"!
// 实际是 copy-in copy-out(或 call by value result)
func swap<T>(_ a: inout T, _ b: inout T) {
let temp = a
a = b
b = temp
}
// 编译器行为:
// 1. 调用时:将原值拷贝到参数位置(copy-in)
// 2. 函数返回:将最终值拷回原变量(copy-out)
// ⚠️ 优化:编译器可能直接用指针(pass-by-reference 优化)
// 但语义上仍是 copy-in copy-out
// ⚠️ 两个 inout 参数不能指向同一内存
var val = 10
swap(&val, &val) // ❌ 编译错误!同一变量传给两个 inout
闭包中的变量捕获冲突:
var counter = 0
let closure = { counter += 1 } // 捕获 counter
counter += 1 // 同时访问 counter
// ❌ 如果这两者并发执行 → 数据竞争
// Swift 5.5+ 的 actor/async 体系帮你避免这种问题