普通视图

发现新文章,点击刷新页面。
昨天 — 2026年1月29日掘金 iOS

聊聊swift闭包

作者 dongczlu
2026年1月29日 17:00

Swift闭包语法详解

代码示例

var b:() -> Void = {() -> Void in
    print("匿名函数")
}
b()

详细解释

这是一个Swift中的闭包(Closure)语法,具体解释如下:

代码分解

var b:() -> Void = {() -> Void in
    print("匿名函数")
}
b()

语法分析

  1. 变量声明

    • var b:声明一个变量 b
    • :() -> Void:指定变量的类型是一个函数类型,() -> Void 表示:
      • ():不接受任何参数
      • -> Void:返回 Void 类型(即不返回值,相当于空类型)
  2. 闭包赋值

    • = {() -> Void in:赋值给一个闭包表达式
      • {}:闭包的开始和结束
      • () -> Void:闭包的类型签名(与变量类型匹配)
      • in:关键字,表示参数列表和函数体的分隔符
  3. 闭包体

    • print("匿名函数"):闭包执行的代码
  4. 调用

    • b():调用这个闭包函数

等价写法

这个闭包还可以写成更简洁的形式:

完整写法(上面使用的)

var b: () -> Void = { () -> Void in
    print("匿名函数")
}

简化写法(省略类型,因为可以从上下文推断)

var b: () -> Void = {
    print("匿名函数")
}

尾随闭包写法(如果作为参数传递)

func executeClosure(closure: () -> Void) {
    closure()
}

executeClosure {
    print("匿名函数")
}

关键概念

  • 闭包:可以捕获和存储其所在上下文中变量和常量的引用
  • 匿名函数:没有名字的函数,可以直接赋值给变量或作为参数传递
  • 类型推断:Swift可以根据上下文自动推断闭包的类型

执行结果

执行这段代码会输出:匿名函数

闭包的特点

  1. 可以捕获上下文:闭包可以捕获和存储其所在作用域中的变量和常量
  2. 可以作为参数传递:可以作为函数的参数传递给其他函数
  3. 可以作为返回值:函数可以返回闭包作为返回值
  4. 有三种形式
    • 全局函数:有名字但不能捕获值
    • 嵌套函数:有名字且能捕获其封闭函数域内的值
    • 闭包表达式:无名字的轻量级语法,能捕获其上下文中变量或常量的值

命名函数 vs 闭包表达式的区别

示例代码对比

第一种写法:命名函数赋值
func test3(param:[Int]) -> String
{
    var temp:String = ""
    for item in param {
        temp = temp + String(item)
    }
    return temp
}
var e:([Int]) -> String = test3
print(e([2,3]))  // 输出:23
第二种写法:闭包表达式
var f:([Int]) -> String = {
    (a:[Int]) -> String in
    var temp:String = ""
    for item in a {
        temp = temp + String(item)
    }
    return temp
}
print(f([4,5,6]))  // 输出:456

核心区别

1. 定义方式
  • 命名函数:使用 func 关键字定义,有函数名,可以在其他地方重复调用
  • 闭包表达式:直接在赋值时定义,没有函数名,是一次性使用的匿名函数
2. 作用域和生命周期
  • 命名函数:有独立的生命周期,可以在定义后多次调用
  • 闭包表达式:赋值给变量后,通过变量名调用,变量的生命周期决定了闭包的生命周期
3. 内存占用
  • 命名函数:函数本身占用内存,但赋值给变量时只是引用传递
  • 闭包表达式:闭包本身和捕获的上下文都会占用内存
4. 使用场景
  • 命名函数:适用于需要重用的逻辑,函数体复杂,功能独立的情况
  • 闭包表达式:适用于简单的回调逻辑,作为参数传递,或需要捕获上下文的场景
5. 语法特点
  • 命名函数:有独立的函数签名,可以有默认参数等
  • 闭包表达式:语法更简洁,可以省略类型标注(类型推断)

实际应用场景

闭包在Swift开发中非常常用,比如:

  • 异步回调
  • 集合类型的操作(如map、filter、sort)
  • UI事件处理
  • 定时器回调
  • 网络请求回调

性能考虑

  • 命名函数:通常性能更好,因为没有额外的上下文捕获开销
  • 闭包表达式:如果捕获了大量上下文变量,可能产生循环引用或内存泄漏风险

最佳实践

  1. 使用命名函数当:

    • 函数逻辑复杂且需要重用
    • 函数需要有明确的名称便于调试
    • 不需要捕获外部上下文
  2. 使用闭包表达式当:

    • 需要简单的回调逻辑
    • 作为参数传递给其他函数
    • 需要捕获和修改外部变量
    • 用于内联定义,提高代码可读性

闭包作为函数参数的进阶用法

示例代码分析

1. 无参数闭包
func test(param:() -> Void)
{
    param()
}

// 调用方式:尾随闭包语法
test{
    print("test")
}

分析

  • test函数接受一个() -> Void类型的闭包参数
  • 使用尾随闭包语法调用:test{ print("test") }
  • 省略了param:标签,直接在大括号中定义闭包
2. 单参数闭包
func test2(param:(Int)-> Void)
{
    param(10)
}

// 完整写法
test2(param: {(value:Int) -> Void in
    print(value)
})

// 简化写法(尾随闭包)
test2{ (value) in
    print(value)
}

分析

  • test2函数接受(Int) -> Void类型的闭包
  • 完整写法:明确指定参数类型和返回类型
  • 简化写法:省略参数类型(类型推断),使用尾随闭包语法
3. 多参数有返回值闭包
func test3(param:(Int, Int) -> Int)
{
    print(param(10,30))
}

// 完整写法
test3(param:{(item1, item2) -> Int in
    return item1 + item2
})

// 极简写法
test3(param:{
    $0 + $1  // 使用$0、$1表示第一个和第二个参数
})

分析

  • test3函数接受(Int, Int) -> Int类型的闭包
  • 完整写法:明确参数名和返回类型
  • 极简写法:使用$0$1等简写参数名,省略returnin

记忆要点

闭包语法简化规则:
  1. 参数类型省略:如果能从上下文推断,可以省略参数类型
  2. 返回类型省略:单表达式闭包可以省略return
  3. 参数名简化:可以使用$0$1等代替参数名
  4. 圆括号省略:单参数时可以省略参数圆括号
尾随闭包语法规则:
  • 当闭包是函数的最后一个参数时,可以使用尾随闭包语法
  • 省略参数标签,直接在大括号中定义闭包
  • 使代码更简洁易读

实际运行结果

// test{ print("test") } 输出:test

// test2{ (value) in print(value) } 输出:10

// test3{ $0 + $1 } 传入(10,30) 输出:40

常见应用场景详解

1. 网络请求异步回调
// 模拟网络请求函数
func fetchUserData(userId: String, completion: @escaping (User?, Error?) -> Void) {
    DispatchQueue.global().async {
        // 模拟网络延迟
        Thread.sleep(forTimeInterval: 1.0)

        // 模拟成功获取用户数据
        let user = User(name: "张三", age: 25)
        DispatchQueue.main.async {
            completion(user, nil)
        }
    }
}

// 使用示例
fetchUserData(userId: "123") { user, error in
    if let user = user {
        print("获取用户成功:\(user.name), 年龄:\(user.age)")
        // 在主线程更新UI
        self.updateUI(with: user)
    } else if let error = error {
        print("获取用户失败:\(error.localizedDescription)")
    }
}

实际应用:iOS开发中所有网络请求几乎都使用闭包回调,如URLSession、Alamofire等。

2. 数组和集合操作
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

// map:转换每个元素
let doubled = numbers.map { $0 * 2 }  // [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

// filter:过滤元素
let evenNumbers = numbers.filter { $0 % 2 == 0 }  // [2, 4, 6, 8, 10]

// reduce:累积计算
let sum = numbers.reduce(0) { $0 + $1 }  // 55

// sort:排序
let sorted = numbers.sorted { $0 > $1 }  // [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]

// 链式调用
let result = numbers
    .filter { $0 > 5 }
    .map { $0 * $0 }
    .reduce(0, +)  // 平方和:36+49+64+81+100=330

实际应用:处理数据转换、过滤、统计等操作。

3. UI事件处理
import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        // UIButton点击事件
        let button = UIButton(type: .system)
        button.setTitle("点击我", for: .normal)
        button.addAction(UIAction { [weak self] _ in
            self?.handleButtonTap()
        }, for: .touchUpInside)

        // UITextField文本变化监听
        let textField = UITextField()
        textField.addAction(UIAction { _ in
            print("文本变化:\(textField.text ?? "")")
        }, for: .editingChanged)

        // 定时器
        Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
            print("定时器触发")
        }
    }

    func handleButtonTap() {
        print("按钮被点击了!")
        // 处理点击逻辑
    }
}

实际应用:所有UI控件的事件响应都使用闭包。

4. 动画和过渡效果
import UIKit

class AnimationViewController: UIViewController {

    let animatedView = UIView()

    override func viewDidLoad() {
        super.viewDidLoad()

        // UIView动画
        UIView.animate(withDuration: 0.5) {
            self.animatedView.alpha = 0.5
            self.animatedView.transform = CGAffineTransform(scaleX: 1.2, y: 1.2)
        } completion: { finished in
            if finished {
                print("动画完成")
                // 执行后续操作
            }
        }

        // UIViewPropertyAnimator
        let animator = UIViewPropertyAnimator(duration: 1.0, curve: .easeInOut) {
            self.animatedView.center = CGPoint(x: 200, y: 300)
        }

        animator.addCompletion { position in
            switch position {
            case .end:
                print("动画正常结束")
            case .current:
                print("动画被中断")
            case .start:
                print("动画开始")
            @unknown default:
                break
            }
        }

        animator.startAnimation()
    }
}

实际应用:所有iOS动画效果都依赖闭包。

5. 多线程和并发
import Foundation

class DataManager {

    // 异步数据处理
    func processDataAsync(data: [Int], completion: @escaping ([Int]) -> Void) {
        DispatchQueue.global(qos: .userInitiated).async {
            // 耗时操作
            let processedData = data.map { $0 * $0 }

            // 回到主线程回调
            DispatchQueue.main.async {
                completion(processedData)
            }
        }
    }

    // 并行处理
    func processInParallel(data: [Int], completion: @escaping ([Int]) -> Void) {
        let group = DispatchGroup()
        var results = [Int]()
        let lock = DispatchQueue(label: "com.example.lock")

        for item in data {
            group.enter()
            DispatchQueue.global().async {
                // 模拟耗时计算
                let result = item * item * item
                lock.sync {
                    results.append(result)
                }
                group.leave()
            }
        }

        group.notify(queue: .main) {
            completion(results.sorted())
        }
    }
}

// 使用示例
let manager = DataManager()
manager.processDataAsync(data: [1, 2, 3, 4, 5]) { results in
    print("处理结果:\(results)")  // [1, 4, 9, 16, 25]
}

实际应用:后台数据处理、文件操作等都需要多线程。

6. 通知中心
import Foundation

class NotificationManager {

    init() {
        // 监听键盘通知
        NotificationCenter.default.addObserver(
            forName: UIResponder.keyboardWillShowNotification,
            object: nil,
            queue: .main
        ) { notification in
            if let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
                print("键盘高度:\(keyboardFrame.height)")
                // 调整UI布局
            }
        }

        // 自定义通知
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(handleCustomNotification(_:)),
            name: NSNotification.Name("CustomNotification"),
            object: nil
        )
    }

    @objc func handleCustomNotification(_ notification: Notification) {
        if let userInfo = notification.userInfo {
            print("收到自定义通知:\(userInfo)")
        }
    }

    func postCustomNotification() {
        NotificationCenter.default.post(
            name: NSNotification.Name("CustomNotification"),
            object: self,
            userInfo: ["message": "Hello from notification!"]
        )
    }
}

实际应用:系统通知和应用内组件通信。

7. 错误处理
enum NetworkError: Error {
    case invalidURL
    case noData
    case parsingError
}

func fetchData(from urlString: String, completion: @escaping (Result<Data, NetworkError>) -> Void) {
    guard let url = URL(string: urlString) else {
        completion(.failure(.invalidURL))
        return
    }

    URLSession.shared.dataTask(with: url) { data, response, error in
        if let error = error {
            completion(.failure(.noData))
            return
        }

        guard let data = data else {
            completion(.failure(.noData))
            return
        }

        completion(.success(data))
    }.resume()
}

// 使用示例
fetchData(from: "https://api.example.com/data") { result in
    switch result {
    case .success(let data):
        print("获取数据成功,大小:\(data.count) bytes")
        // 处理数据
    case .failure(let error):
        switch error {
        case .invalidURL:
            print("无效的URL")
        case .noData:
            print("没有数据")
        case .parsingError:
            print("解析错误")
        }
    }
}

实际应用:网络请求、文件操作等可能出错的操作。

8. 依赖注入和配置
class NetworkService {

    typealias RequestCompletion = (Result<Data, Error>) -> Void

    private let session: URLSession
    private let baseURL: URL

    init(session: URLSession = .shared, baseURL: URL) {
        self.session = session
        self.baseURL = baseURL
    }

    func request(endpoint: String, completion: @escaping RequestCompletion) {
        let url = baseURL.appendingPathComponent(endpoint)
        session.dataTask(with: url) { data, _, error in
            if let error = error {
                completion(.failure(error))
            } else if let data = data {
                completion(.success(data))
            }
        }.resume()
    }
}

// 配置不同的环境
let developmentConfig = NetworkService(
    session: URLSession.shared,
    baseURL: URL(string: "https://dev-api.example.com")!
)

let productionConfig = NetworkService(
    session: URLSession.shared,
    baseURL: URL(string: "https://api.example.com")!
)

实际应用:框架配置、测试mock等。

9. 内存管理和循环引用
class ViewModel {

    var data: [String] = []
    var onDataUpdate: (() -> Void)?

    func loadData() {
        // 模拟异步数据加载
        DispatchQueue.global().async { [weak self] in  // 捕获弱引用
            let newData = ["Item 1", "Item 2", "Item 3"]

            DispatchQueue.main.async {
                self?.data = newData
                self?.onDataUpdate?()  // 调用闭包
            }
        }
    }

    deinit {
        print("ViewModel 被释放")
    }
}

class ViewController {

    let viewModel = ViewModel()

    init() {
        // 使用 [weak self] 避免循环引用
        viewModel.onDataUpdate = { [weak self] in
            self?.updateUI()
        }
    }

    func updateUI() {
        print("UI已更新")
    }

    deinit {
        print("ViewController 被释放")
    }
}

实际应用:MVC/MVVM架构中View和ViewModel的通信。

10. 函数式编程模式
// 函数组合
func compose<A, B, C>(_ f: @escaping (B) -> C, _ g: @escaping (A) -> B) -> (A) -> C {
    return { x in f(g(x)) }
}

// 柯里化
func curry<A, B, C>(_ f: @escaping (A, B) -> C) -> (A) -> (B) -> C {
    return { a in { b in f(a, b) } }
}

// 使用示例
let add = { (a: Int, b: Int) -> Int in a + b }
let curriedAdd = curry(add)
let add5 = curriedAdd(5)
let result = add5(3)  // 8

let double = { (x: Int) -> Int in x * 2 }
let square = { (x: Int) -> Int in x * x }
let doubleThenSquare = compose(square, double)
let result2 = doubleThenSquare(3)  // (3*2)^2 = 36

实际应用:函数式编程风格的代码组织。

练习建议

  1. 记忆口诀

    • "有尾随,无标签;可省略,推断强"
    • "参数多,用$0;单表达式,return省略"
  2. 常见错误避免

    • 闭包参数类型与函数期望不匹配
    • 忘记处理闭包中的错误
    • 循环引用导致的内存泄漏
  3. 调试技巧

    • 使用print语句调试闭包执行
    • 注意闭包捕获的变量作用域
    • 使用weak self避免循环引用

闭包逻辑实现的本质分析

核心概念:逻辑定义 vs 逻辑执行

1. 闭包定义了逻辑,函数调用了逻辑
// 函数定义:只负责调用闭包,不实现具体逻辑
func processData(data: [Int], operation: (Int) -> String) {
    for item in data {
        let result = operation(item)  // 这里调用闭包,执行逻辑
        print("处理结果:\(result)")
    }
}

// 闭包定义:实现具体逻辑
let doubleAndFormat = { (number: Int) -> String in
    let doubled = number * 2
    return "数字 \(number) 的两倍是 \(doubled)"
}

// 使用:传递闭包给函数
let numbers = [1, 2, 3, 4, 5]
processData(data: numbers, operation: doubleAndFormat)

输出结果

处理结果:数字 1 的两倍是 2
处理结果:数字 2 的两倍是 4
处理结果:数字 3 的两倍是 6
处理结果:数字 4 的两倍是 8
处理结果:数字 5 的两倍是 10
2. 分析执行流程
func downloadImage(url: String, completion: @escaping (UIImage?) -> Void) {
    // 第1步:函数开始执行,准备异步操作
    print("开始下载图片:\(url)")

    DispatchQueue.global().async {
        // 第2步:模拟网络请求(这里不实现具体逻辑)
        print("正在从网络获取图片...")
        Thread.sleep(forTimeInterval: 1.0)

        // 第3步:模拟获取图片成功
        let image = UIImage(systemName: "photo")

        // 第4步:回到主线程,调用闭包
        DispatchQueue.main.async {
            print("网络请求完成,调用completion闭包")
            completion(image)  // 执行闭包:UI更新逻辑在这里执行
        }
    }
}

// 闭包定义UI更新逻辑
let imageView = UIImageView()
downloadImage(url: "https://example.com/image.jpg") { downloadedImage in
    // 第5步:闭包执行,实现UI更新逻辑
    print("闭包执行:更新UI")
    imageView.image = downloadedImage
    imageView.setNeedsDisplay()
}

执行顺序分析

  1. downloadImage函数开始执行(网络请求准备)
  2. 异步执行网络请求(不包含UI逻辑)
  3. 网络请求完成,回到主线程
  4. 调用闭包completion(image) - 这是函数调用闭包的地方
  5. 闭包执行imageView.image = downloadedImage - 这是UI逻辑实现的地方

关键理解点

谁负责实现逻辑?
  1. 函数(调用者)

    • 定义接口和调用时机
    • 负责数据准备和传递
    • 决定何时调用闭包
  2. 闭包(被调用者)

    • 实现具体的业务逻辑
    • 处理传递的数据
    • 执行最终的操作
实际开发中的职责分离
class NetworkManager {
    // 函数:只负责网络请求,不负责数据处理
    func fetchUserData(completion: @escaping (Result<User, Error>) -> Void) {
        // 网络请求逻辑在这里实现
        URLSession.shared.dataTask(with: userURL) { data, _, error in
            // 数据处理逻辑在这里实现
            if let error = error {
                completion(.failure(error))
            } else if let data = data {
                // 解析逻辑在这里实现
                do {
                    let user = try JSONDecoder().decode(User.self, from: data)
                    completion(.success(user))
                } catch {
                    completion(.failure(error))
                }
            }
        }.resume()
    }
}

class UserViewController: UIViewController {
    private let networkManager = NetworkManager()

    func loadUser() {
        // UI逻辑在这里实现
        networkManager.fetchUserData { [weak self] result in
            switch result {
            case .success(let user):
                // UI更新逻辑在这里实现
                self?.updateUI(with: user)
            case .failure(let error):
                // 错误处理UI逻辑在这里实现
                self?.showError(error)
            }
        }
    }

    private func updateUI(with user: User) {
        nameLabel.text = user.name
        ageLabel.text = "\(user.age)岁"
        // 更多UI更新逻辑...
    }

    private func showError(_ error: Error) {
        let alert = UIAlertController(title: "错误", message: error.localizedDescription, preferredStyle: .alert)
        present(alert, animated: true)
    }
}

记忆总结

函数 vs 闭包的职责分工

方面 函数(调用者) 闭包(被调用者)
逻辑实现 框架逻辑、流程控制 具体业务逻辑、数据处理
执行时机 决定何时执行 被调用时执行
关注点 何时做、如何传递数据 做什么、如何处理数据
可复用性 固定流程,灵活回调 每次使用可以不同逻辑

经典比喻

  • 函数是"饭店厨师":负责准备食材和烹饪环境
  • 闭包是"顾客点的菜谱":告诉厨师具体做什么菜
  • 调用闭包是"上菜":厨师按照菜谱做出菜给顾客

代码层面的理解

// 函数定义了"做什么事"的框架
func doSomething(action: () -> Void) {
    print("准备做事")
    action()  // 具体"怎么做"由闭包决定
    print("事情完成")
}

// 闭包实现了"怎么做"的具体逻辑
doSomething {
    print("这是我要做的事情")  // 具体逻辑在这里
}

这样理解就能清楚:函数提供执行环境和时机,闭包提供具体的执行逻辑

Swift闭包 vs Objective-C Block 对比

面试常问的核心区别

1. 语言和语法差异

Swift闭包

// 基本语法
let closure: (Int, Int) -> Int = { (a, b) in
    return a + b
}

// 简化语法
let simplified = { $0 + $1 }

// 尾随闭包
func calculate(a: Int, b: Int, operation: (Int, Int) -> Int) -> Int {
    return operation(a, b)
}

let result = calculate(a: 10, b: 5) { $0 - $1 }

Objective-C Block

// 基本语法
int (^block)(int, int) = ^(int a, int b) {
    return a + b;
};

// 使用
int result = block(10, 5);

// 作为参数传递
- (void)calculateWithA:(int)a b:(int)b operation:(int(^)(int, int))operation {
    int result = operation(a, b);
    NSLog(@"Result: %d", result);
}

// 调用
[self calculateWithA:10 b:5 operation:^int(int x, int y) {
    return x * y;
}];
2. 类型系统和类型推断
特性 Swift闭包 Objective-C Block
类型标注 支持类型推断,大多可省略 需要明确类型标注
返回值 支持多返回值、元组 只能单返回值
可选类型 原生支持Optional 需要使用指针
泛型 强大泛型支持 有限泛型支持
3. 内存管理和循环引用

Swift闭包

class ViewController: UIViewController {
    var completionHandler: (() -> Void)?

    func setup() {
        // 避免循环引用
        completionHandler = { [weak self] in
            self?.updateUI()
        }

        // 或使用 unowned(当self一定存在时)
        completionHandler = { [unowned self] in
            self.updateUI()
        }
    }
}

Objective-C Block

@interface ViewController ()
@property (nonatomic, copy) void (^completionHandler)(void);
@end

@implementation ViewController

- (void)setup {
    // 避免循环引用
    __weak typeof(self) weakSelf = self;
    self.completionHandler = ^{
        [weakSelf updateUI];
    };

    // 或使用 __unsafe_unretained
    __unsafe_unretained typeof(self) unsafeSelf = self;
    self.completionHandler = ^{
        [unsafeSelf updateUI];
    };
}

@end
4. 变量捕获机制
特性 Swift闭包 Objective-C Block
值捕获 自动捕获,支持修改 自动捕获,默认const
引用捕获 inout关键字 __block修饰符
作用域 清晰的作用域规则 需要注意block作用域

Swift捕获示例

func createCounter() -> () -> Int {
    var count = 0
    return {
        count += 1  // 可以修改捕获的变量
        return count
    }
}

Objective-C捕获示例

int (^createCounter)(void) {
    __block int count = 0;  // 需要__block修饰符才能修改
    return ^int{
        count += 1;
        return count;
    };
}
5. 性能和优化
特性 Swift闭包 Objective-C Block
编译优化 LLVM优化,更好的内联 GCC优化,较少内联
运行时开销 较小(值类型优化) 较大(堆分配)
调试体验 更好的错误信息 较难调试
6. 使用场景和生态

Swift闭包优势场景

  • 现代iOS开发(iOS 8+)
  • 函数式编程
  • 协议和泛型结合
  • SwiftUI开发

Objective-C Block优势场景

  • 遗留代码库维护
  • 与C/C++交互
  • 底层系统编程

优缺点对比总结

Swift闭包的优点:
  1. 语法简洁:类型推断、尾随闭包、简化参数
  2. 类型安全:编译时类型检查,更少运行时错误
  3. 现代化特性:泛型、协议、Optional等
  4. 性能更好:优化程度更高
  5. 易学易用:学习曲线平缓
Swift闭包的缺点:
  1. 学习成本:需要掌握新语法
  2. 兼容性:无法直接与Objective-C代码交互
  3. 迁移成本:从Block迁移需要重写
Objective-C Block的优点:
  1. 成熟稳定:经过多年验证
  2. 兼容性好:与C/Objective-C完美集成
  3. 底层控制:更直接的内存控制
