Swift面试宝典(详尽版)

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

编译器自动生成的三个条件:

  1. 所有存储属性都遵守 Codable
  2. enum 自动生成(不需要 raw value)
  3. 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(&copy)
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 体系帮你避免这种问题


附录:高频代码手写题

Swift 手写题

1. Copy-on-Write 容器

2. 自定义 @propertyWrapper

3. async/await 版本的并发队列

4. 泛型 LRU 缓存

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • iOS / Swift / Flutter 面试宝典(详尽版) 涵盖 OC、Swift、Flutter 三大部分,...
    携YOU手同行阅读 16评论 0 0
  • 一、Swift 语言基础 问:值类型与引用类型区别?何时选 struct,何时选 class? 答:值类型拷贝语义...
    gc_jp阅读 387评论 0 1
  • Swift中有如此多的语法和功能术语,有时候容易感到困惑。因此,此文章将充当术语的一站式词典,为Swift语言中使...
    LV大树阅读 1,999评论 0 5
  • 这不是一份“背题答案集”,而是一份面向 iOS 高级面试的知识地图。目标是把零散、重复、年代不一的资料整理成一条递...
    gaookey阅读 712评论 0 1
  • 文章已发在快手大前端公众号,欢迎关注,文章地址如下: A站 的 Swift 实践 —— 上篇[https://mp...
    星光社的戴铭阅读 3,324评论 4 35

友情链接更多精彩内容