Objective-C Block的缺点:
  1. 语法复杂:类型标注繁琐
  2. 类型不安全:容易出现类型错误
  3. 调试困难:错误信息不清晰
  4. 现代性不足:缺少泛型等特性

面试回答要点

如果问到选择哪个

  • 新项目:推荐Swift闭包
  • 遗留项目:继续使用Block
  • 混合项目:视情况而定

核心理解

  • Swift闭包是Block的现代化版本
  • 两者解决相同问题,但Swift更优雅
  • 掌握两者有助于理解编程范式演进

迁移指南

从Block到Swift闭包的常见转换:

// Objective-C Block
[self fetchDataWithCompletion:^(NSData *data, NSError *error) {
    if (error) {
        [self showError:error];
    } else {
        [self processData:data];
    }
}];
// Swift闭包
fetchData { data, error in
    if let error = error {
        showError(error)
    } else if let data = data {
        processData(data)
    }
}

Swift 属性包装器

2026年1月29日 16:49

我们来看 The Swift Programming Language (6.2.3) 中的例子。

@propertyWrapper
struct TwelveOrLess {
    private var number = 0
    
    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, 12) }
    }
}

结构 TwelveOrLess 是属性包装器,属性包装器可以是 class、struct 和 enum。属性包装器需要有个属性 wrappedValue,表示被包装的值。TwelveOrLess 的 wrappedValue 属性是计算属性,读写私有的存储属性 number,其 setter 确保 number 小于或等于 12。

struct SmallRectangle {
    @TwelveOrLess var height: Int
    @TwelveOrLess var width: Int
}

结构 SmallRectangle 应用包装器 TwelveOrLess 到属性 height 和 width,编译器重写代码为:

struct SmallRectangle {
    private var _height = TwelveOrLess()
    private var _width = TwelveOrLess()
    var height: Int {
        get { return _height.wrappedValue }
        set { _height.wrappedValue = newValue }
    }
    var width: Int {
        get { return _width.wrappedValue }
        set { _width.wrappedValue = newValue }
    }
}

生成 _height 和 _width 存储属性,存储包装器 TwelveOrLess 的实例。height 和 width 成为计算属性,访问 _height 和 _width 的 wrappedValue。

v2-a712b4a4c7d80561227b6bc40f5c8608_1440w.png

编译器还会为 SmallRectangle 生成 memberwise 初始化器,此时生成的初始化器为

init(
    height: TwelveOrLess = TwelveOrLess(),
    width: TwelveOrLess = TwelveOrLess()
)

参数 height 和 width 的类型为包装器类型 TwelveOrLess,TwelveOrLess 的初始化器为默认初始化器 init()。 如果 TwelveOrLess 增加初始化器 init(wrappedValue: Int),

@propertyWrapper
struct TwelveOrLess {   
    private var number = 0
    
    init(wrappedValue: Int) {
        self.number = min(wrappedValue, 12)
    }

    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, 12) }
    }
}

则 SmallRectangle 的初始化器为

init(
    height: Int,
    width: Int
)

参数 height 和 width 的类型为原始类型 Int。

如果 TwelveOrLess 增加初始化器 init(),

@propertyWrapper
struct TwelveOrLess {   
    private var number = 0
    
    init() {
        number  = 1
    }
    
    init(wrappedValue: Int) {
        self.number = min(wrappedValue, 12)
    }

    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, 12) }
    }
}

则 SmallRectangle 的初始化器为

init(
    height: TwelveOrLess = TwelveOrLess(),
    width: TwelveOrLess = TwelveOrLess()
)

此时想让 SmallRectangle 的 memberwise 初始化器参数类型为原始类型 Int,需要修改为

struct SmallRectangle {
    @TwelveOrLess var height: Int = 1
    @TwelveOrLess var width: Int = 1
}

编译器生成的代码为

struct SmallRectangle {
    private var _height = TwelveOrLess(wrappedValue: 1)
    private var _width = TwelveOrLess(wrappedValue: 1)
    var height: Int {
        get { return _height.wrappedValue }
        set { _height.wrappedValue = newValue }
    }
    var width: Int {
        get { return _width.wrappedValue }
        set { _width.wrappedValue = newValue }
    }
    
    init(height: Int, width: Int) {
        self.height = height
        self.width = width
    }
}

总结:对一个 struct 的某个属性应用包装器,要使 memberwise 初始化器对应参数类型为原始类型,需要如下条件之一,

  • 属性包装器有初始化器 init(wrappedValue:),并且没有 init()
  • 属性有初始值,像 @TwelveOrLess var height: Int = 1

否则,memberwise 初始化器参数类型为包装器类型。

除了被包装的值,属性包装器可以通过定义一个 projected value 暴露额外的功能。

@propertyWrapper
struct SmallNumber {
    private var number: Int
    private(set) var projectedValue: Bool
    
    var wrappedValue: Int {
        get { return number }
        set {
            if newValue > 12 {
                number = 12
                projectedValue = true
            } else {
                number = newValue
                projectedValue = false
            }
        }
    }
    
    init() {
        self.number = 0
        self.projectedValue = false
    }
}

上面代码中,SmallNumber 结构增加了一个属性 projectedValue,用来记录包装器是否调整了被包装的值。

struct SomeStructure {
    @SmallNumber var someNumber: Int
}

var someStructure = SomeStructure()
print(someStructure.$someNumber)
// 打印 false
someStructure.someNumber = 55
print(someStructure.$someNumber)
// 打印 true

通过在被包装的属性名前增加 $ 来访问包装器的 projectedValue。

BSBacktraceLogger源码解析

作者 iOS在入门
2026年1月29日 14:55

借助AI辅助。

源码地址

github.com/bestswifter…

逐行注释

//
//  BSBacktraceLogger.m
//  BSBacktraceLogger
//
//  Created by 张星宇 on 16/8/27.
//  Copyright © 2016年 bestswifter. All rights reserved.
//

// ==================== 头文件引入说明 ====================

// 导入自定义头文件,包含对外公开的接口声明
#import "BSBacktraceLogger.h"

// 【Mach内核】导入Mach内核相关头文件
// Mach是XNU内核的核心部分,提供了底层的线程、任务、内存管理等功能
// 这个头文件包含了thread_t(线程类型)、task_t(任务/进程类型)、kern_return_t(内核返回值类型)等
#import <mach/mach.h>

// 【动态链接】导入动态链接库相关函数
// 主要包含dladdr()函数,用于将内存地址转换为符号信息(本代码自己实现了类似功能)
// Dl_info结构体定义也在这里,用于存储符号信息(文件名、函数名、地址等)
#include <dlfcn.h>

// 【POSIX线程】导入POSIX线程库(pthread)
// pthread是跨平台的线程标准,Mach线程是macOS/iOS特有的底层线程
// 本代码需要在NSThread、pthread和Mach线程之间进行转换
#include <pthread.h>

// 【系统类型】导入系统基础类型定义
// 如size_t、ssize_t等基础类型
#include <sys/types.h>

// 【限制值】导入系统限制值定义
// 如ULONG_MAX(无符号长整型最大值)、UINT_MAX(无符号整型最大值)等
#include <limits.h>

// 【字符串操作】导入C标准库字符串操作函数
// 如strcmp(字符串比较)、strrchr(查找字符)等
#include <string.h>

// 【动态链接器】导入dyld(dynamic linker)相关函数
// dyld负责加载和链接动态库,提供了获取已加载镜像(动态库/可执行文件)信息的函数
// 如_dyld_image_count()、_dyld_get_image_header()等
#include <mach-o/dyld.h>

// 【符号表】导入Mach-O文件格式中符号表相关的结构体定义
// nlist/nlist_64是符号表项的结构体,包含符号名、地址、类型等信息
#include <mach-o/nlist.h>

// ==================== CPU架构适配宏定义 ====================
// 不同CPU架构的寄存器名称和结构不同,需要通过条件编译来适配

#pragma -mark DEFINE MACRO FOR DIFFERENT CPU ARCHITECTURE

// 【ARM64架构】适用于iPhone 5s及以后的iOS设备、Apple Silicon Mac
#if defined(__arm64__)

// ARM64指令地址去标签宏
// 背景知识:ARM64架构中,指针的低位可能被用来存储额外信息(称为"标签位")
// 低2位通常用于标识指针类型或其他元数据,需要清除这些位才能得到真实地址
// ~(3UL) 等于 11111...11100(二进制),与操作后会清除低2位
#define DETAG_INSTRUCTION_ADDRESS(A) ((A) & ~(3UL))

// 线程状态结构体的元素数量
// ARM_THREAD_STATE64_COUNT 是系统定义的常量,表示ARM64架构下线程状态结构体包含多少个字段
#define BS_THREAD_STATE_COUNT ARM_THREAD_STATE64_COUNT

// 线程状态类型标识符
// 用于告诉thread_get_state()函数我们想要获取哪种类型的线程状态
#define BS_THREAD_STATE ARM_THREAD_STATE64

// 帧指针寄存器(Frame Pointer Register)
// __fp 是ARM64架构中的x29寄存器,专门用作帧指针
// 帧指针指向当前函数的栈帧起始位置,通过它可以遍历整个调用栈
#define BS_FRAME_POINTER __fp

// 栈指针寄存器(Stack Pointer Register)
// __sp 是ARM64架构中的SP寄存器,指向栈顶
// 栈是向下增长的,SP总是指向最后一个压入栈的数据
#define BS_STACK_POINTER __sp

// 程序计数器/指令指针寄存器(Program Counter)
// __pc 是ARM64架构中的PC寄存器,指向当前正在执行的指令地址
// 这是我们获取调用栈的起点
#define BS_INSTRUCTION_ADDRESS __pc

// 【ARM32架构】适用于iPhone 5及之前的旧设备(已基本淘汰)
#elif defined(__arm__)

// ARM32指令地址去标签宏
// ARM32的低1位用于标识Thumb模式(ARM有两种指令集:ARM和Thumb)
// 如果低位是1,表示Thumb模式;如果是0,表示ARM模式
// ~(1UL) 等于 11111...11110(二进制),与操作后会清除低1位
#define DETAG_INSTRUCTION_ADDRESS(A) ((A) & ~(1UL))

// 线程状态结构体的元素数量(32位ARM)
#define BS_THREAD_STATE_COUNT ARM_THREAD_STATE_COUNT

// 线程状态类型标识符(32位ARM)
#define BS_THREAD_STATE ARM_THREAD_STATE

// 帧指针寄存器(32位ARM使用r7寄存器作为帧指针)
// ARM32有16个通用寄存器r0-r15,其中r7在iOS中被约定用作帧指针
#define BS_FRAME_POINTER __r[7]

// 栈指针寄存器(r13,也称为SP)
#define BS_STACK_POINTER __sp

// 程序计数器寄存器(r15,也称为PC)
#define BS_INSTRUCTION_ADDRESS __pc

// 【x86_64架构】适用于64位Intel Mac、iOS模拟器
#elif defined(__x86_64__)

// x86架构的指令地址不需要去标签,直接返回原值
// x86架构不像ARM那样在指针中嵌入额外信息
#define DETAG_INSTRUCTION_ADDRESS(A) (A)

// 线程状态结构体的元素数量(x86_64)
#define BS_THREAD_STATE_COUNT x86_THREAD_STATE64_COUNT

// 线程状态类型标识符(x86_64)
#define BS_THREAD_STATE x86_THREAD_STATE64

// 帧指针寄存器(x86_64使用rbp寄存器)
// rbp是Base Pointer的缩写,专门用作帧指针
#define BS_FRAME_POINTER __rbp

// 栈指针寄存器(rsp = Stack Pointer)
#define BS_STACK_POINTER __rsp

// 指令指针寄存器(rip = Instruction Pointer)
// x86_64使用rip而不是pc来表示指令指针
#define BS_INSTRUCTION_ADDRESS __rip

// 【i386架构】适用于32位Intel Mac、旧的iOS模拟器(已基本淘汰)
#elif defined(__i386__)

// x86架构不需要去标签
#define DETAG_INSTRUCTION_ADDRESS(A) (A)

// 线程状态结构体的元素数量(i386)
#define BS_THREAD_STATE_COUNT x86_THREAD_STATE32_COUNT

// 线程状态类型标识符(i386)
#define BS_THREAD_STATE x86_THREAD_STATE32

// 帧指针寄存器(32位x86使用ebp)
// ebp是Extended Base Pointer的缩写
#define BS_FRAME_POINTER __ebp

// 栈指针寄存器(esp = Extended Stack Pointer)
#define BS_STACK_POINTER __esp

// 指令指针寄存器(eip = Extended Instruction Pointer)
#define BS_INSTRUCTION_ADDRESS __eip

#endif

// ==================== 通用宏定义 ====================

// 【返回地址转调用指令地址】
// 背景知识:当函数A调用函数B时:
//   1. CPU会将"返回地址"(return address)压入栈,这个地址指向函数A中CALL指令的下一条指令
//   2. 如果我们想知道是在哪里调用的函数B,需要找到CALL指令本身的地址
//   3. CALL指令的地址 = 返回地址 - 1(或更多,取决于指令长度)
// 这个宏就是做这个转换:先去标签,然后减1
#define CALL_INSTRUCTION_FROM_RETURN_ADDRESS(A) (DETAG_INSTRUCTION_ADDRESS((A)) - 1)

// 【根据指针大小定义格式化字符串】
// __LP64__ 表示"Long and Pointer are 64-bit",用于区分32位和64位系统
#if defined(__LP64__)

// 64位系统的格式定义
// 堆栈跟踪输出格式示例:
// 0   MyApp                           0x0000000100001234 main + 52
// %-4d:序号,左对齐,占4个字符
// %-31s:模块名,左对齐,占31个字符
// 0x%016lx:地址,16位十六进制(前面补0)
// %s:符号名(函数名)
// %lu:偏移量(无符号长整型)
#define TRACE_FMT         "%-4d%-31s 0x%016lx %s + %lu"

// 指针的完整格式:0x0000000100001234(16位十六进制,前面补0)
#define POINTER_FMT       "0x%016lx"

// 指针的短格式:0x100001234(不补0)
#define POINTER_SHORT_FMT "0x%lx"

// 符号表结构体类型(64位)
// nlist_64包含:符号名索引、符号类型、段索引、描述符、符号地址(64位)
#define BS_NLIST struct nlist_64

#else

// 32位系统的格式定义
// 输出格式示例:
// 0   MyApp                           0x00001234 main + 52
#define TRACE_FMT         "%-4d%-31s 0x%08lx %s + %lu"

// 指针的完整格式:0x00001234(8位十六进制)
#define POINTER_FMT       "0x%08lx"

// 指针的短格式
#define POINTER_SHORT_FMT "0x%lx"

// 符号表结构体类型(32位)
// nlist包含:符号名索引、符号类型、段索引、描述符、符号地址(32位)
#define BS_NLIST struct nlist

#endif

// ==================== 数据结构定义 ====================

// 【栈帧结构体】
// 背景知识:什么是栈帧(Stack Frame)?
// 每次函数调用时,系统会在栈上分配一块内存区域,称为"栈帧",用于存储:
//   - 函数的局部变量
//   - 函数参数
//   - 返回地址(调用者的下一条指令)
//   - 前一个栈帧的地址(用于回溯)
//
// 栈帧通过帧指针(Frame Pointer)连接成链表结构:
//   main的栈帧 ← funcA的栈帧 ← funcB的栈帧 ← 当前函数栈帧
//
// BSStackFrameEntry就是这个链表节点的简化版本,只包含:
//   - previous: 指向调用者(上一个函数)的栈帧
//   - return_address: 返回到调用者的地址
typedef struct BSStackFrameEntry{
    // 指向前一个栈帧的指针(const表示这个指针本身不可修改)
    // 通过这个指针可以一层层往回遍历,直到找到main函数甚至更底层
    const struct BSStackFrameEntry *const previous;
    
    // 返回地址:当前函数执行完毕后,应该返回到哪里继续执行
    // 这个地址指向调用者的代码中,CALL指令的下一条指令
    // uintptr_t是无符号整型,其大小与指针相同(32位系统是32位,64位系统是64位)
    const uintptr_t return_address;
} BSStackFrameEntry;

// ==================== 静态全局变量 ====================

// 【主线程ID】
// mach_port_t 是Mach内核中的"端口"类型,用于进程间通信
// 在Mach中,每个线程都有一个唯一的端口ID(实际上就是一个整数)
// 这个变量在+load方法中初始化,保存主线程的Mach端口ID,用于后续快速识别主线程
static mach_port_t main_thread_id;

// ==================== 类实现 ====================

@implementation BSBacktraceLogger

// 【类加载方法】
// +load方法的特点:
//   1. 在程序启动时,类被加载到内存时自动调用(比main函数还早)
//   2. 每个类只调用一次
//   3. 即使类没有被使用也会调用
//   4. 调用顺序:父类 → 子类 → 分类
+ (void)load {
    // mach_thread_self() 返回当前线程的Mach端口ID
    // 因为+load在主线程中执行,所以这里获取的就是主线程ID
    // 保存这个ID是因为后续判断主线程时比较方便(直接比较ID,而不需要通过名称匹配)
    main_thread_id = mach_thread_self();
}

#pragma -mark Implementation of interface
// ==================== 公共接口实现 ====================

// 【获取指定NSThread的调用栈】
// 参数:thread - NSThread对象(OC层的线程抽象)
// 返回:格式化的调用栈字符串
+ (NSString *)bs_backtraceOfNSThread:(NSThread *)thread {
    // 分两步:
    // 1. bs_machThreadFromNSThread: 将OC的NSThread转换为底层的Mach线程ID
    // 2. _bs_backtraceOfThread: 根据Mach线程ID获取调用栈
    // 为什么要转换?因为真正的线程操作需要使用Mach内核API,它只认Mach线程ID
    return _bs_backtraceOfThread(bs_machThreadFromNSThread(thread));
}

// 【获取当前线程的调用栈】
// 应用场景:在检测到异常或需要记录日志时,快速获取当前位置的调用栈
+ (NSString *)bs_backtraceOfCurrentThread {
    // [NSThread currentThread] 返回当前正在执行的线程对象
    return [self bs_backtraceOfNSThread:[NSThread currentThread]];
}

// 【获取主线程的调用栈】
// 应用场景:卡顿检测 - 当检测到主线程卡顿时,获取主线程调用栈分析卡在哪里
+ (NSString *)bs_backtraceOfMainThread {
    // [NSThread mainThread] 返回主线程对象
    return [self bs_backtraceOfNSThread:[NSThread mainThread]];
}

// 【获取所有线程的调用栈】
// 应用场景:
//   1. 死锁检测 - 查看所有线程状态,分析是否存在相互等待
//   2. 性能分析 - 定期采样所有线程,统计CPU热点
//   3. 崩溃日志 - 在崩溃时记录所有线程状态
+ (NSString *)bs_backtraceOfAllThread {
    // thread_act_array_t 是线程ID数组的类型(实际上是 thread_t* 指针)
    thread_act_array_t threads;
    
    // mach_msg_type_number_t 是Mach消息类型的数量类型(实际上是unsigned int)
    mach_msg_type_number_t thread_count = 0;
    
    // task_t 是Mach中"任务"的类型,任务就是进程的Mach术语
    // mach_task_self() 返回当前进程的任务端口
    const task_t this_task = mach_task_self();
    
    // 【task_threads函数】获取指定任务(进程)的所有线程
    // 参数1:任务端口(进程)
    // 参数2:输出参数,返回线程ID数组
    // 参数3:输出参数,返回线程数量
    // 返回值:kern_return_t 是内核函数的返回值类型,KERN_SUCCESS表示成功
    kern_return_t kr = task_threads(this_task, &threads, &thread_count);
    
    // 检查是否成功获取线程列表
    if(kr != KERN_SUCCESS) {
        return @"Fail to get information of all threads";
    }
    
    // 创建可变字符串,用于拼接所有线程的回溯信息
    // %u 是无符号整型格式符
    NSMutableString *resultString = [NSMutableString stringWithFormat:@"Call Backtrace of %u threads:\n", thread_count];
    
    // 遍历所有线程
    for(int i = 0; i < thread_count; i++) {
        // threads[i] 是第i个线程的Mach端口ID
        // 获取该线程的调用栈并追加到结果字符串
        [resultString appendString:_bs_backtraceOfThread(threads[i])];
    }
    
    // 返回不可变副本(防止外部修改)
    return [resultString copy];
}

#pragma -mark Get call backtrace of a mach_thread
// ==================== 核心:获取Mach线程的调用栈回溯 ====================

// 【核心函数】根据Mach线程ID获取调用栈
// 参数:thread - thread_t类型,即Mach线程的端口ID(实际上是一个整数)
// 返回:格式化的调用栈字符串
NSString *_bs_backtraceOfThread(thread_t thread) {
    // 【步骤1:准备缓冲区】
    // 创建一个数组,用于存储最多50层的函数调用地址
    // uintptr_t 是"unsigned integer pointer type"的缩写,保证能容纳一个指针
    // 为什么是50?这是一个经验值,通常调用栈不会超过50层
    uintptr_t backtraceBuffer[50];
    
    // 当前处理到第几层调用栈
    int i = 0;
    
    // 创建结果字符串,包含线程ID(用于识别是哪个线程)
    NSMutableString *resultString = [[NSMutableString alloc] initWithFormat:@"Backtrace of Thread %u:\n", thread];
    
    // 【步骤2:获取线程的寄存器状态】
    // _STRUCT_MCONTEXT 是机器上下文结构体,包含所有寄存器的当前值
    // mcontext = machine context(机器上下文)
    // 为什么需要它?因为调用栈的起点是当前寄存器的值(PC、FP、SP等)
    _STRUCT_MCONTEXT machineContext;
    
    // 调用函数获取线程状态并填充到machineContext中
    // 如果失败(比如线程已销毁、权限不足等),返回错误信息
    if(!bs_fillThreadStateIntoMachineContext(thread, &machineContext)) {
        return [NSString stringWithFormat:@"Fail to get information about thread: %u", thread];
    }
    
    // 【步骤3:提取当前指令地址(PC寄存器)】
    // PC(Program Counter)= 程序计数器,指向当前正在执行的指令
    // 这是调用栈的第0层(最内层,即当前函数)
    const uintptr_t instructionAddress = bs_mach_instructionAddress(&machineContext);
    backtraceBuffer[i] = instructionAddress;
    ++i;
    
    // 【步骤4:提取链接寄存器(LR,仅ARM架构)】
    // LR(Link Register)= 链接寄存器,ARM架构特有
    // 背景知识:ARM架构中,当函数A调用函数B时:
    //   - 返回地址不是压入栈,而是保存在LR寄存器中(为了提高性能)
    //   - 如果函数B还要调用函数C,那么会先把LR的值压入栈,再更新LR
    // 所以LR通常包含直接调用者的地址(调用栈的第1层)
    uintptr_t linkRegister = bs_mach_linkRegister(&machineContext);
    if (linkRegister) {
        backtraceBuffer[i] = linkRegister;
        i++;
    }
    
    // 【安全检查】确保指令地址有效
    if(instructionAddress == 0) {
        return @"Fail to get instruction address";
    }
    
    // 【步骤5:开始遍历栈帧链表】
    // 初始化栈帧结构体(全部清零)
    BSStackFrameEntry frame = {0};
    
    // 获取帧指针(FP寄存器的值)
    // FP指向当前函数的栈帧,栈帧的开头就是BSStackFrameEntry结构体
    const uintptr_t framePtr = bs_mach_framePointer(&machineContext);
    
    // 检查帧指针是否有效,并尝试读取第一个栈帧
    // bs_mach_copyMem是安全的内存读取函数(使用内核API,可以读取其他线程的内存)
    if(framePtr == 0 ||
       bs_mach_copyMem((void *)framePtr, &frame, sizeof(frame)) != KERN_SUCCESS) {
        return @"Fail to get frame pointer";
    }
    
    // 【步骤6:循环遍历栈帧链表】
    // 栈帧链表:当前栈帧 → 调用者栈帧 → 调用者的调用者栈帧 → ... → main → _start
    for(; i < 50; i++) {
        // 保存当前栈帧的返回地址(即"从哪里调用过来的")
        backtraceBuffer[i] = frame.return_address;
        
        // 终止条件(满足任一条件就停止遍历):
        // 1. 返回地址为0 - 已经到达栈底
        // 2. 前一个栈帧指针为0 - 没有更上层的调用者了
        // 3. 无法读取前一个栈帧 - 内存访问失败(可能栈帧已损坏)
        if(backtraceBuffer[i] == 0 ||
           frame.previous == 0 ||
           bs_mach_copyMem(frame.previous, &frame, sizeof(frame)) != KERN_SUCCESS) {
            break;
        }
    }
    
    // 【步骤7:符号化所有地址】
    // 记录实际获取到多少层调用栈
    int backtraceLength = i;
    
    // Dl_info 是动态链接信息结构体(定义在<dlfcn.h>),包含:
    //   - dli_fname: 文件名(如"MyApp"、"libSystem.dylib")
    //   - dli_fbase: 文件加载的基地址
    //   - dli_sname: 符号名(函数名,如"main"、"-[ViewController viewDidLoad]")
    //   - dli_saddr: 符号地址(函数的起始地址)
    Dl_info symbolicated[backtraceLength];
    
    // bs_symbolicate函数将每个地址转换为符号信息
    // 参数1:地址数组
    // 参数2:输出符号信息数组
    // 参数3:数组长度
    // 参数4:跳过的条目数(0表示不跳过)
    bs_symbolicate(backtraceBuffer, symbolicated, backtraceLength, 0);
    
    // 【步骤8:格式化输出】
    // 遍历所有调用栈层级,格式化成可读字符串
    for (int i = 0; i < backtraceLength; ++i) {
        // bs_logBacktraceEntry格式化单条调用栈信息
        // 输出格式类似:0   MyApp    0x0000000100001234 main + 52
        [resultString appendFormat:@"%@", bs_logBacktraceEntry(i, backtraceBuffer[i], &symbolicated[i])];
    }
    
    // 添加结尾换行
    [resultString appendFormat:@"\n"];
    
    // 返回不可变副本
    return [resultString copy];
}

#pragma -mark Convert NSThread to Mach thread
// ==================== NSThread转Mach线程 ====================

// 【线程转换函数】将NSThread对象转换为Mach线程ID
// 背景知识:iOS/macOS中有三种线程抽象:
//   1. NSThread - OC层的线程封装,提供面向对象的接口
//   2. pthread - POSIX标准的线程,跨平台(Unix/Linux/macOS都支持)
//   3. Mach thread - Mach内核的原生线程,macOS/iOS特有,性能最高
// NSThread内部实际上封装了pthread,而pthread底层又是Mach thread
// 但它们之间没有直接的API可以互相转换,所以需要用一些技巧
//
// 参数:nsthread - NSThread对象
// 返回:thread_t - Mach线程ID(实际上是mach_port_t类型,即端口号)
thread_t bs_machThreadFromNSThread(NSThread *nsthread) {
    // 【准备工作】
    // 用于存储pthread线程名称的缓冲区(最多256字符)
    char name[256];
    
    // 线程数量
    mach_msg_type_number_t count;
    
    // 线程列表数组
    thread_act_array_t list;
    
    // 获取当前进程的所有Mach线程
    // mach_task_self() 返回当前进程的任务端口
    task_threads(mach_task_self(), &list, &count);
    
    // 【转换策略:通过线程名称匹配】
    // 因为没有直接的API,所以采用"临时修改线程名称"的方法:
    // 1. 给NSThread设置一个唯一的名称(时间戳)
    // 2. 遍历所有Mach线程,将其转换为pthread
    // 3. 通过pthread的名称找到匹配的线程
    // 4. 恢复原始名称
    
    // 获取当前时间戳(精确到微秒),确保唯一性
    // timeIntervalSince1970 返回自1970-01-01 00:00:00到现在的秒数(浮点数)
    NSTimeInterval currentTimestamp = [[NSDate date] timeIntervalSince1970];
    
    // 保存NSThread的原始名称(稍后需要恢复)
    NSString *originName = [nsthread name];
    
    // 将线程名称临时设置为时间戳字符串
    // 例如:"1609459200.123456"
    [nsthread setName:[NSString stringWithFormat:@"%f", currentTimestamp]];
    
    // 【特殊处理:主线程】
    // 主线程ID在+load方法中已经保存,可以直接返回,无需遍历
    if ([nsthread isMainThread]) {
        return (thread_t)main_thread_id;
    }
    
    // 【遍历所有Mach线程进行匹配】
    for (int i = 0; i < count; ++i) {
        // pthread_from_mach_thread_np: 将Mach线程转换为pthread
        // _np后缀表示"non-portable"(非可移植),即macOS/iOS特有的扩展函数
        pthread_t pt = pthread_from_mach_thread_np(list[i]);
        
        // 【冗余检查】这段代码实际上永远不会执行
        // 因为如果isMainThread为true,前面已经return了
        // 这可能是代码重构时遗留的冗余逻辑
        if ([nsthread isMainThread]) {
            if (list[i] == main_thread_id) {
                return list[i];
            }
        }
        
        // 如果pthread转换成功(有些Mach线程可能没有对应的pthread)
        if (pt) {
            // 清空名称缓冲区
            name[0] = '\0';
            
            // pthread_getname_np: 获取pthread的线程名称
            // 参数1:pthread
            // 参数2:输出缓冲区
            // 参数3:缓冲区大小
            pthread_getname_np(pt, name, sizeof name);
            
            // strcmp: 字符串比较函数,相等返回0
            // 如果pthread的名称与我们设置的时间戳匹配,说明找到了对应的Mach线程
            if (!strcmp(name, [nsthread name].UTF8String)) {
                // 【找到匹配】恢复原始线程名称
                [nsthread setName:originName];
                
                // 返回Mach线程ID
                return list[i];
            }
        }
    }
    
    // 【未找到匹配】恢复原始名称
    [nsthread setName:originName];
    
    // 返回当前线程ID作为后备方案
    // mach_thread_self() 返回当前正在执行的线程ID
    // 这种情况通常不应该发生,除非传入的NSThread对象有问题
    return mach_thread_self();
}

#pragma -mark GenerateBacbsrackEnrty
// ==================== 格式化调用栈条目 ====================

// 【格式化单条调用栈】将地址和符号信息格式化为可读字符串
// 输出示例:0   MyApp       0x0000000100001234 main + 52
//          ^   ^            ^                  ^    ^ ^
//          |   |            |                  |    | |
//        序号 模块名        地址              函数名 + 偏移量
//
// 参数:
//   entryNum - 调用栈序号(0表示最内层,数字越大越外层)
//   address - 内存地址
//   dlInfo - 符号信息(包含文件名、函数名、地址等)
// 返回:格式化的字符串
NSString* bs_logBacktraceEntry(const int entryNum,
                               const uintptr_t address,
                               const Dl_info* const dlInfo) {
    // 文件地址缓冲区(当文件名为空时用于存储地址的字符串形式)
    char faddrBuff[20];
    
    // 符号地址缓冲区(当符号名为空时用于存储地址的字符串形式)
    char saddrBuff[20];
    
    // 【提取文件名】
    // dlInfo->dli_fname 是完整路径,如"/System/Library/Frameworks/UIKit.framework/UIKit"
    // bs_lastPathEntry 提取最后一部分,如"UIKit"
    const char* fname = bs_lastPathEntry(dlInfo->dli_fname);
    
    // 如果文件名为空(符号信息不完整)
    if(fname == NULL) {
        // sprintf: 格式化输出到字符串
        // POINTER_FMT 是根据32/64位定义的格式(如"0x%08lx"或"0x%016lx")
        // dlInfo->dli_fbase 是文件的基地址(加载到内存的起始地址)
        sprintf(faddrBuff, POINTER_FMT, (uintptr_t)dlInfo->dli_fbase);
        
        // 使用地址字符串作为文件名
        fname = faddrBuff;
    }
    
    // 【计算偏移量】
    // 偏移量 = 当前地址 - 符号起始地址
    // 例如:main函数起始地址是0x100001200,当前地址是0x100001234,偏移量就是52字节
    uintptr_t offset = address - (uintptr_t)dlInfo->dli_saddr;
    
    // 【提取符号名】
    // dlInfo->dli_sname 是函数名,如"main"、"-[ViewController viewDidLoad]"
    const char* sname = dlInfo->dli_sname;
    
    // 如果符号名为空(可能是被strip剥离了符号,或者是动态生成的代码)
    if(sname == NULL) {
        // 使用文件基地址的字符串形式作为符号名
        sprintf(saddrBuff, POINTER_SHORT_FMT, (uintptr_t)dlInfo->dli_fbase);
        sname = saddrBuff;
        
        // 重新计算偏移量(相对于文件基地址而不是符号地址)
        offset = address - (uintptr_t)dlInfo->dli_fbase;
    }
    
    // 【格式化输出】
    // PRIxPTR 是可移植的指针格式符(定义在<inttypes.h>),根据平台自动选择正确的格式
    // 输出格式:
    //   %-30s: 文件名,左对齐,占30个字符
    //   0x%08" PRIxPTR ": 地址,十六进制,至少8位(32位)或16位(64位)
    //   %s: 符号名
    //   + %lu: 偏移量(无符号长整型)
    //   \n: 换行符
    return [NSString stringWithFormat:@"%-30s  0x%08" PRIxPTR " %s + %lu\n" ,fname, (uintptr_t)address, sname, offset];
}

// 【路径提取函数】从完整路径中提取文件名
// 例如:"/System/Library/Frameworks/UIKit.framework/UIKit" → "UIKit"
//       "MyApp" → "MyApp"(如果没有路径分隔符,返回原字符串)
//
// 参数:path - 完整路径字符串
// 返回:文件名字符串(指向原字符串的某个位置,不是新分配的内存)
const char* bs_lastPathEntry(const char* const path) {
    // 空指针检查
    if(path == NULL) {
        return NULL;
    }
    
    // strrchr: 从右往左查找指定字符
    // 查找最后一个'/'字符的位置
    char* lastFile = strrchr(path, '/');
    
    // 三元运算符:
    // 如果找到'/',返回'/'后面的部分(lastFile + 1跳过'/'字符)
    // 如果没找到'/',说明path本身就是文件名,直接返回
    return lastFile == NULL ? path : lastFile + 1;
}

#pragma -mark HandleMachineContext
// ==================== 机器上下文处理 ====================

// 【获取线程状态】将线程的寄存器状态填充到机器上下文结构体
// 这是获取调用栈的第一步:需要知道线程当前的寄存器值(PC、FP、SP等)
//
// 参数:
//   thread - Mach线程ID
//   machineContext - 输出参数,_STRUCT_MCONTEXT结构体指针,用于存储寄存器状态
// 返回:bool - true表示成功,false表示失败
bool bs_fillThreadStateIntoMachineContext(thread_t thread, _STRUCT_MCONTEXT *machineContext) {
    // 【mach_msg_type_number_t】
    // 状态结构体的大小(以"自然单位"为单位,通常是4字节)
    // BS_THREAD_STATE_COUNT 是根据CPU架构定义的宏,表示状态结构体包含多少个字段
    mach_msg_type_number_t state_count = BS_THREAD_STATE_COUNT;
    
    // 【thread_get_state】Mach内核函数,获取线程状态
    // 这是一个非常底层的系统调用,可以获取线程的完整寄存器快照
    //
    // 参数:
    //   thread - 线程ID
    //   BS_THREAD_STATE - 状态类型(ARM_THREAD_STATE64、x86_THREAD_STATE64等)
    //   (thread_state_t)&machineContext->__ss - 输出缓冲区
    //       __ss是"saved state"的缩写,表示保存的CPU状态
    //       thread_state_t 是void*的别名,所以需要类型转换
    //   &state_count - 输入/输出参数,输入时是缓冲区大小,输出时是实际写入的大小
    //
    // 返回值:kern_return_t - 内核函数的返回值
    //   KERN_SUCCESS (0) - 成功
    //   KERN_INVALID_ARGUMENT - 参数无效
    //   KERN_FAILURE - 其他失败
    kern_return_t kr = thread_get_state(thread, BS_THREAD_STATE, (thread_state_t)&machineContext->__ss, &state_count);
    
    // 检查是否成功
    return (kr == KERN_SUCCESS);
}

// 【提取帧指针】从机器上下文中读取帧指针寄存器的值
// 帧指针(Frame Pointer)指向当前函数的栈帧起始位置
// 通过帧指针可以遍历整个调用栈链表
//
// 参数:machineContext - 机器上下文(mcontext_t是_STRUCT_MCONTEXT的typedef别名)
// 返回:uintptr_t - 帧指针的值(一个内存地址)
uintptr_t bs_mach_framePointer(mcontext_t const machineContext){
    // machineContext->__ss 是"saved state",包含所有寄存器的值
    // BS_FRAME_POINTER 是根据CPU架构定义的宏:
    //   ARM64: __fp(x29寄存器)
    //   ARM32: __r[7](r7寄存器)
    //   x86_64: __rbp
    //   i386: __ebp
    return machineContext->__ss.BS_FRAME_POINTER;
}

// 【提取栈指针】从机器上下文中读取栈指针寄存器的值
// 栈指针(Stack Pointer)指向栈顶(最后压入的数据)
// 注意:栈是向下增长的,所以栈顶的地址比栈底小
//
// 参数:machineContext - 机器上下文
// 返回:uintptr_t - 栈指针的值
uintptr_t bs_mach_stackPointer(mcontext_t const machineContext){
    // BS_STACK_POINTER 根据CPU架构定义:
    //   ARM: __sp(Stack Pointer)
    //   x86: __rsp(x86_64)或__esp(i386)
    return machineContext->__ss.BS_STACK_POINTER;
}

// 【提取指令地址】从机器上下文中读取程序计数器/指令指针的值
// 程序计数器(PC)指向当前正在执行的指令地址
// 这是调用栈回溯的起点
//
// 参数:machineContext - 机器上下文
// 返回:uintptr_t - 当前指令地址
uintptr_t bs_mach_instructionAddress(mcontext_t const machineContext){
    // BS_INSTRUCTION_ADDRESS 根据CPU架构定义:
    //   ARM: __pc(Program Counter)
    //   x86_64: __rip(Instruction Pointer)
    //   i386: __eip
    return machineContext->__ss.BS_INSTRUCTION_ADDRESS;
}

// 【提取链接寄存器】从机器上下文中读取链接寄存器的值(仅ARM架构有效)
// 链接寄存器(LR, Link Register)是ARM架构特有的寄存器
// 当函数A调用函数B时,返回地址会保存在LR中(而不是压入栈)
// 这样做的好处:
//   1. 性能更好(寄存器访问比内存访问快得多)
//   2. 简化了叶子函数(不调用其他函数的函数)的栈帧管理
//
// 参数:machineContext - 机器上下文
// 返回:uintptr_t - 链接寄存器的值(x86架构返回0,因为没有LR)
uintptr_t bs_mach_linkRegister(mcontext_t const machineContext){
#if defined(__i386__) || defined(__x86_64__)
    // x86架构没有链接寄存器
    // 函数调用时,返回地址直接压入栈
    return 0;
#else
    // ARM架构:返回LR寄存器的值
    // __lr 是ARM架构中的x30寄存器(ARM64)或r14寄存器(ARM32)
    return machineContext->__ss.__lr;
#endif
}

// 【安全的内存复制】从指定内存地址复制数据到目标地址
// 为什么需要这个函数?
//   1. 直接使用指针访问其他线程的内存可能会崩溃(权限问题、野指针等)
//   2. vm_read_overwrite是内核级别的内存读取,更安全、更可靠
//   3. 可以跨线程读取内存(读取其他线程的栈内存)
//
// 参数:
//   src - 源地址(要读取的内存地址)
//   dst - 目标地址(复制到哪里)
//   numBytes - 要复制的字节数
// 返回:kern_return_t - 内核函数返回值(KERN_SUCCESS表示成功)
kern_return_t bs_mach_copyMem(const void *const src, void *const dst, const size_t numBytes){
    // vm_size_t 是虚拟内存大小类型(实际上就是unsigned long)
    // 用于记录实际复制了多少字节
    vm_size_t bytesCopied = 0;
    
    // 【vm_read_overwrite】虚拟内存读取覆写函数(Mach内核API)
    // 这是一个非常底层的系统调用,可以读取任意进程的内存
    //
    // 参数:
    //   mach_task_self() - 目标任务(进程),这里是当前进程
    //   (vm_address_t)src - 源地址,要读取的内存位置
    //   (vm_size_t)numBytes - 要读取的字节数
    //   (vm_address_t)dst - 目标地址,读取的数据写入到哪里
    //   &bytesCopied - 输出参数,实际复制的字节数
    //
    // 返回值:
    //   KERN_SUCCESS - 成功
    //   KERN_INVALID_ADDRESS - 地址无效(如野指针、空指针)
    //   KERN_PROTECTION_FAILURE - 权限不足
    //   KERN_NO_SPACE - 目标空间不足
    return vm_read_overwrite(mach_task_self(), (vm_address_t)src, (vm_size_t)numBytes, (vm_address_t)dst, &bytesCopied);
}

#pragma -mark Symbolicate
// ==================== 符号化:地址转换为可读符号 ====================

// 【符号化】将内存地址数组转换为符号信息数组
// 符号化就是"地址 → 可读信息"的过程:
//   0x0000000100001234 → "MyApp: main + 52"
//
// 为什么需要符号化?
//   - 内存地址对人类来说毫无意义
//   - 符号化后可以知道:哪个模块、哪个函数、距离函数起始地址多少字节
//   - 方便调试、性能分析、崩溃分析
//
// 参数:
//   backtraceBuffer - 输入,内存地址数组
//   symbolsBuffer - 输出,符号信息数组(Dl_info结构体数组)
//   numEntries - 数组长度
//   skippedEntries - 跳过的条目数(通常为0)
void bs_symbolicate(const uintptr_t* const backtraceBuffer,
                    Dl_info* const symbolsBuffer,
                    const int numEntries,
                    const int skippedEntries){
    // 当前处理的索引
    int i = 0;
    
    // 【特殊处理第一个地址】
    // 如果没有跳过条目,且索引有效
    if(!skippedEntries && i < numEntries) {
        // 第一个地址是当前指令地址(PC寄存器),直接符号化,不需要调整
        // 为什么不需要调整?因为PC指向的就是当前正在执行的指令
        bs_dladdr(backtraceBuffer[i], &symbolsBuffer[i]);
        i++;
    }
    
    // 【处理剩余的返回地址】
    // 从第二个地址开始,都是返回地址(return address)
    for(; i < numEntries; i++) {
        // 【关键点】返回地址需要-1才能得到调用指令的地址
        // 背景知识:
        //   当函数A调用函数B时,CPU执行CALL指令:
        //     1. 将"CALL的下一条指令地址"压入栈(这就是返回地址)
        //     2. 跳转到函数B
        //   所以返回地址指向的是CALL之后的指令,不是CALL本身
        //   为了知道是在哪里调用的,需要返回地址-1得到CALL指令的地址
        //
        // CALL_INSTRUCTION_FROM_RETURN_ADDRESS宏做两件事:
        //   1. DETAG_INSTRUCTION_ADDRESS: 去除ARM架构的标签位
        //   2. -1: 得到CALL指令地址
        bs_dladdr(CALL_INSTRUCTION_FROM_RETURN_ADDRESS(backtraceBuffer[i]), &symbolsBuffer[i]);
    }
}

// 【自定义dladdr】将内存地址转换为符号信息
// dladdr是系统函数(定义在<dlfcn.h>),但这里自己实现了一遍
// 为什么要自己实现?
//   1. 学习Mach-O文件格式和符号表结构
//   2. 可以添加自定义逻辑(如特殊符号处理)
//   3. 更好地理解符号化过程
//
// 参数:
//   address - 要查询的内存地址
//   info - 输出参数,Dl_info结构体指针,用于存储符号信息
// 返回:bool - true表示成功找到符号,false表示失败
bool bs_dladdr(const uintptr_t address, Dl_info* const info) {
    // 【步骤0:初始化输出结构体】
    // Dl_info结构体定义(来自<dlfcn.h>):
    //   typedef struct dl_info {
    //       const char *dli_fname;  // 文件名(模块名)
    //       void *dli_fbase;        // 文件基地址
    //       const char *dli_sname;  // 符号名(函数名)
    //       void *dli_saddr;        // 符号地址(函数起始地址)
    //   } Dl_info;
    info->dli_fname = NULL; // 文件名
    info->dli_fbase = NULL; // 文件基地址
    info->dli_sname = NULL; // 符号名(函数名)
    info->dli_saddr = NULL; // 符号地址
    
    // 【步骤1:查找包含该地址的镜像(动态库/可执行文件)】
    // 背景知识:iOS/macOS程序运行时会加载多个镜像:
    //   - 可执行文件本身(如MyApp)
    //   - 系统库(如UIKit.framework、libSystem.dylib)
    //   - 第三方库
    // bs_imageIndexContainingAddress 遍历所有镜像,找到包含该地址的那一个
    const uint32_t idx = bs_imageIndexContainingAddress(address);
    
    // 如果没找到(UINT_MAX表示失败),返回false
    // 这种情况很少发生,除非传入了无效地址
    if(idx == UINT_MAX) {
        return false;
    }
    
    // 【步骤2:获取镜像的Mach-O头部】
    // _dyld_get_image_header 是dyld提供的函数,返回指定索引的镜像头部
    // mach_header是Mach-O文件的头部结构体,包含:
    //   - magic: 魔数(标识文件类型和字节序)
    //   - cputype: CPU类型(ARM、x86等)
    //   - cpusubtype: CPU子类型
    //   - filetype: 文件类型(可执行文件、动态库等)
    //   - ncmds: 加载命令的数量
    //   - sizeofcmds: 所有加载命令的总大小
    const struct mach_header* header = _dyld_get_image_header(idx);
    
    // 【步骤3:处理ASLR(地址空间布局随机化)】
    // ASLR(Address Space Layout Randomization)是一种安全机制:
    //   - 每次程序启动时,镜像加载到内存的地址是随机的
    //   - 这样可以防止黑客利用固定地址进行攻击
    //
    // 地址关系:
    //   实际地址 = 文件中的地址 + ASLR偏移(slide)
    //
    // _dyld_get_image_vmaddr_slide 返回ASLR偏移量
    const uintptr_t imageVMAddrSlide = (uintptr_t)_dyld_get_image_vmaddr_slide(idx);
    
    // 计算去除ASLR偏移后的地址(文件中的原始地址)
    const uintptr_t addressWithSlide = address - imageVMAddrSlide;
    
    // 【步骤4:获取段基地址】
    // 段基地址用于计算符号表在内存中的位置
    // 符号表存储在__LINKEDIT段中,通过段基地址可以将文件偏移转换为内存地址
    const uintptr_t segmentBase = bs_segmentBaseOfImageIndex(idx) + imageVMAddrSlide;
    
    // 如果段基地址为0,说明获取失败
    if(segmentBase == 0) {
        return false;
    }
    
    // 【步骤5:填充基本信息】
    // _dyld_get_image_name 返回镜像的完整路径
    // 例如:"/System/Library/Frameworks/UIKit.framework/UIKit"
    info->dli_fname = _dyld_get_image_name(idx);
    
    // 文件基地址就是Mach-O头部地址
    info->dli_fbase = (void*)header;
    
    // 【步骤6:查找符号表并匹配最接近的符号】
    // 最佳匹配的符号表项(初始化为NULL,表示还没找到)
    const BS_NLIST* bestMatch = NULL;
    
    // 最小距离(初始化为最大值)
    // 我们要找的是:地址 >= 符号地址,且距离最小的那个符号
    uintptr_t bestDistance = ULONG_MAX;
    
    // 【步骤7:获取第一个加载命令的地址】
    // Mach-O文件结构:
    //   [Mach Header] [Load Commands] [Segments/Sections] [Data]
    // 加载命令紧跟在头部之后
    uintptr_t cmdPtr = bs_firstCmdAfterHeader(header);
    
    // 如果命令指针为0,说明头部损坏
    if(cmdPtr == 0) {
        return false;
    }
    
    // 【步骤8:遍历所有加载命令,查找符号表命令】
    // header->ncmds 是加载命令的数量
    for(uint32_t iCmd = 0; iCmd < header->ncmds; iCmd++) {
        // 将指针转换为load_command结构体
        // load_command是所有加载命令的基类,包含:
        //   - cmd: 命令类型(LC_SEGMENT、LC_SYMTAB等)
        //   - cmdsize: 命令大小
        const struct load_command* loadCmd = (struct load_command*)cmdPtr;
        
        // 【查找符号表命令】
        // LC_SYMTAB表示这是一个符号表命令
        if(loadCmd->cmd == LC_SYMTAB) {
            // 转换为symtab_command结构体
            // symtab_command包含:
            //   - symoff: 符号表在文件中的偏移
            //   - nsyms: 符号数量
            //   - stroff: 字符串表在文件中的偏移
            //   - strsize: 字符串表大小
            const struct symtab_command* symtabCmd = (struct symtab_command*)cmdPtr;
            
            // 【计算符号表在内存中的地址】
            // 符号表地址 = 段基地址 + 文件偏移
            // BS_NLIST是根据32/64位定义的符号表项类型(nlist或nlist_64)
            const BS_NLIST* symbolTable = (BS_NLIST*)(segmentBase + symtabCmd->symoff);
            
            // 【计算字符串表在内存中的地址】
            // 字符串表存储所有符号的名称字符串
            // 符号表项中只存储字符串的索引,真正的字符串在字符串表中
            const uintptr_t stringTable = segmentBase + symtabCmd->stroff;
            
            // 【遍历符号表,查找最佳匹配】
            for(uint32_t iSym = 0; iSym < symtabCmd->nsyms; iSym++) {
                // nlist/nlist_64结构体:
                //   - n_un.n_strx: 符号名在字符串表中的索引
                //   - n_type: 符号类型(函数、变量、调试符号等)
                //   - n_sect: 符号所在的段索引
                //   - n_desc: 描述符(引用类型、可见性等)
                //   - n_value: 符号地址(函数/变量的地址)
                
                // 【过滤外部符号】
                // 如果n_value为0,说明这是一个外部符号(引用其他库的符号)
                // 外部符号的地址在当前镜像中不存在,需要跳过
                if(symbolTable[iSym].n_value != 0) {
                    // 符号的基地址(函数/变量的起始地址)
                    uintptr_t symbolBase = symbolTable[iSym].n_value;
                    
                    // 计算地址与符号基地址的距离
                    uintptr_t currentDistance = addressWithSlide - symbolBase;
                    
                    // 【最佳匹配算法】
                    // 条件1:地址必须 >= 符号基地址(即地址在符号的范围内)
                    // 条件2:距离必须 <= 当前最佳距离(找最接近的符号)
                    //
                    // 为什么这样做?
                    // 例如:
                    //   main函数起始地址:0x100001200
                    //   其他函数起始地址:0x100001300
                    //   查询地址:0x100001234
                    // 那么最佳匹配是main(距离0x34),而不是其他函数(距离为负)
                    if((addressWithSlide >= symbolBase) &&
                       (currentDistance <= bestDistance)) {
                        // 更新最佳匹配
                        bestMatch = symbolTable + iSym;
                        // 更新最小距离
                        bestDistance = currentDistance;
                    }
                }
            }
            
            // 【步骤9:提取符号信息】
            // 如果找到了匹配的符号
            if(bestMatch != NULL) {
                // 符号地址 = 文件中的地址 + ASLR偏移
                info->dli_saddr = (void*)(bestMatch->n_value + imageVMAddrSlide);
                
                // 【提取符号名】
                // 符号名地址 = 字符串表基地址 + 符号名索引
                // (intptr_t)强制转换是为了正确处理指针算术
                info->dli_sname = (char*)((intptr_t)stringTable + (intptr_t)bestMatch->n_un.n_strx);
                
                // 【处理C/C++符号前缀】
                // C/C++编译器会在符号名前加下划线(_)
                // 例如:main → _main,printf → _printf
                // 为了输出更友好,跳过开头的下划线
                if(*info->dli_sname == '_') {
                    info->dli_sname++;
                }
                
                // 【处理stripped符号】
                // 如果符号地址等于文件基地址,且类型为3(N_SECT)
                // 说明这是一个section符号(不是真正的函数/变量符号)
                // 这种情况通常发生在符号被strip剥离后
                // 剥离符号可以减小文件大小,但会导致调试困难
                if(info->dli_saddr == info->dli_fbase && bestMatch->n_type == 3) {
                    info->dli_sname = NULL;
                }
                
                // 找到符号后跳出循环(不需要继续遍历其他加载命令)
                break;
            }
        }
        
        // 移动到下一个加载命令
        // cmdPtr += loadCmd->cmdsize 等价于 cmdPtr = cmdPtr + cmdsize
        cmdPtr += loadCmd->cmdsize;
    }
    
    // 返回成功(即使没找到符号,也返回true,因为至少找到了文件名)
    return true;
}

// ==================== 辅助函数:Mach-O文件解析 ====================

// 【获取第一个加载命令地址】
// Mach-O文件结构:[Header][Load Commands][Segments][Data]
// 这个函数计算加载命令的起始地址
//
// 参数:header - Mach-O头部指针
// 返回:uintptr_t - 第一个加载命令的地址(0表示头部损坏)
uintptr_t bs_firstCmdAfterHeader(const struct mach_header* const header) {
    // 根据魔数(magic number)判断文件类型和字节序
    switch(header->magic) {
        // 【32位Mach-O】
        case MH_MAGIC:    // 0xfeedface - 32位小端序
        case MH_CIGAM:    // 0xcefaedfe - 32位大端序(字节序相反)
            // 32位Mach-O头部大小是sizeof(struct mach_header)
            // header + 1 会跳过整个头部,指向加载命令起始位置
            return (uintptr_t)(header + 1);
            
        // 【64位Mach-O】
        case MH_MAGIC_64: // 0xfeedfacf - 64位小端序
        case MH_CIGAM_64: // 0xcffaedfe - 64位大端序
            // 64位Mach-O头部大小是sizeof(struct mach_header_64)
            // 需要先转换为mach_header_64*,然后+1跳过头部
            return (uintptr_t)(((struct mach_header_64*)header) + 1);
            
        default:
            // 未知魔数,说明文件损坏或不是Mach-O文件
            return 0;  // Header is corrupt
    }
}

// 【查找包含指定地址的镜像索引】
// 背景知识:程序运行时会加载多个镜像(可执行文件、动态库)
// 每个镜像被加载到内存的不同区域,这个函数找出地址属于哪个镜像
//
// 参数:address - 要查询的内存地址
// 返回:uint32_t - 镜像索引(UINT_MAX表示未找到)
uint32_t bs_imageIndexContainingAddress(const uintptr_t address) {
    // 【获取镜像总数】
    // _dyld_image_count() 返回当前进程加载的镜像数量
    // 镜像(image)包括:
    //   - 主程序可执行文件(如MyApp)
    //   - 动态库(如UIKit.framework、libSystem.dylib)
    //   - 插件、bundle等
    const uint32_t imageCount = _dyld_image_count();
    
    // Mach-O头部指针
    const struct mach_header* header = 0;
    
    // 【遍历所有镜像】
    for(uint32_t iImg = 0; iImg < imageCount; iImg++) {
        // _dyld_get_image_header(i) 返回第i个镜像的Mach-O头部
        header = _dyld_get_image_header(iImg);
        
        // 检查头部是否有效
        if(header != NULL) {
            // 【处理ASLR偏移】
            // 计算去除ASLR偏移后的地址(文件中的原始虚拟地址)
            uintptr_t addressWSlide = address - (uintptr_t)_dyld_get_image_vmaddr_slide(iImg);
            
            // 获取第一个加载命令的地址
            uintptr_t cmdPtr = bs_firstCmdAfterHeader(header);
            
            // 如果命令指针无效,跳过这个镜像
            if(cmdPtr == 0) {
                continue;
            }
            
            // 【遍历该镜像的所有加载命令,查找段命令】
            // 段(segment)是内存中的一块连续区域,包含多个节(section)
            // 常见的段:
            //   - __TEXT: 代码段(只读、可执行)
            //   - __DATA: 数据段(可读写)
            //   - __LINKEDIT: 链接信息段(符号表、字符串表等)
            for(uint32_t iCmd = 0; iCmd < header->ncmds; iCmd++) {
                // 当前加载命令
                const struct load_command* loadCmd = (struct load_command*)cmdPtr;
                
                // 【处理32位段命令】
                if(loadCmd->cmd == LC_SEGMENT) {
                    // 转换为segment_command结构体
                    // segment_command包含:
                    //   - segname: 段名(如"__TEXT"、"__DATA")
                    //   - vmaddr: 虚拟内存地址
                    //   - vmsize: 虚拟内存大小
                    //   - fileoff: 文件偏移
                    //   - filesize: 文件大小
                    const struct segment_command* segCmd = (struct segment_command*)cmdPtr;
                    
                    // 【检查地址是否在该段的虚拟地址范围内】
                    // 条件:vmaddr <= address < vmaddr + vmsize
                    if(addressWSlide >= segCmd->vmaddr &&
                       addressWSlide < segCmd->vmaddr + segCmd->vmsize) {
                        // 找到了!返回镜像索引
                        return iImg;
                    }
                }
                // 【处理64位段命令】
                else if(loadCmd->cmd == LC_SEGMENT_64) {
                    // segment_command_64与segment_command结构类似
                    // 只是地址和大小字段是64位的
                    const struct segment_command_64* segCmd = (struct segment_command_64*)cmdPtr;
                    
                    // 检查地址是否在该段的范围内
                    if(addressWSlide >= segCmd->vmaddr &&
                       addressWSlide < segCmd->vmaddr + segCmd->vmsize) {
                        // 找到了!返回镜像索引
                        return iImg;
                    }
                }
                
                // 移动到下一个加载命令
                cmdPtr += loadCmd->cmdsize;
            }
        }
    }
    
    // 【没有找到】
    // 返回UINT_MAX表示失败
    // 这种情况很少发生,除非:
    //   1. 传入了无效地址(野指针、栈溢出等)
    //   2. 地址指向动态分配的内存(malloc分配的堆内存)
    //   3. 地址指向系统保留区域
    return UINT_MAX;
}

// 【获取镜像的段基地址】
// 段基地址用于将文件偏移转换为虚拟内存地址
// 公式:虚拟内存地址 = 段基地址 + 文件偏移
//
// 为什么需要段基地址?
// - 符号表、字符串表等信息存储在文件中,用文件偏移表示
// - 程序运行时需要访问这些信息,必须转换为内存地址
// - 段基地址 = __LINKEDIT段的虚拟地址 - 文件偏移
//
// 参数:idx - 镜像索引
// 返回:uintptr_t - 段基地址(0表示失败)
uintptr_t bs_segmentBaseOfImageIndex(const uint32_t idx) {
    // 获取镜像的Mach-O头部
    const struct mach_header* header = _dyld_get_image_header(idx);
    
    // 获取第一个加载命令的地址
    uintptr_t cmdPtr = bs_firstCmdAfterHeader(header);
    
    // 如果命令指针无效,返回0
    if(cmdPtr == 0) {
        return 0;
    }
    
    // 【遍历所有加载命令,查找__LINKEDIT段】
    // __LINKEDIT段(Link Edit Segment)包含:
    //   - 符号表(Symbol Table)
    //   - 字符串表(String Table)
    //   - 重定位信息(Relocation Info)
    //   - 代码签名(Code Signature)
    // 这些信息在运行时需要被dyld访问
    for(uint32_t i = 0; i < header->ncmds; i++) {
        // 当前加载命令
        const struct load_command* loadCmd = (struct load_command*)cmdPtr;
        
        // 【处理32位段命令】
        if(loadCmd->cmd == LC_SEGMENT) {
            // 转换为segment_command结构体
            const struct segment_command* segmentCmd = (struct segment_command*)cmdPtr;
            
            // strcmp: 字符串比较,相等返回0
            // SEG_LINKEDIT是系统定义的常量:"__LINKEDIT"
            if(strcmp(segmentCmd->segname, SEG_LINKEDIT) == 0) {
                // 【计算段基地址】
                // 段基地址 = 虚拟内存地址 - 文件偏移
                //
                // 为什么这样计算?
                // 假设__LINKEDIT段:
                //   - 文件中的偏移:0x4000
                //   - 加载到内存的地址:0x100004000
                //   - 段基地址 = 0x100004000 - 0x4000 = 0x100000000
                //
                // 那么,符号表在文件中的偏移是0x5000,其内存地址就是:
                //   0x100000000 + 0x5000 = 0x100005000
                return segmentCmd->vmaddr - segmentCmd->fileoff;
            }
        }
        // 【处理64位段命令】
        else if(loadCmd->cmd == LC_SEGMENT_64) {
            // segment_command_64与segment_command结构类似
            const struct segment_command_64* segmentCmd = (struct segment_command_64*)cmdPtr;
            
            // 查找__LINKEDIT段
            if(strcmp(segmentCmd->segname, SEG_LINKEDIT) == 0) {
                // 计算段基地址(64位版本,需要类型转换)
                return (uintptr_t)(segmentCmd->vmaddr - segmentCmd->fileoff);
            }
        }
        
        // 移动到下一个加载命令
        cmdPtr += loadCmd->cmdsize;
    }
    
    // 【没有找到__LINKEDIT段】
    // 返回0表示失败
    // 这种情况不应该发生,因为所有Mach-O文件都有__LINKEDIT段
    return 0;
}

@end

4-1.【协议导向编程】什么是协议导向编程(POP),它解决了传统面向对象设计中的哪些问题?

作者 项阿丑
2026年1月29日 11:05

1️⃣ 什么是协议导向编程(POP)

协议导向编程 是 Swift 提出的编程范式,它的核心思想是:

  • 通过协议(protocol)定义接口和行为,而不是依赖类继承。
  • 通过协议扩展(protocol extensions)提供默认实现,减少重复代码。
  • 关注行为组合而非继承层级,强调“能力”的组合而不是类的层级关系。

简单理解:

在 POP 中,我们关注对象 能做什么(行为),而不是它 是什么(类型/类继承)。


Swift 中的协议示例

protocol Flyable {
    func fly()
}

protocol Swimmable {
    func swim()
}

// 给 Flyable 提供默认实现
extension Flyable {
    func fly() {
        print("I can fly!")
    }
}

// 使用协议组合
struct Bird: Flyable, Swimmable {
    func swim() {
        print("I can swim too!")
    }
}

let duck = Bird()
duck.fly()   // 输出: I can fly!
duck.swim()  // 输出: I can swim too!

✅ 这里你可以看到:

  • 没有继承自基类。
  • 通过协议组合实现多种能力。
  • 默认实现避免重复代码。

2️⃣ POP 解决了传统 OOP 的哪些问题

在传统面向对象编程(OOP)中,我们通常使用 类继承 来复用代码:

class Animal { ... }
class Bird: Animal { ... }
class Duck: Bird { ... }

OOP 的局限

  1. 继承层级僵化

    • 类只能单继承(Swift 中类也只支持单继承)。
    • 行为复用受限。
    • 如果想让一个类同时拥有多种能力(fly、swim、run),继承树会变得复杂。
  2. 代码复用困难

    • 父类提供通用方法,但子类可能需要修改。
    • 多个不相关类要重复实现相同功能。
  3. 类型耦合强

    • 子类依赖父类,导致灵活性下降。
    • 改变父类可能影响整个继承链。

POP 的优势

问题 POP 的解决方案
单继承限制 协议可以任意组合,轻松实现多能力组合(多重“继承”效果)
代码重复 协议扩展提供默认实现,避免重复实现
耦合性强 类型依赖协议接口而非具体类,实现低耦合
灵活性差 POP 强调行为组合,可在不改变继承关系的前提下扩展功能
测试难度大 使用协议作为依赖(依赖注入),单元测试更容易模拟/mock

Swift POP 的典型实践

  1. 使用协议定义能力(Abilities)

    • Flyable, Swimmable, Runnable 等。
  2. 协议扩展提供默认实现

    • 避免每个类型都重复实现。
  3. 通过协议组合创建对象

    • 组合多种能力而不依赖继承层级。
  4. 依赖抽象而非具体类型

    • 代码更灵活,可测试性高。

总结

协议导向编程(POP) 的核心思想是:

“面向协议编程,而不是面向类编程”
通过协议组合和扩展,实现行为复用和灵活的类型组合,从而解决了传统 OOP 的继承僵化、代码重复、耦合度高等问题。

Swift 官方甚至强调:

Swift 是协议导向语言,而不是类继承语言

Neo-Cupertino 档案:撕开 Actor 的伪装,回归 Non-Sendable 的暴力美学

2026年1月29日 10:18

在这里插入图片描述

当前时间:2077年,一个阴雨连绵的周二 地点:Neo-Cupertino,第 42 区,“无限循环” 咖啡馆 人物

  • Jet:资深架构师,义眼闪烁着蓝光的代码老兵,热衷于复古的 Swift 语法。
  • Nova:刚入行的初级工程师,满脑子是最新的神经链接框架,但经常被编译器暴揍。
  • 反派“The Race Condition” (竞态幽灵),一个游荡在系统内存缝隙中的古老 AI 病毒,专门吞噬不安全的变量。

窗外的霓虹灯光透过雨幕,在 Jet 的合成皮风衣上投下斑驳的阴影。他抿了一口手中的高浓度咖啡因液,看着面前焦头烂额的 Nova。

“我的编译器又在尖叫了,” Nova 把全息屏幕推向 Jet,上面红色的错误提示像鲜血一样流淌,“我只是想在一个 Actor 里用个简单的类,结果 Swift 的并发检查像个疯狗一样咬着我不放。我是不是该把所有东西都加上 @MainActor 算了?”

在这里插入图片描述

Jet 叹了口气,那是见惯了无数次堆栈溢出后的沧桑。“滥用主线程隔离?那是饮鸩止渴。Nova,你被恐惧蒙蔽了双眼。来,看看这份 2025 年的加密文档。那时候我们管这个叫——Non-Sendable First Design。”

在本篇博文中,您将学到如下内容:

  • 🦾 序章:尴尬却必要的兴奋
  • 🧠 核心概念重载 (Quick Refresher)
  • 🔒 困兽之斗?不,是绝对领域 (Getting Stuck)
  • ⚖️ 权衡:光与影 (The Pros and Cons)
    • 💎 优势 #1:极致的简单 (Simplicity)
    • 🌐 优势 #2:通用性 (Generality)
    • 🕳️ 弱点 #1:启动任务的陷阱 (Starting Tasks)
  • 💊 并不完美的解药
  • 🛑 启动任务本身就是一种充满危险的诱惑
    • 🕸️ 弱点 #2:诡异的属性 (Weird Properties)
  • 🎬 终章:回归本源 (First for a Reason)

Jet 手指轻挥,将文档投影在两人中间,开始了他的解构。

在这里插入图片描述


🦾 序章:尴尬却必要的兴奋

Jet 指着文档的开头说道:“这作者是个老实人。他承认自己对 Non-Sendable Types (非跨域传输类型) 的痴迷程度简直到了走火入魔的地步,甚至有点尴尬。”

“这就好比给自己的战术命名一样,听起来有点自以为是。但他把这种设计理念称为 ‘Non-Sendable First Design’。虽然名字听起来像是什么二流黑客的代号,但其核心思想在当年可是振聋发聩。”

在这里插入图片描述

“在那个年代,因为语言特性的限制,大家对 Non-Sendable 类型避之唯恐不及。它们虽然有用,但用起来就像是在满是碎玻璃的地上跳舞——人体工程学极差。直到有一天,NonisolatedNonsendingByDefault(即 Swift 6 中的‘可接近并发’特性)横空出世。这一切都变了!Non-Sendable 类型突然变得顺滑无比,仿佛这才是它们原本的宿命。”


🧠 核心概念重载 (Quick Refresher)

“在深入之前,我们要先进行一次思维格式化。” Jet 的义眼转动,调出了基础理论图谱。

Swift 解决 Data Race (数据竞争) 的手段非常硬核:它要求在编译时就确立对非线程安全数据的保护。这种抽象被称为 Isolation (隔离)

  • 实现层:可能是锁 (Lock)、队列 (Queue),或者是专用的线程。
  • 运行时:由 Actor 负责具体的保护机制。

“编译器其实是个‘脸盲’,” Jet 解释道,“它不知道某个 Actor 到底是怎么运作的(比如 MainActor 其实是把任务倒进主线程这个大漏斗里),它只知道一点:Actor 能护犊子。”

在这里插入图片描述

Swift 将数据世界一分为二:

  1. Sendable (可传输类型):天生的战士,线程安全,可以在并发的枪林弹雨中随意穿梭,无需保护。
  2. Non-Sendable (非可传输类型):共享的可变状态。它们是我们程序中最有趣、最核心的部分,比如你的用户数据、缓存状态。但它们也是脆弱的,就像没有穿护甲的平民,必须被保护。

“很多人误以为 Actor 只是用来‘后台运行’的工具,这完全是买椟还珠。Actor 的真正使命,是充当 Non-Sendable 数据的保镖,防止它们被竞态幽灵吞噬。”


🔒 困兽之斗?不,是绝对领域 (Getting Stuck)

“听着,Nova。世界上最好的安保系统,如果不允许任何人进出,那也没用。但在 Swift Concurrency 的法则里,有一个非常有趣的特性:”

如果一个 Actor 拥有(通常是创建)了一个 Non-Sendable 类型,这个类型就被‘困’住了。

“想象一下,” Jet 描绘道,“一个由类和协议组成的庞大网络,彼此交织,协同工作。它们可以在 Actor 的围墙内为所欲为。但编译器这个冷酷的守门人,绝对禁止你不安全地将它们扔到墙外。”

在这里插入图片描述

“这听起来像是限制,但这正是 Non-Sendable 类型的强大之处——画地为牢,反而成就了绝对的安全。”


⚖️ 权衡:光与影 (The Pros and Cons)

Jet 调出了对比数据面板。“作者曾纠结于如何展示这些,最后他决定返璞归真,先给你看甜头,再告诉你陷阱。注意,这里默认的语境是 nonisolated (非隔离) 的。”

💎 优势 #1:极致的简单 (Simplicity)

“看这段代码,Nova。它简单得像是一首儿歌。”

class Counter {
    var state = 0
    
    func reset() {
        self.state = 0
    }
}

“这就是一个普通的类,甚至还有可变状态。再看这一步:”

class Counter {
    // ...

    // 这里加上了 async
    func toggle() async {
        self.state += 1
    }
}

extension Counter: Equatable {
    static func == (lhs: Counter, rhs: Counter) -> Bool {
        lhs.state == rhs.state
    }
}

Jet 敲着桌子强调:“这里有两个关键点!”

  1. Async 方法:在旧时代,nonisolated + async 意味着代码会跑去后台线程,但这对于 Non-Sendable 类型来说是个悖论(它不能离开 Actor 的保护)。这曾经是个第22条军规式的死锁。但现在,有了 NonisolatedNonsendingByDefault,这个问题迎刃而解。
  2. 协议一致性 (Protocol Conformance):看那个 Equatable。这对于普通类来说易如反掌。但如果是 MainActor 隔离的类型?那简直是噩梦,你得处理各种隔离上下文的匹配问题。

Non-Sendable 类型拥有隔离类型所不具备的纯粹和简单。

在这里插入图片描述

🌐 优势 #2:通用性 (Generality)

“这需要一点悟性,” Jet 眯起眼睛,“这关乎同步访问 (Synchronous Access)。”

“如果你给类型加上了 @MainActor,那只有主线程的朋友才能同步访问它。这就像是个 VIP 俱乐部。但 Non-Sendable 类型是通用的雇佣兵。”

actor ActorClient {
    // ActorClient 拥有这个 counter
    private let counter = Counter()
    
    func accessSynchronously() {
        // ✅ 这是完全可能的!
        // 因为 counter 是 Non-Sendable,它被"困"在了 ActorClient 的隔离域内,
        // 所以 ActorClient 可以像操作自家后院一样同步操作它。
        counter.reset() 
    }
}

“它的接口保持不变,不管是谁‘拥有’它。这就是海纳百川的通用性。”

在这里插入图片描述


🕳️ 弱点 #1:启动任务的陷阱 (Starting Tasks)

此时,全息投影变成了警告的红色。

“这就是那个让我开始反思的地方,” Jet 指着下面的代码,“也是新手最容易踩的雷区。”

class Counter {
    // ...

    func printStateEventually() {
        Task {
            // ❌ 错误: 将闭包作为 'sending' 参数传递...
            // 编译器会阻止你,因为你试图在新的 Task 里捕获非 Sendable 的 self
            print(self.state)
        }
    }
}

“为什么不编译?把它翻译成古老的 GCD 你就懂了:”

// ⚠️ 警告: 在 @Sendable 闭包中捕获了 non-Sendable 类型 'Counter'
DispatchQueue.global().async {
    print(self.state)
}

“这就像你在没有任何保护措施的情况下,试图把一个易碎的花瓶扔到正在高速运转的传送带上。在一个 nonisolated 的函数里启动 Task,意味着上下文是不确定的。没有线程安全的保证,编译器绝不会放行。

在这里插入图片描述

💊 并不完美的解药

“为了解决这个问题,我们要么回到原来的队列(像 GCD 那样),要么把 isolation 参数传进去。”

class Counter {
    // 显式传递隔离上下文,这代码丑得像被辐射过的变异体
    func printStateEventually(isolation: isolated any Actor) {
        Task {
            _ = isolation // 这里的魔法是为了继承隔离域
            print(self.state)
        }
    }
}

“这方案有毒,” Jet 摇摇头,“第一,它有传染性,调用者必须提供 Actor。第二,这语法太啰嗦。最重要的是……”

在这里插入图片描述

🛑 启动任务本身就是一种充满危险的诱惑

“Jet 突然严肃起来:“有人曾告诉我,不应该在这种类型内部创建非结构化的 Task。以前我觉得那是废话,现在我觉得那是金玉良言。”

“在类型内部悄悄启动一个外部无法等待 (await) 的任务,这是埋雷。这让代码变得难以观测、难以测试。这其实是一种语法盐 (Syntactic Salt)——语言故意让你难受,是为了告诉你:别这么干!

“正确的做法是使用 async 方法,保持在结构化并发的世界里。如果非要开新任务,让调用者去开。”

在这里插入图片描述


🕸️ 弱点 #2:诡异的属性 (Weird Properties)

“还有一些边缘情况,” Jet 快速带过,“比如 lazy var、属性包装器 (Property Wrappers) 或者宏 (Macros)。如果在这些东西里混合复杂的并发要求,你会发现自己进退维谷。”

class Counter {
    // 这种延迟加载在并发环境下可能极其复杂
    lazy var internal = {
        SpecialCounter(isolatedParam: ???) 
    }()
}

“这虽然罕见,但一旦遇到,就像是撞上了隐形墙。前车之鉴,不可不防。”

在这里插入图片描述


🎬 终章:回归本源 (First for a Reason)

雨渐渐停了,Neo-Cupertino 的黎明即将来临。Jet 关闭了全息投影,看着若有所思的 Nova。

“‘Non-Sendable First’,不是让你永远只用这一招,而是把它作为起点。”

在这里插入图片描述

“在 Swift 5.5 之前,所有东西本质上都是 nonisolated 的。这才是‘常态’。只要不涉及跨线程,它们就是最轻量、最高效的选择。”

“当然,当你遇到真正的并发需求,当你发现无法在 Actor 之间安全传递数据时,再考虑加上 SendableActor 约束。但在那之前……” Jet 站起身,整理了一下衣领。

不要为了并不存在的并发问题,去背负隔离带来的沉重枷锁。 利用 Region Isolation (区域隔离) 的特性,让编译器帮你推导安全性。这种感觉就像是作弊,但却是合法的。”

在这里插入图片描述

Nova 看着屏幕上终于变绿的编译通过提示,眼中闪过一丝光芒。“所以,大道至简?”

“没错,” Jet 转身走进晨雾中,留下最后一句话,“如果觉得复杂,说明你走错了路。Non-Sendable 的世界,简单得让人上瘾。”

在这里插入图片描述

赛博深渊(上):用 Apple Foundation Models 提炼“禁忌知识”的求生指南

2026年1月29日 10:16

在这里插入图片描述

🍎 引子

新九龙城的雨从未停过。霓虹灯的废气在湿漉漉的街道上晕染开来,像极了那个死于代码过载的倒霉蛋老王流出的脑浆。

在贫民窟第 404 区的一间昏暗安全屋里,一名代号为“老 K”的黑客正对着一块发着幽蓝光芒的屏幕,手指在键盘上敲击出残影。墙角的服务器风扇发出濒死的哀鸣,仿佛随时都会起飞爆炸。

在这里插入图片描述

在这个被巨型企业“果核公司(Fruit Corp)”统治的赛博世界里,数据就是生命,而算力就是货币。

老 K 刚刚从公司的主机里偷出了一份代号为“创世纪”的加密文本,文件大得惊人,如果不赶紧在本地进行 “摘要提炼”,追踪程序(那些被称为“猎犬”的 AI)顺着网线摸过来,把他那可怜的脑机接口烧成灰烬只是时间问题。

在本次冒险中,您将学到如下内容:

  • 🍎 引子
  • 🧠 缩水的“大脑”与本地化的艺术
  • 🛠️ 装备检查:不要试图在烤面包机上运行核程序
  • 🕵️‍♂️ 侦测可用性:敌我识别系统
  • 💉 注入代码:瞒天过海
  • 🎭 模拟测试:在母体中演练
  • 🎆 尾声:黎明前的微光

但他不能上传到云端处理。云端是“它们”的地盘。他必须在本地,用那个刚刚解禁的传说级武器——Apple Foundation Models


🧠 缩水的“大脑”与本地化的艺术

“听着,菜鸟,”老 K 转过头,仿佛打破了第四面墙,对着作为学徒的你说道,“想要活命,就得学会如何在端侧(On-device)跑大模型。”

Apple Foundation Models 提供了一种能在用户终端设备(不管是你的义体植入终端,还是手里的 iPhone/Mac)上本地运行的大语言模型(LLM)

在这里插入图片描述

你要知道,传统的 LLM 就像是住在数据中心里的巨型怪兽,吃的是高功率 GPU,喝的是海量的显存和电力。想要把这种怪兽塞进你的口袋里,不仅需要勇气,还需要黑科技。

这里有两个关键的 “瘦身” 魔法:

  1. 参数削减 (Reducing Parameters):把脑子里那些没用的神经元切掉,只保留核心逻辑。
  2. 模型量化 (Quantizing Model Values):如果说原来每个神经元需要 32 位浮点数的高精度,现在我们把它压缩到 4 位或 8 位。虽然精度略有下降,但体积小到可以忽略不计。

在这里插入图片描述

⚠️ 风险提示:这些被“阉割”过的模型依然保留了 LLM 的通病——幻觉 (Hallucinations)。它可能会一本正经地胡说八道,就像喝了假酒的算命先生。但在提炼摘要、理解文本、甚至简单的数据生成任务上,它依然是把好手。

好了,废话少说,让我们开始改装你的代码,让它能驾驭这股力量。


🛠️ 装备检查:不要试图在烤面包机上运行核程序

想要驾驭 Apple Intelligence,你的硬件得跟得上。

“别拿你那台老古董来丢人现眼。”老 K 吐了一口烟圈。

硬性指标:

  • 操作系统:必须是 macOS 26.0 或更高版本(对于 iOS 设备,则是 iOS 26/iPadOS 26)。
  • 硬件支持:设备必须原生支持 Apple Intelligence(那种山寨的义体插件不仅没用,还会炸)。
  • 开关状态:用户必须在设置里手动开启了 Apple Intelligence。

在这里插入图片描述

☠️ 避坑指南:如果你在 macOS 26 的虚拟机里折腾,大概率会翻车。Apple Foundation Models 对虚拟化环境过敏。找台真机,或者支持该特性的物理设备。

在这里插入图片描述


🕵️‍♂️ 侦测可用性:敌我识别系统

我们的目标是那个 Share Extension(分享扩展)。我们要让它变成一个能吞噬长文本并吐出精华摘要的黑洞。

在这里插入图片描述

现在的 App 像个傻子,只会把你选中的文字原样复读一遍。我们要改造它。

首先,在 SummarizeExtension 文件夹下创建一个新的 SwiftUI 视图,命名为 ModelCheckView.swift。这就像是我们的看门狗,用来检测当前环境是否安全。

在这里插入图片描述

在该文件顶部引入这一行禁忌的咒语:

import FoundationModels // 引入 Apple 的本地模型库

然后在结构体里加入这些属性:

// 这里的 sharedText 是我们要处理的“赃物”(文本)
let sharedText: String
// 任务完成后的回调,毕竟我们得知道什么时候跑路
let onDone: () -> Void
// 核心:获取系统默认的语言模型实例
let model = SystemLanguageModel.default 

接下来,把原本的 body 替换成这一段充满求生欲的代码:

// 1. 检查模型的可用性状态
switch model.availability {

// 2. 状态:可用。谢天谢地,我们可以干活了
case .available:
  // 这里调用真正干活的摘要视图(后面我们会讲)
  SummaryView(sharedText: sharedText, onDone: onDone)

// 3. 各种不可用的“死法”
case .unavailable(.deviceNotEligible):
  Text("⚠️ 你的设备太老了,跑不动 Apple Intelligence。")
  
case .unavailable(.appleIntelligenceNotEnabled):
  Text("🛑 Apple Intelligence 虽已就绪,但你还没打开开关。快去设置!")
  
case .unavailable(.modelNotReady):
  Text("⏳ 模型还没热身完毕,稍后再试。") 
  // 注意:在模拟器里看到这个,通常意味着你的宿主机不支持,或者你在虚拟机里套娃。

// 4. 未知错误,最可怕的一种
case .unavailable:
  Text("👾 未知错误阻止了 Apple Intelligence 的运行。可能是赛博幽灵作祟。")
}

在这里插入图片描述

老 K 的技术旁白

  • model.availability:这是你的盖革计数器。一定要先 switch 它,不然直接调用模型会导致程序崩溃,就像在没有氧气的地方点火一样。
  • unavailable 的各种姿势:一定要给用户(或者你自己)清晰的反馈。与其让 App 闪退,不如告诉他“你的装备不行”。

在这里插入图片描述


💉 注入代码:瞒天过海

现在,我们要把这个检测机制植入到 ShareViewController.swift 里。这个文件是连接古老的 UIKit 世界和新锐 SwiftUI 世界的桥梁。

找到 showSwiftUIView(with:) 方法,我们要玩一招“偷梁换柱”。

// 用我们新的 ModelCheckView 替换掉原来的直接调用
let wvc = UIHostingController(
  rootView: ModelCheckView(
    sharedText: text,
    onDone: closeExtension // 事情办完就销毁现场
  )
)

这样,当你在 Safari 里选中一段长得令人发指的文本,点击“分享 -> LocalSummarizer”时,系统会先经过我们的 ModelCheckView 查岗。

在这里插入图片描述


🎭 模拟测试:在母体中演练

“别急着上线,现在的网络全是眼线。”老 K 按住你的手,“先在 Xcode 的模拟环境中跑一遍。”

即使你的设备牛逼哄哄,你也得测试一下如果用户没开功能会怎样。

在这里插入图片描述

  1. 在 Xcode 顶部选择 SummarizeExtension 方案。
  2. 点击 Edit Scheme...
  3. Run -> Options 标签页下,找到 Simulated Foundation Models Availability

在这里插入图片描述

这就是你的上帝模式开关。你可以把它设为:

  • Apple Intelligence Not Enabled:看看你的 App 会不会乖乖提示用户去开启。
  • Off:这是默认值,反映你真实设备的状态。

在这里插入图片描述

⚠️ 警告:测完记得把它改回 Off。别到时候明明设备支持,却因为这里没改回来而对着屏幕怀疑人生,那种感觉就像是找了半天眼镜结果眼镜架在鼻子上一样蠢。

在这里插入图片描述

为了让主 App (LocalSummarizer) 也能自检,我们顺手改一下 ContentView.swift

import FoundationModels

// ... 在 body 里 ...
switch SystemLanguageModel.default.availability {
case .available:
  Text("✅ 此设备已准备好接入 Apple Foundation Models。")
// ... 其他 case 照抄上面的 ...
}

在这里插入图片描述


🎆 尾声:黎明前的微光

老 K 敲下最后一个回车键。屏幕闪烁了一下,那段数百万字的冗长文本,瞬间被 Apple Foundation Models 压缩成了寥寥几百字的精华摘要。

没有联网,没有上传,一切都在悄无声息中完成。

在这里插入图片描述

“搞定了。”老 K 拔掉数据线,嘴角露出一丝不易察觉的微笑,“猎犬们还在云端嗅探我们的踪迹,殊不知我们已经带着情报,大摇大摆地从它们眼皮子底下溜走了。”

在这里插入图片描述

窗外的雨似乎小了一些。在这个被数据淹没的时代,掌握本地大模型技术,不仅仅是为了开发几个花哨的功能,更是为了在这个监视无处不在的赛博丛林里,保留最后一份隐私和自由。

在这里插入图片描述

你看着手里显示着 "Summary Generated" 的屏幕,深知这只是开始。下一章,我们将利用这股力量,去触碰更深层的禁忌……

(未完待续...)

NSProcessInfoThermalState 的作用

作者 iOS在入门
2026年1月28日 22:59

借助AI辅助。

NSProcessInfoThermalState 的作用

NSProcessInfoThermalState 是一个用于表示设备散热状态的枚举类型。它帮助应用程序了解设备当前的温度水平,从而采取适当的措施来防止设备过热。

枚举值及含义

这个枚举包含四个级别:

  1. Nominal(正常) - 设备温度处于可接受水平,对用户无负面影响

  2. Fair(一般) - 设备温度略有升高,风扇可能开始工作并发出声音

  3. Serious(严重) - 设备温度明显升高,风扇全速运转,系统性能可能受到影响

  4. Critical(临界) - 设备温度显著升高,设备需要冷却降温

使用方式

// 获取当前散热状态
let thermalState = ProcessInfo.processInfo.thermalState

// 监听散热状态变化通知
NotificationCenter.default.addObserver(
    forName: ProcessInfo.thermalStateDidChangeNotification,
    object: nil,
    queue: nil
) { notification in
    // 处理散热状态变化
}

应用场景

当散热状态升高时,应用程序应该采取措施降低系统负载:

  • 减少 CPU/GPU 使用率
  • 降低帧率
  • 减少 I/O 操作
  • 使用低质量的视觉效果
  • 暂停后台任务

这对于保护设备硬件、提升用户体验(避免设备过热导致的性能下降或风扇噪音)非常重要,特别是在进行密集计算或图形渲染的应用中。

昨天以前掘金 iOS

3-27.【函数式编程】讨论 Combine 的 Publisher 如何体现 Functor / Monad 特性。

作者 项阿丑
2026年1月28日 17:00

1️⃣ Functor 特性在 Publisher 中的体现

Functor 核心:能够把函数映射到容器/上下文中的值,同时保持结构不变。

在 Combine 中:

  • Publisher 可以看作一个 容器,异步发出值
  • 使用 map 可以对 Publisher 发出的每个元素进行函数映射
  • 不改变 Publisher 的结构,只改变输出值

示例

import Combine
import Foundation

let publisher = [1, 2, 3].publisher // ArrayPublisher<Int>

let mapped = publisher.map { $0 * 2 }

let cancellable = mapped.sink { value in
    print("Mapped value:", value)
}
// 输出:Mapped value: 2
//       Mapped value: 4
//       Mapped value: 6
  • map 只改变 输出值,不改变 Publisher 的类型
  • 多次 map 可组合,满足 Functor 规律:
let f: (Int) -> Int = { $0 + 1 }
let g: (Int) -> Int = { $0 * 10 }

let left = publisher.map(f).map(g)
let right = publisher.map { g(f($0)) }
  • left == right ✅ 组合律满足

2️⃣ Monad 特性在 Publisher 中的体现

Monad 核心:支持 bind(flatMap)操作,能够把返回容器的函数平铺,避免嵌套容器。

在 Combine 中:

  • flatMap 对应 Monad 的 bind
  • 输入:(Output) -> Publisher<NewOutput, Failure>
  • 输出:单一 Publisher,把可能产生的多个嵌套 Publisher 平铺
  • 支持链式组合多个异步操作

示例

假设我们有两个异步 Publisher 函数:

func fetchNumber(_ id: Int) -> AnyPublisher<Int, Never> {
    Just(id * 2).eraseToAnyPublisher()
}

func fetchString(_ number: Int) -> AnyPublisher<String, Never> {
    Just("Number is (number)").eraseToAnyPublisher()
}

// 使用 flatMap 链式组合
let cancellable2 = [1, 2, 3].publisher
    .flatMap { fetchNumber($0) }   // Monad bind,平铺 Publisher
    .flatMap { fetchString($0) }
    .sink { print($0) }

解释

  1. [1,2,3].publisher → Publisher

  2. .flatMap(fetchNumber) → Publisher

    • 每个值会生成一个新的 Publisher
    • flatMap 平铺输出,而不是产生嵌套 Publisher<Publisher>
  3. .flatMap(fetchString) → Publisher

  4. 最终输出一个 扁平 Publisher,可以订阅


3️⃣ Functor vs Monad 对比在 Combine 中

特性 Functor (map) Monad (flatMap)
操作类型 (Output) -> NewOutput (Output) -> Publisher<NewOutput, Failure>
结构变化 不改变 Publisher 层级 平铺嵌套 Publisher
适用场景 对值做同步转换 异步链式调用,生成新的 Publisher
示例 .map { $0 * 2 } .flatMap { fetchNumber($0) }

4️⃣ 总结

  1. Functor

    • Publisher.map → 映射输出值,保持 Publisher 结构
    • 遵循恒等和组合律
  2. Monad

    • Publisher.flatMap → 绑定新的 Publisher,平铺嵌套,短路失败可处理
    • 支持函数式异步流水线,链式组合

💡 核心思想

Combine 的 Publisher 就是一个 异步容器,Functor 和 Monad 的特性保证了:

  1. 可以安全映射数据 (map)
  2. 可以安全组合异步操作 (flatMap)
  3. 异步值处理保持函数式风格,副作用集中在 sinkassign

2-6.【Concurrency】TaskGroup 的设计目的是什么?它如何保证并发安全?

作者 项阿丑
2026年1月28日 14:11

一句话先给结论

TaskGroup 的设计目的,是在保持结构化并发的前提下,
安全地表达“一组并发执行、统一收敛结果的子任务”。

或者更狠一点的说法:

TaskGroup = “不会失控的 fork–join 并发”。


1️⃣ 为什么需要 TaskGroup?(设计动机)

先看一个“天真实现”的问题:

var results: [Int] = []

Task {
    Task { results.append(await f1()) }
    Task { results.append(await f2()) }
    Task { results.append(await f3()) }
}

❌ 问题一堆:

  • results 有数据竞争
  • 子任务生命周期不受控
  • 取消无法统一传播
  • 无法确定什么时候全部完成

TaskGroup 想解决的正是这些问题

let sum = await withTaskGroup(of: Int.self) { group in
    group.addTask { await f1() }
    group.addTask { await f2() }
    group.addTask { await f3() }

    var total = 0
    for await value in group {
        total += value
    }
    return total
}

2️⃣ TaskGroup 的核心设计目标

🎯 目标 1:结构化并发(Structured Concurrency)

  • 所有子任务:

    • 必须在 group 作用域内完成
    • 不能逃逸
  • 离开 withTaskGroup

    • 要么所有完成
    • 要么全部取消

👉 不会“偷偷活着的任务”


🎯 目标 2:安全地共享结果,而不是共享状态

TaskGroup 的哲学是:

并发任务之间不共享可变状态,只通过结果汇合

  • 子任务:

    • 不写共享变量
    • return
  • 父任务:

    • 串行地消费结果

🎯 目标 3:明确的并发边界

  • 并发只发生在:

    • group.addTask
  • 收敛点是:

    • for await value in group

这让代码在语义上可推理


3️⃣ TaskGroup 的工作模型(非常重要)

逻辑结构

Parent Task
 └─ TaskGroup
     ├─ Child Task 1
     ├─ Child Task 2
     └─ Child Task 3

特点:

  • 子任务都是:

    • 当前 task 的子 task
    • 自动继承取消 / 优先级 / actor

生命周期规则(强约束)

行为 结果
父 task 被取消 group 中所有子任务被取消
任一子任务抛错(throwing group) 其余子任务全部取消
离开作用域 等待所有子任务完成

4️⃣ TaskGroup 如何保证并发安全?

这是你问题的核心 👇


✅ 1. 类型系统层面的隔离

withTaskGroup(of: Int.self) { group in
    group.addTask {
        // 只能返回 Int
    }
}
  • 子任务无法直接访问 group 的内部状态
  • 只能通过 返回值通信

✅ 2. 串行消费结果

for await value in group {
    // 这里是顺序执行的
}
  • 即使子任务并发完成
  • 结果的处理是单线程、顺序的
  • 所以你可以安全地:
var sum = 0
sum += value

✅ 3. 作用域限制(无法逃逸)

你做不到:

var g: TaskGroup<Int>?
withTaskGroup(of: Int.self) { group in
    g = group // ❌ 不允许
}
  • group 不能被存储
  • 不能被跨作用域使用

👉 这从根源上消灭了悬垂并发。


✅ 4. 自动取消传播

  • 父取消 → 子取消
  • 错误 → 全部取消

这避免了:

  • 子任务写了一半共享状态
  • 父任务已经不关心结果

✅ 5. 与 actor 隔离完美配合

actor Store {
    func loadAll() async {
        await withTaskGroup(of: Item.self) { group in
            for id in ids {
                group.addTask {
                    await fetchItem(id)
                }
            }
            for await item in group {
                self.items.append(item) // actor 内,安全
            }
        }
    }
}
  • 子任务不直接修改 actor 状态
  • 所有 mutation 都在 actor 上顺序执行

5️⃣ TaskGroup vs async let(顺便对比)

特性 TaskGroup async let
子任务数量 动态 静态
结果处理 流式 一次性
错误处理 可逐个 一起
适合场景 批量 / 不定量 少量固定

6️⃣ 常见误区(非常常见)

❌ 误区 1:TaskGroup 里共享变量是安全的

var sum = 0
withTaskGroup(of: Int.self) { group in
    group.addTask { sum += 1 } // ❌ 数据竞争
}

子任务仍然是并发的。


❌ 误区 2:TaskGroup 会限制并发数量

不会。
如果你要限流,需要:

withTaskGroup { group in
    // 自己控制 addTask 的节奏
}

或使用 semaphore / AsyncSemaphore。


7️⃣ 终极总结(可以直接背)

TaskGroup 的设计目的,是在结构化并发模型下,
安全地表达“多个并发子任务 + 统一收敛”的计算模式。
它通过作用域约束、结果传递而非状态共享、串行消费、自动取消传播,
从语言和类型系统层面避免了数据竞争和失控并发。


2-5.【Concurrency】Task 和 DetachedTask 有什么区别?使用场景各是什么?

作者 项阿丑
2026年1月28日 14:11

一句话总览

Task {} = 有父任务、会继承上下文的结构化并发
Task.detached {} = 没有父任务、完全独立的非结构化并发


1️⃣ 最重要的区别(先看这个)

维度 Task {} Task.detached {}
是否有父 Task ✅ 有 ❌ 没有
继承取消状态
继承优先级
继承 Task-local
继承 actor / executor
结构化并发
推荐使用 ⭐⭐⭐⭐⭐ ⭐⭐(谨慎)

99% 的情况下,你应该用 Task {}


2️⃣ Task {} 到底是什么?

定义

Task {
    await doSomething()
}

这是一个 child task(子任务)

它会继承什么?

假设你在这里:

@MainActor
func onTap() {
    Task {
        await loadData()
    }
}

这个 Task 会继承:

  • 当前 Task 的取消状态
  • 当前 优先级
  • 当前 Task-local 值
  • 当前 actor(MainActor)

所以:

loadData() // 默认仍在 MainActor

除非你 await 到别的 executor。


为什么这很重要?

因为它保证了:

  • 生命周期清晰
  • 取消可以向下传播
  • 行为可预测

👉 这就是“结构化并发”


3️⃣ Task.detached {} 是什么?

定义

Task.detached {
    await doSomething()
}

这是一个 完全独立的任务

特点(非常关键):

  • ❌ 不属于当前 Task
  • ❌ 不继承 MainActor
  • ❌ 不继承优先级
  • ❌ 不继承取消
  • ❌ 不继承 Task-local

相当于:

“在并发世界里新开了一个孤儿线程(逻辑上)”


4️⃣ 一个最容易踩坑的例子 ⚠️

@MainActor
func onTap() {
    Task.detached {
        updateUI() // ❌ 运行期错误
    }
}

原因:

  • detached task 不在 MainActor
  • 却访问了 MainActor 隔离的状态

必须显式切回:

Task.detached {
    await MainActor.run {
        updateUI()
    }
}

5️⃣ 取消(cancellation)语义差异

Task {}

let task = Task {
    try await longWork()
}

task.cancel()
  • 子任务会被标记为 cancelled
  • await 点会抛 CancellationError
  • 和父任务强关联

Task.detached {}

let task = Task.detached {
    try await longWork()
}
  • 不会被父任务取消
  • 必须手动管理生命周期
  • 很容易“偷偷活很久”

6️⃣ 什么时候该用 Task {}?(几乎所有时候)

✅ UI 事件响应

Button("Load") {
    Task {
        await viewModel.load()
    }
}

✅ actor 内部并发工作

actor Cache {
    func refresh() {
        Task {
            await self.loadFromDisk()
        }
    }
}

✅ 需要自动取消、优先级继承


7️⃣ 什么时候才该用 Task.detached {}

⚠️ 使用条件(缺一不可)

真的需要:

  1. 不继承任何上下文
  2. 生命周期独立于调用者
  3. 手动处理取消 / actor hop

合理场景 1️⃣:全局后台维护任务

Task.detached(priority: .background) {
    await cleanTempFiles()
}
  • 不该被 UI 取消
  • 明确是系统级任务

合理场景 2️⃣:跨 actor / 跨 subsystem 的桥接

Task.detached {
    let data = await fetch()
    await logger.log(data)
}

合理场景 3️⃣:底层库内部(而不是 App 代码)

Swift 官方建议:

Task.detached 主要用于框架 / runtime 层


8️⃣ 官方态度(很明确)

Swift Concurrency 的设计哲学是:

默认结构化并发,尽量消灭 detached task

你可以理解成:

  • Task {} = safe default
  • Task.detached {} = unsafe escape hatch

9️⃣ 快速记忆口诀 🧠

要继承上下文,用 Task
要彻底断亲,用 Task.detached
不确定?别 detached


10️⃣ 最后一段总结(面试版)

Task {} 创建的是结构化的子任务,会继承当前任务的取消、优先级、actor 和 task-local,适合绝大多数并发场景;
Task.detached {} 创建的是非结构化任务,不继承任何上下文,生命周期完全独立,适合极少数系统级或框架级后台工作,普通业务代码应尽量避免。

2-4.【Concurrency】Continuation 是什么?与 async/await 有何关系?

作者 项阿丑
2026年1月28日 14:11

在 Swift(乃至整个并发世界)里:

如果你真正理解了 continuation,你就理解了 async/await 的底层本质。

我会按 概念 → Swift 里的具体形态 → 与 async/await 的关系 → 常见误区 来讲。


一句话定义(先刻在脑子里)

Continuation =「从当前点开始,程序接下来该怎么继续执行」的一个可保存、可传递、可稍后调用的对象。

换句话说:

continuation 就是 “未来的自己”


1️⃣ 直观理解(不用术语)

看这段代码:

let x = await foo()
print(x)

await foo() 这一行:

  • 当前函数不能继续往下跑
  • 但“等 foo 完成之后该干什么”是完全确定的

这段“之后该干什么”的逻辑:

print(x)

👉 这就是一个 continuation


2️⃣ continuation 在 Swift 中到底是什么?

在 Swift Concurrency 里,continuation 不是抽象概念,而是真实存在的东西

在编译器 / runtime 层面

continuation 由以下几部分组成:

  1. resume function

    • 一个函数指针
    • 指向“await 之后的代码块”
  2. async context

    • 保存局部变量、状态机状态
  3. executor 信息

    • 决定在哪里恢复执行

合在一起:

continuation = (async context + resume entry + executor)


3️⃣ async/await 和 continuation 的关系(核心)

❓ async/await 做了什么?

async/await 的本质就是:
把 continuation 自动、隐式地帮你创建和管理了。


不用 async/await(手写 continuation)

func foo(_ cont: @escaping (Int) -> Void) {
    bar { result in
        cont(result + 1)
    }
}

你在手动传递 continuation


用 async/await

func foo() async -> Int {
    let x = await bar()
    return x + 1
}

编译器在背后自动做了:

  • 创建 continuation
  • 保存当前上下文
  • 把 continuation 传给 bar
  • 在合适的 executor 上 resume

👉 你写的是顺序代码,底层仍然是 continuation。


4️⃣ withUnsafeContinuation 是什么角色?

这是 Swift 暴露给用户的 continuation API

func legacy() async -> Int {
    await withCheckedContinuation { cont in
        legacyAPI { value in
            cont.resume(returning: value)
        }
    }
}

这里:

  • cont 就是当前 async 函数的 continuation

  • 调用 resume

    • 就等于“触发 await 后面的代码继续执行”

⚠️ 注意:

  • resume 只能调用一次
  • 否则 continuation 会崩(checked 版会直接 trap)

5️⃣ continuation 与“状态机”的关系

continuation ≠ 状态机
continuation = 状态机的一个“入口点”

  • 状态机:

    • 决定你现在在哪个状态
  • continuation:

    • 是“从这个状态继续跑”的 callable handle

你可以把它理解为:

state machine + continuation = coroutine

6️⃣ continuation 与线程的关系(常见误解)

❌ 错误理解

continuation = 保存当前线程

✅ 正确理解

continuation 不保存线程,只保存“逻辑上的下一步”。

  • 恢复时:

    • 可能在同一个线程
    • 也可能在完全不同的线程
  • 线程由 executor 决定


7️⃣ continuation 在错误、取消中的作用

错误传播

await foo() // throws
  • continuation 包含:

    • normal resume
    • error resume

cancellation

  • task 被取消时

  • runtime 会:

    • 找到 continuation
    • 恢复执行
    • 抛出 CancellationError

8️⃣ 和其他语言的对比(帮助定位)

语言 continuation 表现
Swift 隐式 + 显式(withContinuation)
Kotlin Continuation<T> 参数
JS Promise.then
C++20 coroutine_handle
Scheme call/cc

Swift 的设计目标是:

让 99% 的 continuation 消失在语法糖后面


9️⃣ 终极记忆版总结

Continuation 是“await 之后要做什么”的可执行表示。
async/await 的全部魔法,就是把 continuation 自动保存、传递、恢复。


1-30.【GCD】GCD 如何缓解优先级反转?开发者还能做什么?

作者 项阿丑
2026年1月28日 11:31

1️⃣ GCD 内部缓解机制:QoS 继承

1.1 原理

  • 问题来源

    • 高 QoS 任务等待低 QoS 队列中的任务或锁
    • 如果低 QoS 任务占用线程 → 高 QoS 任务被延迟
    • 典型场景:串行队列 + 低 QoS 在前 + 高 QoS 在后
  • GCD 解决方案

    1. 高 QoS 任务提交到队列时,系统检测 任务阻塞
    2. 临时提升低 QoS 任务到高 QoS
    3. 保证低 QoS 任务尽快完成 → 高 QoS 任务继续执行

1.2 示例

let serialQueue = DispatchQueue(label: "serialQueue", qos: .utility)

serialQueue.async(qos: .background) {
    Thread.sleep(forTimeInterval: 5) // 低 QoS 任务
}

serialQueue.async(qos: .userInitiated) {
    print("高 QoS 任务执行")
}
  • 低 QoS 任务在前,正常会阻塞高 QoS
  • GCD 会临时提升低 QoS 任务到 .userInitiated → 缩短高 QoS 阻塞时间

2️⃣ 开发者可以做的优化

2.1 避免串行队列中高低 QoS 混用

  • 串行队列顺序固定,低 QoS 在前会阻塞高 QoS

  • 建议

    • 高 QoS 任务单独使用高 QoS 队列
    • 低 QoS 任务使用后台队列
let highQueue = DispatchQueue(label: "highQueue", qos: .userInitiated)
let lowQueue = DispatchQueue(label: "lowQueue", qos: .background)

2.2 使用异步调度

  • async 提交任务不会阻塞调用线程
  • 可以减少高 QoS 任务等待低 QoS 队列任务的时间
serialQueue.async {
    // 任务不会阻塞调用线程
}
  • 避免在高 QoS 任务内调用 sync 到低 QoS 队列 → 避免优先级反转

2.3 避免长时间阻塞高 QoS 线程

  • 高 QoS 线程上执行耗时任务 → 阻塞自己或其他高 QoS 任务

  • 优化

    • CPU 密集型 / 耗时操作 → 移到低或中等 QoS 队列
    • 使用 DispatchWorkItem 或后台队列处理

2.4 对共享资源使用 GCD 原语

  • 串行队列Dispatch Barrier管理共享资源
  • 避免高 QoS 任务被低 QoS 任务持有锁阻塞
let concurrentQueue = DispatchQueue(label: "concurrentQueue", attributes: .concurrent)
concurrentQueue.async(flags: .barrier) {
    // 独占访问共享资源
}
  • 可以保证高 QoS 任务访问共享资源时不会被低 QoS 长时间阻塞

2.5 合理拆分任务

  • 避免一个低 QoS 大任务占用串行队列过久
  • 拆成多个小任务,让系统调度机会更多 → 高 QoS 任务不会被长时间阻塞

3️⃣ 总结

方法 GCD/系统机制 开发者措施
缓解优先级反转 QoS 继承:临时提升低 QoS 任务 避免串行队列高低 QoS 混用
异步 vs 同步 async 不阻塞调用线程 避免高 QoS sync 调用低 QoS 队列
阻塞控制 高 QoS 阻塞会影响系统响应 长耗时任务移到低 QoS 队列
共享资源 串行队列 / barrier 保证独占 避免高 QoS 等待低 QoS 持有的锁
任务拆分 系统调度机会更多 将大任务拆成小任务减少阻塞

💡 核心思想:GCD 会临时提升低 QoS 任务优先级,但开发者仍需合理划分任务、队列和资源使用,才能最小化优先级反转对性能的影响。


1-24.【GCD】QoS 和线程优先级是 1:1 映射的吗?

作者 项阿丑
2026年1月28日 11:27

QoS 和线程优先级并不是 1:1 映射,它们只是相关,但机制上存在差别。下面详细解释:


1️⃣ 核心结论

QoS 是 GCD 的任务级别优先策略,用于指导系统调度任务和线程资源;线程优先级(pthread / sched)是操作系统层面线程调度权重。二者关联,但并非严格一一对应。

换句话说:

  • QoS → 告诉系统“任务重要性”,GCD 根据这个在全局线程池中选择或创建线程
  • 线程优先级 → 控制 CPU 时间分配
  • 高 QoS 通常对应较高线程优先级,但不是绝对,也会受到系统调度和能耗优化影响

2️⃣ 关联机制

2.1 任务到线程的映射

当你提交一个任务到队列:

  1. GCD 查看任务 QoS

  2. 选择或创建合适线程

    • 高 QoS → 尝试在高优先级线程或更多线程上执行
    • 低 QoS → 使用低优先级线程池或延迟执行
  3. 线程可能共享

    • 同一线程可以执行不同 QoS 任务
    • 系统会动态调整线程优先级以匹配任务 QoS

⚠️ 所以一个线程上可能交替执行不同 QoS 的任务,线程优先级会动态调整,并非固定 1:1


2.2 系统动态调节

  • macOS/iOS 内核会综合考虑:

    • 当前 CPU 利用率
    • 能耗和电量策略
    • 多任务调度公平性
  • 因此:

    • 高 QoS 任务不一定始终在最高优先级线程上执行
    • 低 QoS 任务可能在空闲线程上抢占 CPU

3️⃣ 与 pthread 优先级的区别

特性 QoS (GCD) 线程优先级 (pthread / sched)
粒度 任务级别 线程级别
作用 指导系统调度任务和线程分配 决定线程在 CPU 上调度权重
线程绑定 任务可能在任何线程上执行 固定线程调度权重
动态性 GCD 可动态提升/继承 QoS 通常固定,除非手动修改
对能耗影响 高 QoS → 可能更激进分配 CPU 无直接能耗优化机制

4️⃣ QoS 优先级与线程优先级关系

GCD 文档说明:

  • .userInteractive / .userInitiated → 高 QoS → 系统会尽量在高优先级线程或更多线程执行
  • .utility / .background → 低 QoS → 系统可能延迟执行或降低线程 CPU 优先级
  • 线程优先级只是 一个调度参考,实际调度会动态调整

✅ 核心:高 QoS 更可能获得高线程优先级,但不是一条任务对应一条线程的固定关系


5️⃣ 总结一句话

QoS 影响 GCD 如何选择线程和调度任务,但不是 1:1 映射到线程优先级;线程优先级只是系统层面的调度权重,GCD 会动态调整线程以匹配任务 QoS。


1-22.【GCD】使用 DispatchSource 时最容易踩的坑有哪些?

作者 项阿丑
2026年1月28日 11:27

1️⃣ 忘记 resume()

let source = DispatchSource.makeTimerSource(queue: DispatchQueue.global())
source.schedule(deadline: .now(), repeating: 1.0)
source.setEventHandler { print("Timer fired") }
// source.resume() // ⚠️ 忘记 resume
  • :创建 DispatchSource 后必须调用 resume() 才会启动事件源
  • 原因:DispatchSource 默认是暂停状态
  • 后果:事件永远不会触发
  • 解决:调用 resume();如果需要暂停/恢复,结合 suspend() 使用

2️⃣ DispatchSource 被释放

DispatchSource.makeTimerSource(queue: .global()).schedule(deadline: .now(), repeating: 1.0).setEventHandler {
    print("Fired")
}
  • :DispatchSource 没有被强引用 → 立即释放
  • 原因:DispatchSource 是对象,如果没有外部引用,系统会释放它
  • 后果:事件永远不会触发
  • 解决:保留一个强引用,例如类属性
class MyClass {
    var timer: DispatchSourceTimer?
    
    func start() {
        timer = DispatchSource.makeTimerSource(queue: .global())
        timer?.schedule(deadline: .now(), repeating: 1.0)
        timer?.setEventHandler { print("Fired") }
        timer?.resume()
    }
}

3️⃣ 线程安全误区

  • :认为事件处理 block 内访问的资源线程安全

  • 原因:DispatchSource 的回调在指定队列上执行,并发队列上可能同时执行多个事件 block

  • 后果:共享资源竞争、数据不一致

  • 解决

    • 串行队列保证顺序和互斥
    • 并发队列访问共享资源时加锁或 barrier

4️⃣ 重复 resume 导致 crash

source.resume()
source.resume() // ⚠️ 再次 resume 会 crash
  • :DispatchSource 只能 resume 一次

  • 原因:resume 用来启动事件源,重复调用会触发异常

  • 解决

    • 使用标志位判断是否已 resume
    • 或通过类属性封装管理状态

5️⃣ 使用 sync 触发死锁

let queue = DispatchQueue(label: "serial")
let source = DispatchSource.makeTimerSource(queue: queue)
source.setEventHandler {
    queue.sync { print("Deadlock") } // ⚠️ 死锁
}
source.resume()
  • :事件 block 内调用 sync 队列同队列 → 死锁
  • 原因:串行队列同步调用自己会等待 → 永远无法完成
  • 解决:使用 async 调度,或者将事件 block 放到不同队列

6️⃣ 忘记 cancel

  • :DispatchSource 用完不取消
  • 原因:DispatchSource 会保持队列引用,未 cancel 可能导致内存泄漏
  • 解决
source.cancel()
source.setEventHandler(nil) // 释放闭包引用

7️⃣ 对定时器误用 resume/suspend

  • :误认为 suspend/resume 可以无限暂停定时器

  • 注意

    • DispatchSourceTimer 初始为暂停状态 → 必须 resume 启动
    • suspend/resume 必须成对使用,否则下一次 resume 会 crash

8️⃣ 队列选择不当

  • :将 DispatchSource 绑定到主队列或串行队列,但事件量大

  • 后果

    • 阻塞主线程 → UI 卡顿
    • 串行队列上高频事件 → 队列积压
  • 解决

    • 高频事件使用后台并发队列
    • 根据场景合理选择队列类型

✅ 总结常见坑及解决策略

解决方案
忘记 resume 必须调用 resume() 启动事件源
DispatchSource 被释放 保持强引用(类属性)
线程安全误区 并发队列访问共享资源加锁,或使用串行队列
重复 resume 导致 crash 只 resume 一次,使用标志位
sync 导致死锁 避免在事件 block 内对同队列 sync,使用 async
忘记 cancel 使用完毕后 cancel,并释放事件处理器闭包
suspend/resume 不成对 严格成对调用,确保队列状态正确
队列选择不当 高频事件用后台并发队列,UI 事件用主队列

💡 核心经验

  1. DispatchSource 本质是 事件源 + GCD block → 避免直接对线程或队列做 unsafe 操作
  2. 所有事件处理 block 都遵循绑定队列调度 → 理解串行/并发对共享资源的影响
  3. 强引用 + resume + cancel 是生命周期管理三部曲

1-13.【GCD】dispatch_barrier 的作用和使用场景是什么?

作者 项阿丑
2026年1月28日 09:48

1️⃣ 基本作用

dispatch_barrier 用于在并发队列中创建一个“屏障”,保证屏障前的任务全部完成后才执行屏障任务,屏障任务完成后才允许屏障后的任务继续执行。

  • 本质:把并发队列临时变成“串行屏障”
  • 保证 读-写或写-写安全,同时不阻塞其他并行队列的线程资源

2️⃣ 工作机制

假设有一个并发队列 concurrentQueue

let concurrentQueue = DispatchQueue(label: "concurrent", attributes: .concurrent)

concurrentQueue.async { print("A") }
concurrentQueue.async { print("B") }
concurrentQueue.async(flags: .barrier) { print("Barrier") }
concurrentQueue.async { print("C") }
concurrentQueue.async { print("D") }

执行顺序:

  1. A、B 可以同时执行(并发)
  2. BarrierA、B 完全完成 后执行
  3. C、D 等 Barrier 完成后再执行

⚠️ 注意:

  • barrier 任务前后不会并发执行
  • barrier 前的任务可以并发
  • barrier 后的任务也可以并发(但 Barrier 本身是独占的)

3️⃣ 使用场景

3.1 并发队列上的读写安全

典型场景:多读少写的数据结构

var sharedArray = [Int]()
let queue = DispatchQueue(label: "concurrentQueue", attributes: .concurrent)

// 多个并发读
queue.async {
    print(sharedArray.count)
}
queue.async {
    print(sharedArray.last ?? 0)
}

// 写操作使用 barrier
queue.async(flags: .barrier) {
    sharedArray.append(42)
}

解释:

  • 多个读可以并发执行 → 高性能
  • 写操作必须等前面读完成,并且执行时阻止其他读写 → 数据安全

Barrier 是 并发队列上的写锁机制


3.2 替代传统锁(NSLock / DispatchSemaphore)

  • 可以用 barrier 替代全局锁对共享资源加锁

  • 优势:

    • 保持 读操作并发性
    • 写操作串行化,不阻塞其他队列的线程池
  • 适合 读多写少场景


3.3 串行队列上的 barrier 无效

let serialQueue = DispatchQueue(label: "serial")
serialQueue.async(flags: .barrier) { ... }
  • barrier 在串行队列上 没有任何特殊作用

  • 原因:

    • 串行队列天然保证同一时间只执行一个任务
    • barrier 只是多余标记

4️⃣ 使用注意事项

  1. 只能在自定义并发队列上使用

    • global queue 不允许 barrier
  2. 不要在主队列上使用 barrier

    • 主队列是串行的,没意义
  3. 同步 barrier(sync + barrier)可能死锁

    • 原因和普通 sync 一样,当前线程可能阻塞自己

5️⃣ 总结

作用:

  • 在并发队列中创建一个排它屏障
  • 屏障前的任务先执行,屏障独占执行,屏障后的任务再继续并发执行

典型场景:

  • 并发读 + 写操作的共享数据
  • 读多写少,避免使用锁仍保证线程安全
  • 替代 NSLock / semaphore 进行高性能读写控制

💡 一句话总结

dispatch_barrier 就是给并发队列加了一道“独占关卡”,保证关键写操作在队列中独占执行,而不影响前后的并发读任务。

1-6.【GCD】自定义队列在底层是如何映射到全局队列的?

作者 项阿丑
2026年1月28日 09:46

自定义队列本身不拥有线程;它们通过 target queue 机制,最终把任务“汇流”到某个全局并发队列(按 QoS)上执行。

下面我们从 结构 → 映射路径 → 调度细节 → 常见误解 一层层拆。


1️⃣ 核心模型:Queue ≠ Thread(再强调一次)

在 GCD 里:

  • 队列(DispatchQueue)
    👉 任务的组织和调度规则
  • 线程(worker thread)
    👉 真正执行任务的资源

👉 自定义队列永远不直接绑定线程


2️⃣ 自定义队列创建时发生了什么?

let queue = DispatchQueue(
    label: "com.example.myqueue",
    qos: .utility,
    attributes: [.concurrent]
)

你得到的是一个:

  • 有自己 FIFO / 并发规则的 逻辑队列
  • 没有线程
  • 没有独立线程池

3️⃣ 真正关键的机制:target queue

每一个 dispatch queue 都有一个 target queue

你可以显式指定:

queue.setTarget(queue: DispatchQueue.global(qos: .utility))

但即使你不写:

系统也会自动帮你设

默认 target 规则是:

队列类型 默认 target
main queue main thread / RunLoop
global queue root queue(系统内部)
自定义 queue 对应 QoS 的 global queue

4️⃣ 调度路径(重点流程)

当你执行:

queue.async {
    work()
}

真实发生的是:

你的自定义 queue
   ↓(保持自己的串行 / 并发语义)
target queue(global queue, by QoS)
   ↓
GCD 调度器
   ↓
共享线程池(worker threads)
   ↓
执行 block

👉 自定义队列只负责:

  • 任务顺序
  • barrier
  • 串行 / 并发约束

👉 全局队列负责:

  • 线程选择
  • QoS 调度
  • CPU 分配

5️⃣ 串行队列是如何“跑在并发队列上”的?

这点很容易让人迷糊。

let serial = DispatchQueue(label: "serial")

虽然:

  • serial queue 自己是 串行的
  • 但它的 target 是 并发的 global queue

为什么还能串行?

原因是:

串行语义是在“队列层”保证的,不是在“线程层”

GCD 保证:

  • 同一时间
  • serial queue 只会向 target queue 提交 一个 block

即使 target 是并发的:

  • 永远只会有一个来自该 serial queue 的任务在执行

6️⃣ 并发自定义队列又是如何工作的?

let concurrent = DispatchQueue(
    label: "concurrent",
    attributes: .concurrent
)

特点:

  • 并发规则由 这个队列自己管理
  • 多个 block 可以同时被提交给 target queue
  • target queue(global)负责并行执行

7️⃣ barrier 为什么只能用于自定义并发队列?

queue.async(flags: .barrier) {
    criticalSection()
}

原因很直白:

  • barrier 是 队列内部的语义

  • global queue 是:

    • 共享的
    • 多来源的
  • GCD 无法也不允许:

    • 为“全局公共队列”插入独占屏障

👉 所以:

barrier 只能作用在你“拥有”的那一层队列


8️⃣ QoS 是如何一路传下去的?

  • 自定义队列有自己的 QoS

  • 提交的 block 继承:

    • queue QoS
    • 或 caller QoS(取更高)
  • 最终:

    • 映射到 target global queue
    • 再映射到线程优先级

👉 QoS 是任务属性,不是队列绑定线程。


9️⃣ 一个容易忽略但很重要的点

setTarget(queue:) 可以改变的不只是 QoS

你可以:

queue.setTarget(queue: someSerialQueue)

结果是:

  • 多个队列“汇流”到一个串行队列

  • 从而实现:

    • 资源限流
    • 顺序控制
    • 自定义调度树

这就是 GCD 的“队列树(queue hierarchy)”设计


🔚 一句话终极总结

自定义队列通过 target queue 机制映射到对应 QoS 的全局并发队列执行;它们只定义任务的组织与同步语义,而线程管理与调度完全由全局队列和系统线程池负责。

1-5.【GCD】全局并发队列(global queue)有多少个?是如何区分的?

作者 项阿丑
2026年1月28日 09:45

✅ 结论版(先背这个)

系统一共有 6 组全局并发队列(global concurrent queues),按 QoS(服务质量)区分;
每一组并不是“一个队列”,而是一类共享同一 QoS 的并发队列入口,最终映射到同一个线程池。


1️⃣ 到底有多少个全局并发队列?

GCD 语义层面看:

🧩 一共 6 种 QoS,对应 6 组全局并发队列

QoS Swift 枚举 用途
User Interactive .userInteractive UI 渲染、动画、立即响应
User Initiated .userInitiated 用户触发、马上要结果
Default .default 默认优先级
Utility .utility I/O、网络、数据处理
Background .background 用户不可见的后台任务
Unspecified .unspecified 系统内部使用

调用方式:

DispatchQueue.global(qos: .userInitiated)

⚠️ 注意:
你拿到的不是“一个独立队列实例”,而是一个“QoS 对应的入口”


2️⃣ 那为什么看起来像“无限多个”?

因为这段代码:

let q1 = DispatchQueue.global(qos: .utility)
let q2 = DispatchQueue.global(qos: .utility)
  • q1 === q2 ❌(对象不一定相等)
  • 但它们调度到的是同一套执行资源

👉 GCD 的设计是:

逻辑上提供多个 queue handle,
物理上共享同一个 QoS 对应的线程池


3️⃣ 全局并发队列是如何“区分”的?

✅ 核心区分维度:QoS(不是线程数)

QoS 决定什么?

  • 线程调度优先级
  • CPU 时间分配
  • 是否更容易抢占别的任务
  • 能否影响 UI 流畅度
  • 能否被系统延迟 / 挂起

例如:

DispatchQueue.global(qos: .background).async {
    // 系统允许你慢慢来
}

DispatchQueue.global(qos: .userInteractive).async {
    // 系统会尽可能立刻执行
}

4️⃣ 那每个 QoS 有多少线程?

👉 没有固定数量

GCD 使用的是:

动态线程池(work-stealing thread pool)

线程数量取决于:

  • CPU 核心数
  • 当前系统负载
  • 任务是否阻塞(I/O)
  • 能耗策略(尤其在 iOS)

例如:

  • 8 核设备
  • .userInitiated 可能同时跑 6–8 个线程
  • .background 可能只给 1–2 个,甚至被暂停

5️⃣ 一个很容易误解的点(重点⚠️)

❌ 错误理解

“每个 QoS 是一个独立的并发队列”

✅ 正确理解

每个 QoS 是一类调度优先级 + 共享线程池,
DispatchQueue.global 只是一个提交入口

你不能:

  • 暂停 global queue
  • barrier global queue
  • 控制它的最大并发数

这些只有 自建并发队列 才能做到。


6️⃣ global queue vs 自建并发队列

特性 global queue 自建 concurrent queue
数量 固定 6 组 任意
QoS 系统定义 可自定义
线程池 系统共享 仍然用系统池
barrier ❌ 不支持 ✅ 支持
suspend
控制并发 部分可控

7️⃣ 一句话总结(面试杀器)

GCD 中并不存在“很多个全局并发队列实例”,而是按 QoS 划分的 6 组全局并发执行环境;DispatchQueue.global(qos:) 只是获取对应 QoS 的调度入口,底层由系统统一的线程池动态执行。

👉 追问:不同 QoS 的 global queue 是否共享线程池?

是的,不同 QoS 的 global queue 在实现层面共享同一个底层线程池,但在调度层面被 QoS 强烈隔离和加权;它们共享“人手”,但不共享“优先权”。

下面把这个听起来有点绕的结论拆开。


1️⃣ 先把“共享”说清楚:共享的是什么?

✅ 共享的是:线程池(worker threads)

  • GCD 并不是:

    • 每个 QoS 一组固定线程
  • 而是:

    • 一个(或少量)全局 worker thread pool
    • 线程是系统资源,可复用

当某个线程空闲时:

  • 可以去执行:

    • .background 的任务
    • 也可以去执行 .userInitiated 的任务
  • 线程本身不“属于”某个 QoS

👉 所以从物理层面讲:是共享的


2️⃣ 那 QoS 的作用在哪?(关键)

QoS 决定的是:调度优先级,不是线程归属

调度时,GCD 会考虑:

  • 任务的 QoS
  • 线程当前的优先级
  • 系统负载
  • 能耗 / 前后台状态

然后做两件事:

🔹 1. 决定“先跑谁”

  • 高 QoS 任务:

    • 更容易被调度
    • 更少被延迟
  • 低 QoS 任务:

    • 可能被推迟
    • 甚至暂时不调度

🔹 2. 提升线程优先级(QoS override)

当一个 worker thread 去执行高 QoS 任务时:

  • 线程的调度优先级会被临时提升
  • 执行完再降回来

👉 线程是“变色龙”,任务决定颜色。


3️⃣ 一个非常重要的机制:QoS 继承 / 提升

来看一个经典场景:

DispatchQueue.global(qos: .background).async {
    DispatchQueue.global(qos: .userInteractive).sync {
        // 高优先级任务
    }
}

如果不做特殊处理:

  • background 线程
  • 执行 userInteractive
  • UI 会卡 💀

所以 GCD 有:

QoS override(优先级反转保护)

  • 当低 QoS 线程执行高 QoS block 时
  • 线程会被临时提升到高 QoS
  • 防止高优先级任务被“拖慢”

这再次说明:
👉 线程池是共享的,但优先级是动态的


4️⃣ 那是不是“完全共享、毫无隔离”?不是。

❌ 不是一个“任务大杂烩池”

系统会做软隔离

  • 高 QoS:

    • 有最低执行保障
    • 更积极唤醒线程
  • 低 QoS:

    • 在资源紧张时被限流
    • 可能暂停

例如:

  • 屏幕锁屏
  • 低电量模式
  • App 进入后台

.background 任务可能直接不跑。


5️⃣ 和 Swift Concurrency 的关系(顺一下)

Swift Concurrency 里的:

Task {
    // 默认继承当前 QoS
}

Task.detached(priority: .background) {
    ...
}

最终:

  • 仍然落到 GCD 的全局线程池
  • TaskPriority → 映射到 GCD QoS

所以:

Swift Concurrency 并没有另起炉灶,它复用的就是这套 QoS + 共享线程池模型


6️⃣ 常见误解对照表

误解 真相
每个 QoS 有自己的一组线程 ❌ 共享线程
background 任务永远慢 ❌ 只是优先级低
高 QoS 一定独占 CPU ❌ 只是更容易被调度
QoS = 线程优先级 ❌ QoS 是任务属性

7️⃣ 一句话终极总结

不同 QoS 的 global queue 在物理层面共享同一个 GCD 线程池,但在调度层面通过 QoS 权重、优先级继承和系统策略实现强隔离,从而既节省线程资源,又保证高优先级任务的响应性。

1-3.【GCD】DispatchQueue 的底层数据结构是什么?

作者 项阿丑
2026年1月27日 17:36

它是一个复杂的 C 结构体,集成了任务队列、线程管理、性能优化等多个子系统

核心数据结构:dispatch_queue_s

// libdispatch 源码中的核心结构(简化)
struct dispatch_queue_s {
    // 1. 基础头部信息(所有 dispatch 对象共享)
    struct dispatch_object_s _as_do[0];      // 转换为 dispatch_object
    struct dispatch_queue_s *_as_dq[0];      // 转换为 dispatch_queue
    
    // 2. 队列标识信息
    const struct dispatch_queue_vtable_s *vtable;  // 虚函数表(多态)
    const char *dq_label;                    // 队列标签(我们设置的 label)
    uint16_t dq_width;                       // 并发宽度(串行为1)
    uint32_t dq_serialnum;                   // 序列号(唯一标识)
    
    // 3. 目标队列和层次结构
    struct dispatch_queue_s *dq_targetq;     // 目标队列(优先级继承)
    uintptr_t dq_targetq_override;           // 目标队列覆盖(QoS 传播)
    
    // 4. 任务队列管理
    union {
        struct dispatch_queue_specific_head_s *dq_specific_head;
        struct dispatch_source_refs_s *ds_refs;
    } _dq_specific;
    
    // 5. 同步原语
    struct dispatch_object_s *volatile dq_items_tail;  // 任务队列尾部
    struct dispatch_object_s *volatile dq_items_head;  // 任务队列头部
    uint32_t dq_atomic_flags;                // 原子标志位
    
    // 6. 线程池和性能管理
    struct dispatch_continuation_s *volatile dq_last;  // 最后执行的任务
    uint32_t dq_side_specific_ints;           // 性能计数器
    pthread_priority_t dq_priority;           // 优先级缓存
};

详细结构解析

1. 多态设计:dispatch_object

// 所有 GCD 对象的基类
struct dispatch_object_s {
    _DISPATCH_OBJECT_HEADER(object);  // 头部宏,包含引用计数等
};

// DispatchQueue 通过以下宏实现多态
#define _DISPATCH_QUEUE_CLASS_HEADER(queue_label, ...) \
    _DISPATCH_OBJECT_HEADER(queue) \
    const char *dq_label; \
    uint16_t dq_width;

// 这使得:
dispatch_queue_t queue = dispatch_queue_create("com.test", NULL);
dispatch_object_t obj = (dispatch_object_t)queue;  // 可以向上转型

2. 任务队列:双向链表

// 实际存储任务的结构
struct dispatch_continuation_s {
    struct dispatch_object_s dc_do;           // 对象头部
    dispatch_function_t dc_func;              // 执行函数
    void *dc_ctxt;                            // 上下文参数
    void *dc_data;                            // 额外数据
    void *dc_other;                           // 关联数据
    
    // 链表指针
    struct dispatch_continuation_s *volatile dc_next;
    struct dispatch_continuation_s *dc_prev;
    
    // 队列关联
    struct dispatch_queue_s *dc_queue;        // 所属队列
};

// 队列如何管理任务
struct dispatch_queue_s {
    // ...
    struct dispatch_continuation_s *dq_items_head;  // 队头
    struct dispatch_continuation_s *dq_items_tail;  // 队尾
    uint32_t dq_nitems;                           // 任务计数
};

3. 队列层次结构

// 队列间的父子关系(目标队列机制)
struct dispatch_queue_hierarchy_s {
    dispatch_queue_t dqh_queue;              // 当前队列
    dispatch_queue_t dqh_target;             // 目标队列
    uintptr_t dqh_override;                  // QoS 覆盖
    uint16_t dqh_priority;                   // 优先级
};

// 示例:
// 自定义队列 → 全局队列 → 根队列
// com.test.queue → com.apple.root.default-qos → kernel

4. 性能优化结构

// 队列的侧面(side)数据结构
struct dispatch_queue_side_s {
    // 用于性能优化的缓存
    uint64_t dq_side_timer;                 // 定时器相关
    uint64_t dq_side_wlh;                   // 工作循环句柄
    uint32_t dq_side_bits;                  // 状态位
};

// 队列特定数据(dispatch_queue_set_specific/get_specific)
struct dispatch_queue_specific_head_s {
    struct dispatch_specific_queue_s *dsq_next;
    void *dsq_data;                         // 用户设置的数据
    uintptr_t dsq_key;                      // 键值
};

不同队列类型的内部差异

1. 全局队列(Global Queue)

// 全局队列有特殊结构
struct dispatch_queue_global_s {
    struct dispatch_queue_s _as_dq[0];      // 基础队列部分
    
    // 全局队列特有
    int dgq_priority;                       // 优先级索引
    unsigned int dgq_flags;                 // 标志位
    
    // 共享线程池引用
    struct dispatch_pthread_root_queue_s *dgq_thread_pool;
    struct dispatch_workloop_s *dgq_wlh;    // 工作循环
};

2. 主队列(Main Queue)

// 主队列的特殊处理
struct dispatch_queue_main_s {
    struct dispatch_queue_s _as_dq[0];
    
    // 与 RunLoop 集成
    CFRunLoopRef _dq_runloop;               // 关联的 RunLoop
    CFRunLoopSourceRef _dq_runloop_source;  // 事件源
    
    // 串行执行保证
    pthread_t _dq_main_thread;              // 主线程标识
    uint32_t _dq_main_flags;                // 主队列标志
};

3. 并发队列 vs 串行队列

// 区别主要在 dq_width 字段:
struct dispatch_queue_s {
    uint16_t dq_width;  // 并发宽度
    // 值为 1:串行队列(DISPATCH_QUEUE_SERIAL)
    // 值 > 1:并发队列(DISPATCH_QUEUE_CONCURRENT)
    // 特殊值:DISPATCH_QUEUE_WIDTH_MAX(并发无限)
};

// 并发队列还有额外的管理结构
struct dispatch_queue_concurrent_s {
    struct dispatch_queue_s _as_dq[0];
    
    // 用于并发控制
    os_unfair_lock _dq_lock;                // 内部锁
    uint32_t _dq_running;                   // 正在运行的任务数
    uint32_t _dq_max_running;               // 最大并发数
};

内存布局示例

// DispatchQueue 在内存中的大致布局
+-----------------------------------+
| dispatch_object_s header          | ← 引用计数、类型信息等
|-----------------------------------|
| vtable pointer                    | ← 虚函数表(决定队列类型行为)
|-----------------------------------|
| dq_label ("com.example.queue")    | ← 队列标签指针
|-----------------------------------|
| dq_width (1 或 n)                 | ← 并发宽度
|-----------------------------------|
| dq_serialnum (0x1234)             | ← 序列号
|-----------------------------------|
| dq_targetq (指向全局队列)          | ← 目标队列指针
|-----------------------------------|
| dq_items_head → [任务1] → [任务2]  | ← 任务链表头
| dq_items_tail → [任务n]            | ← 任务链表尾
|-----------------------------------|
| dq_last (最后执行的任务指针)        | ← 性能优化
|-----------------------------------|
| dq_priority (QoS 值)              | ← 优先级缓存
+-----------------------------------+

队列创建过程

创建自定义队列时的内部操作:

dispatch_queue_t dispatch_queue_create(const char *label, 
                                       dispatch_queue_attr_t attr) {
    // 1. 分配内存
    struct dispatch_queue_s *dq = calloc(1, sizeof(struct dispatch_queue_s));
    
    // 2. 设置基本字段
    dq->dq_label = label ? strdup(label) : NULL;
    dq->dq_serialnum = dispatch_atomic_inc(&g_serialnum);  // 全局递增
    
    // 3. 根据属性设置并发宽度
    if (attr == DISPATCH_QUEUE_SERIAL || attr == NULL) {
        dq->dq_width = 1;  // 串行
    } else if (attr == DISPATCH_QUEUE_CONCURRENT) {
        dq->dq_width = DISPATCH_QUEUE_WIDTH_MAX;  // 并发
    }
    
    // 4. 设置目标队列(通常是全局队列)
    dq->dq_targetq = _dispatch_get_root_queue(qos, overcommit);
    
    // 5. 设置虚函数表
    if (dq->dq_width == 1) {
        dq->vtable = &_dispatch_queue_serial_vtable;
    } else {
        dq->vtable = &_dispatch_queue_concurrent_vtable;
    }
    
    // 6. 初始化任务链表
    dq->dq_items_head = dq->dq_items_tail = NULL;
    
    return dq;
}

任务执行流程数据结构

// 任务提交和执行涉及的数据结构
struct dispatch_queue_workloop_s {
    // 工作循环(每个线程一个)
    pthread_workqueue_t dqw_workqueue;      // 底层工作队列
    uint32_t dqw_refcnt;                    // 引用计数
    uint16_t dqw_qos;                       // QoS 级别
    
    // 任务调度
    struct dispatch_continuation_s *dqw_head;
    struct dispatch_continuation_s *dqw_tail;
    
    // 线程池管理
    uint32_t dqw_thread_pool_size;          // 线程池大小
    uint32_t dqw_thread_pool_active;        // 活跃线程数
};

调试信息

// 可以通过私有 API 查看内部结构(仅调试)
extension DispatchQueue {
    func debugInfo() {
        let queue = self as AnyObject
        
        // 使用 Objective-C 运行时查看内部结构
        let ivars = class_copyIvarList(object_getClass(queue), nil)
        while let ivar = ivars?.pointee {
            let name = String(cString: ivar_getName(ivar)!)
            print("Ivar: (name)")
        }
    }
}

// 或者使用 LLDB 调试
(lldb) po (id)0x600003d6c040  // 假设这是队列地址
// 可以看到内部字段(如果符号可用)

性能优化特性

1. 内联缓存

struct dispatch_queue_cache_s {
    // 缓存最近使用的队列
    struct dispatch_queue_s *dqc_queues[DISPATCH_QUEUE_CACHE_SIZE];
    uint32_t dqc_hits;                      // 缓存命中次数
    uint32_t dqc_misses;                    // 缓存未命中
};

2. 优先级继承

struct dispatch_queue_priority_s {
    uint16_t dqp_priority;                  // 当前优先级
    uint16_t dqp_inherited;                 // 继承的优先级
    struct dispatch_queue_s *dqp_source;    // 优先级来源队列
};

总结

DispatchQueue 的底层数据结构:

  1. 复杂但高效:多层结构体设计,兼顾灵活性和性能
  2. 基于对象模型:所有 dispatch 对象共享基类,实现多态
  3. 链表管理任务:使用双向链表存储待执行任务
  4. 层次化设计:队列有目标队列,形成优先级继承链
  5. 类型特定扩展:不同队列类型有不同字段和虚函数表
  6. 性能优化密集:包含大量缓存、计数器和优化字段

简单来说DispatchQueue 在底层是一个精心设计的任务调度引擎,它:

  • 结构体表示队列(而非类)
  • 链表管理任务
  • 虚函数表实现不同队列类型的行为
  • 目标队列机制实现优先级继承
  • 高度优化以减少内存占用和提高性能

这种设计使得 GCD 能够在极小的开销下处理数千个队列和数百万个任务调度。

👉 追问:任务是如何入队、出队的?

任务入队出队是 GCD 的核心机制,涉及原子操作、无锁队列、优先级调度等多个复杂系统。

核心数据结构关系

// 简化版数据结构关系
struct dispatch_queue_s {
    // 任务链表(无锁队列)
    struct dispatch_continuation_s *volatile dq_items_head;  // 队头
    struct dispatch_continuation_s *volatile dq_items_tail;  // 队尾
    uint32_t dq_nitems;                           // 任务计数
    
    // 线程池引用
    struct dispatch_queue_workloop_s *dq_wlh;     // 工作循环
};

struct dispatch_continuation_s {
    // 任务数据和函数指针
    dispatch_function_t dc_func;      // 要执行的函数
    void *dc_ctxt;                    // 上下文参数
    
    // 链表指针(双向链表)
    struct dispatch_continuation_s *volatile dc_next;
    struct dispatch_continuation_s *dc_prev;
    
    // 标记信息
    uintptr_t dc_flags;              // 标志位(同步/异步/屏障等)
};

1. 入队过程(Enqueue)

异步任务入队(dispatch_async)

// 用户调用
queue.async {
    print("任务执行")
}

// 内部处理流程
func dispatch_async(queue: dispatch_queue_t, block: @escaping () -> Void) {
    // 1. 封装任务
    let continuation = _dispatch_continuation_alloc()
    continuation.dc_func = _dispatch_call_block_and_release
    continuation.dc_ctxt = Block_copy(block)  // 复制block到堆上
    
    // 2. 获取队列状态
    let old_state = queue.dq_state
    
    // 3. 尝试快速路径(无锁操作)
    if _dispatch_queue_try_acquire_async(queue) {
        // 快速路径:队列空闲,直接调度
        _dispatch_continuation_schedule(queue, continuation)
        return
    }
    
    // 4. 慢速路径:需要加锁或队列繁忙
    _dispatch_queue_push(queue, continuation)
}

详细入队步骤

// 实际入队函数
void _dispatch_queue_push(dispatch_queue_t dq, 
                         dispatch_continuation_t dc) {
    
    // 步骤1:设置任务状态
    dc->dc_queue = dq;           // 关联队列
    dc->dc_flags = ASYNC;        // 标记为异步
    
    // 步骤2:原子操作将任务加入链表尾部
    dispatch_continuation_t prev_tail;
    do {
        prev_tail = dq->dq_items_tail;
        dc->dc_prev = prev_tail;           // 设置前驱
    } while (!os_atomic_cmpxchg(&dq->dq_items_tail, 
                                prev_tail, 
                                dc, 
                                relaxed));
    
    // 步骤3:更新前一个节点的next指针
    if (prev_tail) {
        prev_tail->dc_next = dc;          // 连接链表
    } else {
        // 这是第一个任务,更新头指针
        dq->dq_items_head = dc;
    }
    
    // 步骤4:原子递增任务计数
    os_atomic_inc(&dq->dq_nitems, relaxed);
    
    // 步骤5:唤醒工作线程(如果需要)
    _dispatch_queue_wakeup(dq);
}

屏障任务特殊处理

// 屏障任务的入队
void _dispatch_barrier_async(dispatch_queue_t dq, 
                            dispatch_block_t block) {
    
    dispatch_continuation_t dc = _dispatch_continuation_alloc();
    dc->dc_func = _dispatch_call_block_and_release;
    dc->dc_ctxt = Block_copy(block);
    dc->dc_flags = BARRIER;               // 关键:设置屏障标志
    
    // 屏障任务需要特殊处理:
    // 1. 插入到队列末尾
    // 2. 标记队列进入"屏障模式"
    // 3. 等待前面所有任务完成
    
    _dispatch_queue_push_barrier(dq, dc);
    
    // 更新队列状态
    dq->dq_atomic_flags |= DISPATCH_QUEUE_IN_BARRIER;
}

2. 出队过程(Dequeue)

工作线程取任务

// 工作线程的主循环
void *_dispatch_worker_thread(void *context) {
    dispatch_queue_t dq = (dispatch_queue_t)context;
    
    while (1) {
        // 步骤1:获取下一个任务
        dispatch_continuation_t dc = _dispatch_queue_pop(dq);
        
        if (dc) {
            // 步骤2:执行任务
            _dispatch_continuation_invoke(dq, dc);
            
            // 步骤3:任务完成后处理
            _dispatch_continuation_free(dc);
        } else {
            // 步骤4:无任务,可能休眠或处理其他队列
            _dispatch_worker_yield_or_exit(dq);
        }
    }
    return NULL;
}

详细出队实现

// 从队列弹出任务
dispatch_continuation_t _dispatch_queue_pop(dispatch_queue_t dq) {
    
    // 步骤1:检查队列状态
    if (dq->dq_nitems == 0) {
        return NULL;  // 队列为空
    }
    
    // 步骤2:处理串行队列(简单)
    if (dq->dq_width == 1) {  // 串行队列
        return _dispatch_queue_pop_serial(dq);
    }
    
    // 步骤3:处理并发队列(复杂)
    return _dispatch_queue_pop_concurrent(dq);
}

// 串行队列出队(简单FIFO)
dispatch_continuation_t _dispatch_queue_pop_serial(dispatch_queue_t dq) {
    
    // 原子操作获取队头
    dispatch_continuation_t head;
    do {
        head = dq->dq_items_head;
        if (!head) return NULL;  // 队列为空
        
        // 尝试将头指针指向下一个任务
    } while (!os_atomic_cmpxchg(&dq->dq_items_head, 
                                head, 
                                head->dc_next, 
                                acquire));
    
    // 如果队列变空,清空尾指针
    if (head->dc_next == NULL) {
        dq->dq_items_tail = NULL;
    }
    
    // 更新任务计数
    os_atomic_dec(&dq->dq_nitems, relaxed);
    
    // 清理链表指针
    head->dc_next = NULL;
    head->dc_prev = NULL;
    
    return head;
}

// 并发队列出队(多线程安全)
dispatch_continuation_t _dispatch_queue_pop_concurrent(dispatch_queue_t dq) {
    
    // 使用原子操作+重试机制
    dispatch_continuation_t task = NULL;
    bool acquired = false;
    
    while (!acquired) {
        // 原子读取队头
        dispatch_continuation_t old_head = dq->dq_items_head;
        
        if (!old_head) {
            return NULL;  // 队列为空
        }
        
        // 尝试获取任务所有权
        acquired = os_atomic_cmpxchg(&dq->dq_items_head, 
                                     old_head, 
                                     old_head->dc_next, 
                                     acquire);
        
        if (acquired) {
            task = old_head;
            
            // 如果这是最后一个任务
            if (task->dc_next == NULL) {
                // 需要原子更新尾指针
                os_atomic_store(&dq->dq_items_tail, NULL, relaxed);
            }
        }
        // 如果失败,说明其他线程抢先获取了,重试
    }
    
    os_atomic_dec(&dq->dq_nitems, relaxed);
    task->dc_next = NULL;
    task->dc_prev = NULL;
    
    return task;
}

3. 任务执行流程

任务执行函数

// 执行任务的函数
void _dispatch_continuation_invoke(dispatch_queue_t dq,
                                   dispatch_continuation_t dc) {
    
    // 步骤1:保存当前队列上下文
    dispatch_queue_t old_dq = _dispatch_thread_getspecific(dispatch_queue_key);
    _dispatch_thread_setspecific(dispatch_queue_key, dq);
    
    // 步骤2:设置线程名字(便于调试)
    if (dq->dq_label) {
        pthread_setname_np(dq->dq_label);
    }
    
    // 步骤3:执行任务函数
    dc->dc_func(dc->dc_ctxt);
    
    // 步骤4:恢复之前的队列上下文
    _dispatch_thread_setspecific(dispatch_queue_key, old_dq);
    
    // 步骤5:如果是同步任务,发送信号
    if (dc->dc_flags & SYNC) {
        _dispatch_semaphore_signal(dc->dc_semaphore);
    }
}

屏障任务的特殊执行

// 屏障任务的执行
void _dispatch_barrier_execute(dispatch_queue_t dq,
                               dispatch_continuation_t dc) {
    
    // 步骤1:等待队列中所有前置任务完成
    while (dq->dq_running > 0) {
        // 忙等待或让出CPU
        _dispatch_hardware_pause();
    }
    
    // 步骤2:执行屏障任务(独占执行)
    _dispatch_continuation_invoke(dq, dc);
    
    // 步骤3:清除屏障标志
    dq->dq_atomic_flags &= ~DISPATCH_QUEUE_IN_BARRIER;
    
    // 步骤4:唤醒等待的后续任务
    _dispatch_queue_wakeup_next(dq);
}

4. 性能优化机制

任务批处理

// 批量处理任务(减少锁开销)
void _dispatch_queue_drain(dispatch_queue_t dq) {
    
    // 尝试一次性取出多个任务
    dispatch_continuation_t batch[16];
    int count = 0;
    
    // 批量出队
    for (int i = 0; i < 16; i++) {
        dispatch_continuation_t dc = _dispatch_queue_pop_fast(dq);
        if (!dc) break;
        
        batch[count++] = dc;
    }
    
    if (count == 0) return;
    
    // 批量执行
    for (int i = 0; i < count; i++) {
        _dispatch_continuation_invoke(dq, batch[i]);
        _dispatch_continuation_free(batch[i]);
    }
}

工作窃取(Work Stealing)

// 当线程空闲时,尝试从其他队列窃取任务
dispatch_continuation_t _dispatch_worksteal(void) {
    
    // 步骤1:获取当前线程的工作队列
    dispatch_queue_t current_queue = _dispatch_thread_get_queue();
    
    // 步骤2:遍历全局队列列表
    for (int i = 0; i < global_queue_count; i++) {
        dispatch_queue_t target = global_queues[i];
        
        // 跳过自己的队列和空队列
        if (target == current_queue) continue;
        if (target->dq_nitems == 0) continue;
        
        // 步骤3:尝试窃取任务
        dispatch_continuation_t stolen = _dispatch_queue_try_steal(target);
        if (stolen) {
            return stolen;  // 窃取成功
        }
    }
    
    return NULL;  // 没有可窃取的任务
}

5. 同步任务特殊处理

dispatch_sync 的实现

void dispatch_sync(dispatch_queue_t dq, dispatch_block_t block) {
    
    // 优化:如果当前已经在目标队列上,直接执行
    if (_dispatch_queue_is_current(dq)) {
        block();
        return;
    }
    
    // 创建同步任务结构
    struct dispatch_sync_context_s {
        dispatch_semaphore_t sema;
        dispatch_block_t block;
        bool done;
    } context;
    
    context.sema = dispatch_semaphore_create(0);
    context.block = block;
    context.done = false;
    
    // 创建任务
    dispatch_continuation_t dc = _dispatch_continuation_alloc();
    dc->dc_func = _dispatch_sync_invoke;
    dc->dc_ctxt = &context;
    dc->dc_flags = SYNC;
    dc->dc_semaphore = context.sema;
    
    // 入队
    _dispatch_queue_push(dq, dc);
    
    // 等待任务完成
    dispatch_semaphore_wait(context.sema, DISPATCH_TIME_FOREVER);
    
    // 检查是否成功执行
    if (!context.done) {
        // 发生错误
        dispatch_panic("dispatch_sync failed");
    }
}

// 同步任务执行函数
static void _dispatch_sync_invoke(void *ctxt) {
    struct dispatch_sync_context_s *ctx = ctxt;
    ctx->block();
    ctx->done = true;
}

6. 优先级处理

QoS 传播和提升

// 处理任务的优先级
void _dispatch_queue_adjust_priority(dispatch_queue_t dq,
                                     dispatch_continuation_t dc) {
    
    // 获取任务的 QoS
    qos_class_t task_qos = _dispatch_continuation_get_qos(dc);
    
    // 如果任务优先级高于队列当前优先级
    if (task_qos > dq->dq_priority) {
        // 提升队列优先级
        qos_class_t old_qos = dq->dq_priority;
        dq->dq_priority = task_qos;
        
        // 重新调度队列中的工作线程
        _dispatch_queue_reschedule(dq, old_qos, task_qos);
    }
    
    // 设置执行线程的 QoS
    pthread_set_qos_class_self_np(task_qos, 0);
}

完整流程示例

// 模拟一个任务从提交到完成的完整流程
func exampleTaskLifecycle() {
    let queue = DispatchQueue(label: "com.example", attributes: .concurrent)
    
    // 用户提交任务
    queue.async {
        print("任务执行开始")
        sleep(1)
        print("任务执行结束")
    }
    
    // 内部流程:
    // 1. async() 创建 dispatch_continuation_t
    // 2. 原子操作将任务添加到 queue.dq_items_tail
    // 3. queue.dq_nitems 原子递增
    // 4. _dispatch_queue_wakeup() 唤醒工作线程
    // 5. 工作线程从 queue.dq_items_head 取出任务
    // 6. 调用 dc_func(dc_ctxt) 执行任务
    // 7. 任务完成,dc 被释放
    // 8. 工作线程继续取下一个任务或休眠
}

7. 内存管理优化

任务缓存池

// 避免频繁分配释放 dispatch_continuation_t
struct dispatch_continuation_cache_s {
    dispatch_continuation_t free_list;      // 空闲列表
    uint32_t count;                         // 缓存数量
    os_unfair_lock lock;                    // 保护锁
};

// 获取一个任务结构(优先从缓存取)
dispatch_continuation_t _dispatch_continuation_alloc(void) {
    dispatch_continuation_cache_t cache = &g_continuation_cache;
    
    // 尝试从缓存获取
    os_unfair_lock_lock(&cache->lock);
    if (cache->free_list) {
        dispatch_continuation_t dc = cache->free_list;
        cache->free_list = dc->dc_next;
        cache->count--;
        os_unfair_lock_unlock(&cache->lock);
        
        // 清零复用
        memset(dc, 0, sizeof(struct dispatch_continuation_s));
        return dc;
    }
    os_unfair_lock_unlock(&cache->lock);
    
    // 缓存为空,分配新的
    return calloc(1, sizeof(struct dispatch_continuation_s));
}

// 释放任务结构(放入缓存)
void _dispatch_continuation_free(dispatch_continuation_t dc) {
    dispatch_continuation_cache_t cache = &g_continuation_cache;
    
    os_unfair_lock_lock(&cache->lock);
    if (cache->count < DISPATCH_CONTINUATION_CACHE_MAX) {
        // 放入缓存
        dc->dc_next = cache->free_list;
        cache->free_list = dc;
        cache->count++;
        os_unfair_lock_unlock(&cache->lock);
    } else {
        os_unfair_lock_unlock(&cache->lock);
        free(dc);  // 缓存已满,直接释放
    }
}

总结

任务入队出队的核心机制:

入队过程

  1. 封装任务:将block封装成 dispatch_continuation_t
  2. 原子操作:使用CAS(Compare-And-Swap)将任务添加到链表尾部
  3. 唤醒机制:如果需要,唤醒工作线程处理任务
  4. 优先级处理:根据任务QoS调整队列优先级

出队过程

  1. 原子竞争:多个工作线程竞争获取任务(无锁队列)
  2. 批量处理:可能一次性取出多个任务减少开销
  3. 工作窃取:空闲线程可以从其他队列窃取任务
  4. 屏障处理:屏障任务确保前后任务不交错执行

关键优化

  1. 无锁设计:使用原子操作避免锁竞争
  2. 缓存复用:重用任务结构体减少内存分配
  3. 批量处理:减少上下文切换开销
  4. 优先级继承:高优先级任务能更快执行
  5. 工作窃取:提高CPU利用率

简单比喻

  • 入队 像"快递站收件":你提交包裹(任务),快递站记录到清单(队列),通知快递员(线程)来取
  • 出队 像"快递员派件":快递员从清单取包裹,派送(执行),完成后标记完成
  • GCD 像"智能调度系统":动态分配快递员,优化路线,处理急件优先

这种设计使得 GCD 能够以极低的延迟处理数百万级的任务调度,同时保持高吞吐量和低CPU占用。

1-2.【GCD】GCD 是如何管理线程池的?线程是谁创建的?

作者 项阿丑
2026年1月27日 17:36

线程创建者:GCD 自己

GCD 是线程的实际创建者,但不是随意创建,而是通过高度优化的机制:

// 开发者层面:不直接创建线程
DispatchQueue.global().async {
    // GCD 内部决定:
    // 1. 从池中取出空闲线程
    // 2. 或创建新线程(如果必要)
    // 3. 或将任务排队等待
}

GCD 线程池的智能管理

1. 按需创建,延迟销毁

// GCD 线程生命周期:
// 初始状态:线程池为空
// 第一次提交任务 → 创建1个线程
// 持续提交任务 → 逐渐增加线程数
// 空闲一段时间 → 自动销毁多余线程(节省资源)

2. 线程复用策略

// 类似数据库连接池的模式
class GCDThreadPool {
    private var activeThreads: Set<Thread> = []
    private var idleThreads: Set<Thread> = []
    private var maxThreads: Int
    
    func getThread() -> Thread {
        if let thread = idleThreads.popFirst() {
            // 复用空闲线程
            activeThreads.insert(thread)
            return thread
        } else if activeThreads.count < maxThreads {
            // 创建新线程
            let thread = createThread()
            activeThreads.insert(thread)
            return thread
        } else {
            // 等待线程可用
            return waitForAvailableThread()
        }
    }
}

线程池的关键参数和策略

1. 线程数量限制

// 基于系统资源动态调整
class GCDThreadManager {
    // 主要考虑因素:
    // 1. CPU 核心数(决定最大并发度)
    let maxConcurrentThreads = ProcessInfo.processInfo.processorCount * 2
    
    // 2. 队列类型
    // 串行队列:通常1个线程
    // 并发队列:多个线程,但有限制
    
    // 3. 系统负载
    // 高负载时:减少线程数
    // 低负载时:增加线程数(更快响应)
}

2. QoS(服务质量)影响

// 不同优先级的任务使用不同线程池
DispatchQueue.global(qos: .userInteractive) // 最高优先级,更快获取线程
DispatchQueue.global(qos: .background)      // 最低优先级,可能等待更久

// 内部实现简化:
class QoSThreadPool {
    var highPriorityPool: ThreadPool  // .userInteractive, .userInitiated
    var defaultPool: ThreadPool        // .default
    var lowPriorityPool: ThreadPool    // .utility, .background
    
    func getThread(for qos: QoSClass) -> Thread {
        // 优先从对应优先级的池中获取
        // 高优先级可"借用"低优先级的线程(反之不行)
    }
}

具体工作机制示例

场景:处理大量任务

let queue = DispatchQueue.global()

// 模拟100个任务
for i in 1...100 {
    queue.async {
        sleep(1)  // 模拟1秒工作
        print("任务 (i) 完成,线程: (Thread.current)")
    }
}

// GCD 内部行为:
// 1. 前几个任务:创建新线程(比如4个,基于CPU核心数)
// 2. 继续提交:复用现有线程
// 3. 如果所有线程都忙:排队等待
// 4. 如果长时间排队:可能创建更多线程(但不超过上限)
// 5. 任务完成后:线程空闲,可用于新任务
// 6. 长时间空闲:销毁多余线程

避免的问题和优化

1. 防止线程爆炸

// 传统方式的问题
for _ in 0..<1000 {
    Thread { /* 工作 */ }.start()  // 可能创建1000个线程!
}

// GCD 的优化
let queue = DispatchQueue.global()
for _ in 0..<1000 {
    queue.async { /* 工作 */ }  // 可能只用8-16个线程处理
}

2. 负载均衡

// GCD 内部的工作窃取(Work Stealing)机制
class WorkStealingScheduler {
    var threadPools: [ThreadPool]
    
    func scheduleTask(_ task: Task) {
        // 1. 先尝试分配到最空闲的线程
        // 2. 如果所有线程都忙,但其他队列有闲置线程
        // 3. "窃取"闲置线程来执行任务
    }
}

3. 能耗优化(特别在移动设备)

// iOS/macOS 的能耗管理
class EnergyAwareThreadManager {
    func manageThreads() {
        // 考虑因素:
        // - CPU 频率调整(降频时减少线程)
        // - 电池状态(低电量时减少并发)
        // - 温度控制(过热时限制线程)
        // - 后台状态(后台模式用更少线程)
    }
}

线程生命周期管理

// 简化版 GCD 线程管理逻辑
class GCDThread {
    var state: ThreadState = .idle
    var lastUsed: TimeInterval = 0
    
    enum ThreadState {
        case idle      // 空闲,等待任务
        case active    // 正在执行任务
        case sleeping  // 休眠(可能被销毁)
    }
    
    func run() {
        while true {
            if let task = getTaskFromQueue() {
                state = .active
                task.execute()
                state = .idle
                lastUsed = currentTime()
            } else {
                // 空闲处理
                if shouldDestroyThread() {
                    cleanupAndExit()
                } else {
                    sleepForInterval()
                }
            }
        }
    }
}

特殊情况处理

1. 主线程的特殊性

// 主队列绑定到主线程
DispatchQueue.main.async {
    // 总是运行在主线程
    // GCD 不会为 main queue 创建新线程
    // 而是将任务提交到主线程的 RunLoop
}

// 主线程不是由 GCD 创建的
// 它是应用启动时由系统创建的

2. 同步执行的优化

// sync 调用时的线程优化
queue.sync {
    // 可能的优化:
    // 1. 如果当前线程就是目标队列的执行线程
    // 2. 直接在当前线程执行(避免线程切换)
    // 3. 这称为"队列归属检测"
}

实际调试观察

// 查看线程创建情况
let concurrentQueue = DispatchQueue(label: "test", attributes: .concurrent)

print("初始线程数: (Thread.getAll().count)")

for i in 0..<20 {
    concurrentQueue.async {
        print("任务 (i): (Thread.current)")
        usleep(100_000)  // 100ms
    }
}

// 观察结果:
// 1. 线程数不会无限增长
// 2. 线程会被复用(看到相同的 thread number)
// 3. 最终稳定在一定数量(如 CPU 核心数*2)

总结

GCD 线程管理的关键特点:

  1. 线程创建者:GCD 自己(通过系统调用),开发者不直接创建
  2. 按需创建:首次需要时创建,不是预先创建大量线程
  3. 智能复用:线程执行完任务后回到池中,等待下次使用
  4. 动态调整:根据系统负载、CPU核心数、任务类型动态调整线程数
  5. 上限控制:有最大线程数限制,防止线程爆炸
  6. 自动清理:空闲线程会被销毁,节省资源
  7. 优先级感知:不同 QoS 的任务可能使用不同的线程子池

这种设计使得 GCD 能够:

  • 最大化 CPU 利用率
  • 最小化线程创建/销毁开销
  • 自动适应不同硬件和负载
  • 为开发者隐藏复杂的线程管理细节

简单比喻:GCD 就像一个"智能线程管家",你只需要告诉它"要做什么工作"和"工作的重要程度",它会自动安排最合适的"工人"(线程)来完成,同时保证不雇佣过多工人浪费资源。

👉 追问:GCD 线程池和 pthread / NSThread 的关系?

GCD 线程池、pthread 和 NSThread 之间有清晰的层次关系,pthread 是底层基础,GCD 和 NSThread 都是基于它的高层抽象

层级关系

// 从高到低的抽象层次:
┌─────────────────────────┐
│   GCD (DispatchQueue)   │ ← 最高抽象:任务队列,自动线程管理
├─────────────────────────┤
│        NSThread         │ ← 中级抽象:面向对象的线程封装
├─────────────────────────┤
│       pthread_t         │ ← 底层抽象:POSIX线程,C语言API
└─────────────────────────┘

详细关系解析

1. pthread_t:最底层的基础

// 这是所有线程的根基(包括GCD创建的线程)
#include <pthread.h>

pthread_t thread;
pthread_create(&thread, NULL, worker_func, NULL);  // 创建线程

// GCD 内部最终会调用这个函数来创建线程
// 实际上,macOS/iOS 中的所有线程都是 pthread

2. NSThread:Objective-C 的封装

// NSThread 是对 pthread 的面向对象包装
class NSThread {
    // 内部持有 pthread_t
    private var _pthread: pthread_t
    
    // 创建线程时,内部调用 pthread_create()
    init(block: @escaping () -> Void) {
        _pthread = pthread_create(...)
    }
}

// 验证关系:
Thread.current // 返回当前线程的 NSThread 对象
pthread_self() // 返回当前线程的 pthread_t

// 实际上,Thread.current.pthread 可以获取底层 pthread_t
// (虽然这个属性不公开)

3. GCD 线程池的实现

// GCD 内部结构示意(简化版)
class GCDThreadPool {
    private var threads: [pthread_t] = []
    private var taskQueue: Queue<Task>
    
    func createThreadIfNeeded() {
        // 需要新线程时,创建 pthread
        var thread: pthread_t
        pthread_create(&thread, nil, { context in
            // 线程函数:不断从队列取任务执行
            while let task = taskQueue.dequeue() {
                task.execute()
            }
            return nil
        }, nil)
        
        threads.append(thread)
    }
    
    func execute(_ task: Task) {
        taskQueue.enqueue(task)
        // 如果没有空闲线程且未达上限,创建新线程
        if idleThreads.isEmpty && threads.count < maxThreads {
            createThreadIfNeeded()
        }
    }
}

实际运行时关系示例

场景:观察三者关系

// 创建一个 GCD 并发队列
let queue = DispatchQueue(label: "test", attributes: .concurrent)

// 提交任务
queue.async {
    // 获取三个层面的线程信息
    let nsThread = Thread.current        // NSThread 对象
    let pthreadId = pthread_self()       // pthread_t 标识
    let threadNumber = nsThread.value(forKeyPath: "private.seqNum")  // 内部编号
    
    print("""
    层级关系:
    1. NSThread: (nsThread)
    2. pthread_t: (pthreadId)
    3. 是否GCD创建: (nsThread.name?.contains("com.apple.root") == true)
    """)
    
    // 实际输出可能类似:
    // 1. NSThread: <NSThread: 0x600003d6c040>{number = 7, name = (null)}
    // 2. pthread_t: 0x70000a1000
    // 3. 是否GCD创建: true
}

核心区别对比

特性 GCD 线程池 NSThread pthread_t
抽象级别 最高(队列) 中(对象) 最低(句柄)
创建方式 自动管理 手动创建 手动创建
线程复用 ✅ 自动复用 ❌ 一对一 ❌ 一对一
内存管理 自动 ARC 管理 手动(pthread_join/exit)
跨平台 Apple 生态 Apple 生态 POSIX标准

具体实现细节

1. GCD 如何创建线程

// GCD 内部源码简化示意(libdispatch)
void _dispatch_worker_thread(void *context) {
    // 1. 注册为GCD工作线程
    _dispatch_thread_setspecific(dispatch_queue_key, context);
    
    // 2. 设置线程名字(便于调试)
    pthread_setname_np("com.apple.root.default-qos");
    
    // 3. 进入工作循环
    while (1) {
        // 从队列获取任务
        task = _dispatch_queue_get_task(queue);
        
        if (task) {
            _dispatch_worker_execute(task);
        } else {
            // 空闲处理
            if (should_terminate()) {
                pthread_exit(NULL);
            }
        }
    }
}

// 创建线程的函数
void _dispatch_thread_create(pthread_t *thread, dispatch_queue_t queue) {
    pthread_attr_t attr;
    pthread_attr_init(&attr);
    
    // 配置线程属性
    pthread_attr_set_qos_class_np(&attr, QOS_CLASS_DEFAULT, 0);
    
    // 最终调用 pthread_create
    pthread_create(thread, &attr, _dispatch_worker_thread, (void *)queue);
}

2. NSThread 的 pthread 包装

// NSThread 内部实现示意
@implementation NSThread {
    pthread_t _pthread;
    NSMutableDictionary *_threadDictionary;
}

- (void)start {
    if (_pthread != NULL) return;
    
    // 创建 pthread
    pthread_attr_t attr;
    pthread_attr_init(&attr);
    
    // 存储 self 以便在 C 函数中访问
    NSThread *threadSelf = self;
    
    int err = pthread_create(&_pthread, &attr,
                           _NSThread__start__, (__bridge void *)threadSelf);
    
    pthread_attr_destroy(&attr);
}

static void *_NSThread__start__(void *arg) {
    // 获取 NSThread 对象
    NSThread *thread = (__bridge NSThread *)arg;
    
    // 设置线程特定数据
    pthread_setspecific(NSThreadKey, (__bridge void *)thread);
    
    // 执行目标方法
    [thread main];
    
    return NULL;
}
@end

实际开发中的交互

1. 从 NSThread 获取 pthread

// 方法1:直接获取当前 pthread
let currentPthread = pthread_self()

// 方法2:从 NSThread(不推荐,使用私有API)
extension Thread {
    var pthread: pthread_t? {
        // 注意:这是私有API,App Store审核可能不通过
        let key = "pthread"  // 实际是私有字段
        return self.value(forKey: key) as? pthread_t
    }
}

2. 在 GCD 任务中操作线程

DispatchQueue.global().async {
    // 当前运行在 GCD 管理的 pthread 上
    
    // 可以设置线程名字(用于调试)
    pthread_setname_np("MyWorkerThread")
    
    // 可以获取并操作当前 pthread
    let pthread = pthread_self()
    
    // 可以设置线程优先级(但通常用 QoS 更好)
    var policy: Int32 = 0
    var param = sched_param()
    pthread_getschedparam(pthread, &policy, &param)
    param.sched_priority = 45  // 设置优先级
    pthread_setschedparam(pthread, policy, &param)
    
    // 注意:直接操作 pthread 可能干扰 GCD 的管理
}

3. 混合使用的陷阱

// ❌ 危险的混合使用
let thread = Thread {
    // 在线程中使用 GCD 同步到主队列
    DispatchQueue.main.sync {
        // 如果这个线程恰好是主线程 → 死锁!
        // 因为 Thread 创建的不是主线程,
        // 但理论上如果是主线程调用就会死锁
    }
}

// ✅ 安全的做法
let queue = DispatchQueue.global()
queue.async {
    // 在 GCD 管理的线程中使用 GCD
    DispatchQueue.main.async {
        // 总是安全的
    }
}

性能与开销对比

// 性能测试示例
func measureOverhead() {
    // 1. 直接创建 pthread(最快,但最底层)
    measure("pthread_create") {
        var thread: pthread_t
        pthread_create(&thread, nil, { _ in return nil }, nil)
        pthread_join(thread, nil)
    }
    
    // 2. 创建 NSThread(中等开销)
    measure("NSThread") {
        let thread = Thread { /* do nothing */ }
        thread.start()
        // 需要同步机制确保线程结束
    }
    
    // 3. GCD 提交任务(最低开销,因为复用线程)
    measure("GCD async") {
        let semaphore = DispatchSemaphore(value: 0)
        DispatchQueue.global().async {
            semaphore.signal()
        }
        semaphore.wait()
    }
}

// 典型结果(相对时间):
// pthread_create: 100ms(基准)
// NSThread: 120-150ms(额外对象开销)
// GCD async: 10-20ms(线程复用,极低开销)

调试技巧

1. 查看线程信息

// 在调试器中查看所有线程
(lldb) thread list
// 可以看到:
// * thread #1: tid = 0x1a2b3, 0x0000000100000000, 
//   queue = 'com.apple.main-thread'
// * thread #4: tid = 0x1a2b4, 0x0000000100001000,
//   queue = 'com.apple.root.default-qos'

// 查看线程堆栈
(lldb) bt all
// 会显示所有线程的调用栈,包括GCD工作线程

2. 识别线程来源

// 在代码中判断
if Thread.isMainThread {
    print("这是主线程")
}

// 判断是否为GCD线程
let threadName = Thread.current.name ?? ""
if threadName.hasPrefix("com.apple.root") {
    print("这是GCD线程池中的线程")
}

// 获取线程的QoS级别
let qos = Thread.current.qualityOfService
print("线程QoS: (qos)")

总结

关系链

应用层:GCD (DispatchQueue) → 使用并管理 → NSThread/pthread
          ↓
框架层:NSThread → 封装 → pthread_t
          ↓
系统层:pthread_t → 内核线程

核心要点

  1. pthread 是基石:所有线程最终都是 pthread,包括 GCD 创建的线程

  2. NSThread 是包装:提供面向对象的线程操作,底层仍是 pthread

  3. GCD 是管理者:基于 pthread 构建线程池,提供队列抽象

  4. 线程复用是关键:GCD 复用线程,而直接使用 pthread/NSThread 通常是一对一

  5. 抽象层级不同

    • pthread:关心"如何创建线程"
    • NSThread:关心"如何表示线程对象"
    • GCD:关心"如何组织任务执行"

简单比喻

  • pthread 就像"砖块"(基本单元)
  • NSThread 就像"有标签的砖块"(易于识别和管理)
  • GCD 就像"智能建筑系统"(自动调度砖块完成任务)

在实践中,优先使用 GCD,它提供最优的线程管理和性能。只有在需要特殊线程控制(如长时间后台计算、实时操作)时才考虑使用 NSThread 或 pthread。

❌
❌