普通视图

发现新文章,点击刷新页面。
今天 — 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内核相关头文件,用于线程操作
#import <mach/mach.h>
// 动态链接库相关函数,用于符号解析
#include <dlfcn.h>
// POSIX线程库
#include <pthread.h>
// 系统类型定义
#include <sys/types.h>
// 限制值定义(如ULONG_MAX等)
#include <limits.h>
// 字符串操作函数
#include <string.h>
// 动态链接器相关,用于获取镜像信息
#include <mach-o/dyld.h>
// Mach-O符号表结构定义
#include <mach-o/nlist.h>

#pragma -mark DEFINE MACRO FOR DIFFERENT CPU ARCHITECTURE
// 针对不同CPU架构定义相应的宏,以适配不同硬件平台

// ARM64架构(64位ARM,如iPhone 5s及以后的设备)
#if defined(__arm64__)
// 去除指令地址的标签位(ARM64中低2位可能被用作标签)
#define DETAG_INSTRUCTION_ADDRESS(A) ((A) & ~(3UL))
// 线程状态结构体的大小
#define BS_THREAD_STATE_COUNT ARM_THREAD_STATE64_COUNT
// 线程状态类型
#define BS_THREAD_STATE ARM_THREAD_STATE64
// 帧指针寄存器(Frame Pointer)
#define BS_FRAME_POINTER __fp
// 栈指针寄存器(Stack Pointer)
#define BS_STACK_POINTER __sp
// 程序计数器/指令指针寄存器(Program Counter)
#define BS_INSTRUCTION_ADDRESS __pc

// ARM32架构(32位ARM,旧设备)
#elif defined(__arm__)
// 去除指令地址的标签位(ARM32中低1位可能表示Thumb模式)
#define DETAG_INSTRUCTION_ADDRESS(A) ((A) & ~(1UL))
// 线程状态结构体的大小
#define BS_THREAD_STATE_COUNT ARM_THREAD_STATE_COUNT
// 线程状态类型
#define BS_THREAD_STATE ARM_THREAD_STATE
// 帧指针寄存器(r7寄存器)
#define BS_FRAME_POINTER __r[7]
// 栈指针寄存器
#define BS_STACK_POINTER __sp
// 程序计数器寄存器
#define BS_INSTRUCTION_ADDRESS __pc

// x86_64架构(64位Intel,模拟器或Mac)
#elif defined(__x86_64__)
// x86架构不需要去标签,直接返回地址
#define DETAG_INSTRUCTION_ADDRESS(A) (A)
// 线程状态结构体的大小
#define BS_THREAD_STATE_COUNT x86_THREAD_STATE64_COUNT
// 线程状态类型
#define BS_THREAD_STATE x86_THREAD_STATE64
// 帧指针寄存器(Base Pointer)
#define BS_FRAME_POINTER __rbp
// 栈指针寄存器
#define BS_STACK_POINTER __rsp
// 指令指针寄存器(Instruction Pointer)
#define BS_INSTRUCTION_ADDRESS __rip

// i386架构(32位Intel,旧模拟器)
#elif defined(__i386__)
// x86架构不需要去标签
#define DETAG_INSTRUCTION_ADDRESS(A) (A)
// 线程状态结构体的大小
#define BS_THREAD_STATE_COUNT x86_THREAD_STATE32_COUNT
// 线程状态类型
#define BS_THREAD_STATE x86_THREAD_STATE32
// 帧指针寄存器
#define BS_FRAME_POINTER __ebp
// 栈指针寄存器
#define BS_STACK_POINTER __esp
// 指令指针寄存器
#define BS_INSTRUCTION_ADDRESS __eip

#endif

// 从返回地址计算调用指令地址(返回地址-1即为调用指令)
#define CALL_INSTRUCTION_FROM_RETURN_ADDRESS(A) (DETAG_INSTRUCTION_ADDRESS((A)) - 1)

// 根据指针大小(32位或64位)定义不同的格式化字符串
#if defined(__LP64__)
// 64位系统的格式定义
// 堆栈跟踪条目格式:序号(4位) 模块名(31位) 地址(16位十六进制) 符号名 + 偏移量
#define TRACE_FMT         "%-4d%-31s 0x%016lx %s + %lu"
// 指针完整格式(16位十六进制)
#define POINTER_FMT       "0x%016lx"
// 指针短格式
#define POINTER_SHORT_FMT "0x%lx"
// 符号表结构体类型(64位)
#define BS_NLIST struct nlist_64
#else
// 32位系统的格式定义
// 堆栈跟踪条目格式:序号(4位) 模块名(31位) 地址(8位十六进制) 符号名 + 偏移量
#define TRACE_FMT         "%-4d%-31s 0x%08lx %s + %lu"
// 指针完整格式(8位十六进制)
#define POINTER_FMT       "0x%08lx"
// 指针短格式
#define POINTER_SHORT_FMT "0x%lx"
// 符号表结构体类型(32位)
#define BS_NLIST struct nlist
#endif

// 定义栈帧结构体,用于遍历函数调用栈
typedef struct BSStackFrameEntry{
    // 指向前一个栈帧的指针(形成链表结构)
    const struct BSStackFrameEntry *const previous;
    // 当前栈帧的返回地址(即调用者的下一条指令地址)
    const uintptr_t return_address;
} BSStackFrameEntry;

// 静态全局变量,保存主线程的Mach端口ID
static mach_port_t main_thread_id;

@implementation BSBacktraceLogger

// +load方法在类加载时自动调用,早于main函数执行
+ (void)load {
    // 保存主线程的Mach线程ID,用于后续识别主线程
    main_thread_id = mach_thread_self();
}

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

// 获取指定NSThread的调用栈回溯信息
+ (NSString *)bs_backtraceOfNSThread:(NSThread *)thread {
    // 将NSThread转换为Mach线程,然后获取其回溯信息
    return _bs_backtraceOfThread(bs_machThreadFromNSThread(thread));
}

// 获取当前线程的调用栈回溯信息
+ (NSString *)bs_backtraceOfCurrentThread {
    // 获取当前线程对象并调用通用方法
    return [self bs_backtraceOfNSThread:[NSThread currentThread]];
}

// 获取主线程的调用栈回溯信息
+ (NSString *)bs_backtraceOfMainThread {
    // 获取主线程对象并调用通用方法
    return [self bs_backtraceOfNSThread:[NSThread mainThread]];
}

// 获取所有线程的调用栈回溯信息
+ (NSString *)bs_backtraceOfAllThread {
    // 用于存储线程列表的数组
    thread_act_array_t threads;
    // 线程数量
    mach_msg_type_number_t thread_count = 0;
    // 获取当前任务(进程)
    const task_t this_task = mach_task_self();
    
    // 获取当前任务的所有线程列表
    kern_return_t kr = task_threads(this_task, &threads, &thread_count);
    // 如果获取失败,返回错误信息
    if(kr != KERN_SUCCESS) {
        return @"Fail to get information of all threads";
    }
    
    // 创建可变字符串,用于拼接所有线程的回溯信息
    NSMutableString *resultString = [NSMutableString stringWithFormat:@"Call Backtrace of %u threads:\n", thread_count];
    // 遍历所有线程
    for(int i = 0; i < thread_count; i++) {
        // 获取每个线程的回溯信息并追加到结果字符串
        [resultString appendString:_bs_backtraceOfThread(threads[i])];
    }
    // 返回不可变副本
    return [resultString copy];
}

#pragma -mark Get call backtrace of a mach_thread
// 获取指定Mach线程的调用栈回溯(核心函数)
NSString *_bs_backtraceOfThread(thread_t thread) {
    // 创建缓冲区,最多存储50层函数调用栈
    uintptr_t backtraceBuffer[50];
    // 当前栈帧索引
    int i = 0;
    // 创建可变字符串用于存储回溯信息
    NSMutableString *resultString = [[NSMutableString alloc] initWithFormat:@"Backtrace of Thread %u:\n", thread];
    
    // 机器上下文结构体,用于存储线程的寄存器状态
    _STRUCT_MCONTEXT machineContext;
    // 获取线程的机器上下文(寄存器状态)
    if(!bs_fillThreadStateIntoMachineContext(thread, &machineContext)) {
        // 如果获取失败,返回错误信息
        return [NSString stringWithFormat:@"Fail to get information about thread: %u", thread];
    }
    
    // 获取当前指令地址(PC寄存器的值)
    const uintptr_t instructionAddress = bs_mach_instructionAddress(&machineContext);
    // 将指令地址作为第一个回溯地址
    backtraceBuffer[i] = instructionAddress;
    // 索引递增
    ++i;
    
    // 获取链接寄存器的值(ARM架构特有,保存函数返回地址)
    uintptr_t linkRegister = bs_mach_linkRegister(&machineContext);
    // 如果链接寄存器有值(非x86架构)
    if (linkRegister) {
        // 将链接寄存器的值作为第二个回溯地址
        backtraceBuffer[i] = linkRegister;
        // 索引递增
        i++;
    }
    
    // 如果指令地址为0,说明获取失败
    if(instructionAddress == 0) {
        return @"Fail to get instruction address";
    }
    
    // 初始化栈帧结构体
    BSStackFrameEntry frame = {0};
    // 获取帧指针(FP寄存器的值,指向当前栈帧)
    const uintptr_t framePtr = bs_mach_framePointer(&machineContext);
    // 如果帧指针为0,或者无法从该地址读取栈帧数据
    if(framePtr == 0 ||
       bs_mach_copyMem((void *)framePtr, &frame, sizeof(frame)) != KERN_SUCCESS) {
        // 返回错误信息
        return @"Fail to get frame pointer";
    }
    
    // 遍历栈帧链表,最多50层
    for(; i < 50; i++) {
        // 保存当前栈帧的返回地址
        backtraceBuffer[i] = frame.return_address;
        // 如果返回地址为0,或者前一个栈帧指针为0,或者无法读取前一个栈帧
        if(backtraceBuffer[i] == 0 ||
           frame.previous == 0 ||
           bs_mach_copyMem(frame.previous, &frame, sizeof(frame)) != KERN_SUCCESS) {
            // 跳出循环,栈帧遍历结束
            break;
        }
    }
    
    // 记录实际获取的回溯层数
    int backtraceLength = i;
    // 创建符号信息数组,用于存储每个地址对应的符号信息
    Dl_info symbolicated[backtraceLength];
    // 对回溯地址进行符号化(将地址转换为函数名、模块名等)
    bs_symbolicate(backtraceBuffer, symbolicated, backtraceLength, 0);
    // 遍历所有回溯地址
    for (int i = 0; i < backtraceLength; ++i) {
        // 格式化每一条回溯信息并追加到结果字符串
        [resultString appendFormat:@"%@", bs_logBacktraceEntry(i, backtraceBuffer[i], &symbolicated[i])];
    }
    // 追加换行符
    [resultString appendFormat:@"\n"];
    // 返回不可变副本
    return [resultString copy];
}

#pragma -mark Convert NSThread to Mach thread
// 将NSThread对象转换为Mach线程ID
// 这个函数通过临时修改线程名称来匹配NSThread和Mach线程
thread_t bs_machThreadFromNSThread(NSThread *nsthread) {
    // 用于存储线程名称的缓冲区
    char name[256];
    // 线程数量
    mach_msg_type_number_t count;
    // 线程列表数组
    thread_act_array_t list;
    // 获取当前任务的所有线程
    task_threads(mach_task_self(), &list, &count);
    
    // 获取当前时间戳,用作临时线程名称(确保唯一性)
    NSTimeInterval currentTimestamp = [[NSDate date] timeIntervalSince1970];
    // 保存原始线程名称
    NSString *originName = [nsthread name];
    // 将线程名称临时设置为时间戳字符串
    [nsthread setName:[NSString stringWithFormat:@"%f", currentTimestamp]];
    
    // 如果是主线程,直接返回保存的主线程ID
    if ([nsthread isMainThread]) {
        return (thread_t)main_thread_id;
    }
    
    // 遍历所有Mach线程
    for (int i = 0; i < count; ++i) {
        // 将Mach线程转换为POSIX线程(pthread)
        pthread_t pt = pthread_from_mach_thread_np(list[i]);
        // 再次检查是否为主线程(冗余检查,实际上这个条件永远不会满足)
        if ([nsthread isMainThread]) {
            // 如果Mach线程ID匹配主线程ID
            if (list[i] == main_thread_id) {
                return list[i];
            }
        }
        // 如果pthread转换成功
        if (pt) {
            // 清空名称缓冲区
            name[0] = '\0';
            // 获取pthread的线程名称
            pthread_getname_np(pt, name, sizeof name);
            // 比较线程名称是否匹配(即是否为我们临时设置的时间戳)
            if (!strcmp(name, [nsthread name].UTF8String)) {
                // 恢复原始线程名称
                [nsthread setName:originName];
                // 返回匹配的Mach线程ID
                return list[i];
            }
        }
    }
    
    // 如果没有找到匹配的线程,恢复原始名称
    [nsthread setName:originName];
    // 返回当前线程ID(作为后备方案)
    return mach_thread_self();
}

#pragma -mark GenerateBacbsrackEnrty
// 生成单条回溯日志条目
// 参数:条目序号、地址、符号信息
NSString* bs_logBacktraceEntry(const int entryNum,
                               const uintptr_t address,
                               const Dl_info* const dlInfo) {
    // 文件地址缓冲区(当文件名为空时用于存储地址)
    char faddrBuff[20];
    // 符号地址缓冲区(当符号名为空时用于存储地址)
    char saddrBuff[20];
    
    // 从完整路径中提取文件名(只保留最后一段)
    const char* fname = bs_lastPathEntry(dlInfo->dli_fname);
    // 如果文件名为空(未找到符号信息)
    if(fname == NULL) {
        // 将文件基地址格式化为字符串
        sprintf(faddrBuff, POINTER_FMT, (uintptr_t)dlInfo->dli_fbase);
        // 使用地址作为文件名
        fname = faddrBuff;
    }
    
    // 计算地址相对于符号地址的偏移量
    uintptr_t offset = address - (uintptr_t)dlInfo->dli_saddr;
    // 获取符号名称(函数名)
    const char* sname = dlInfo->dli_sname;
    // 如果符号名称为空
    if(sname == NULL) {
        // 将文件基地址格式化为字符串
        sprintf(saddrBuff, POINTER_SHORT_FMT, (uintptr_t)dlInfo->dli_fbase);
        // 使用地址作为符号名
        sname = saddrBuff;
        // 重新计算偏移量(相对于文件基地址)
        offset = address - (uintptr_t)dlInfo->dli_fbase;
    }
    // 格式化输出:模块名(30位对齐) 地址 符号名 + 偏移量
    return [NSString stringWithFormat:@"%-30s  0x%08" PRIxPTR " %s + %lu\n" ,fname, (uintptr_t)address, sname, offset];
}

// 从完整路径中提取最后一部分(文件名)
const char* bs_lastPathEntry(const char* const path) {
    // 如果路径为空,返回NULL
    if(path == NULL) {
        return NULL;
    }
    
    // 查找最后一个'/'字符的位置
    char* lastFile = strrchr(path, '/');
    // 如果没有找到'/',说明path本身就是文件名;否则返回'/'后面的部分
    return lastFile == NULL ? path : lastFile + 1;
}

#pragma -mark HandleMachineContext
// 机器上下文处理相关函数

// 获取线程状态并填充到机器上下文结构体中
bool bs_fillThreadStateIntoMachineContext(thread_t thread, _STRUCT_MCONTEXT *machineContext) {
    // 状态结构体的大小(不同架构大小不同)
    mach_msg_type_number_t state_count = BS_THREAD_STATE_COUNT;
    // 调用内核函数获取线程状态(寄存器快照)
    kern_return_t kr = thread_get_state(thread, BS_THREAD_STATE, (thread_state_t)&machineContext->__ss, &state_count);
    // 返回是否成功获取
    return (kr == KERN_SUCCESS);
}

// 从机器上下文中提取帧指针寄存器的值
uintptr_t bs_mach_framePointer(mcontext_t const machineContext){
    // 通过宏定义访问对应架构的帧指针寄存器
    return machineContext->__ss.BS_FRAME_POINTER;
}

// 从机器上下文中提取栈指针寄存器的值
uintptr_t bs_mach_stackPointer(mcontext_t const machineContext){
    // 通过宏定义访问对应架构的栈指针寄存器
    return machineContext->__ss.BS_STACK_POINTER;
}

// 从机器上下文中提取指令指针/程序计数器的值
uintptr_t bs_mach_instructionAddress(mcontext_t const machineContext){
    // 通过宏定义访问对应架构的指令地址寄存器
    return machineContext->__ss.BS_INSTRUCTION_ADDRESS;
}

// 从机器上下文中提取链接寄存器的值(仅ARM架构有效)
uintptr_t bs_mach_linkRegister(mcontext_t const machineContext){
#if defined(__i386__) || defined(__x86_64__)
    // x86架构没有链接寄存器,返回0
    return 0;
#else
    // ARM架构返回LR寄存器的值
    return machineContext->__ss.__lr;
#endif
}

// 从指定内存地址复制数据到目标地址
// 使用vm_read_overwrite确保可以安全读取其他线程的内存
kern_return_t bs_mach_copyMem(const void *const src, void *const dst, const size_t numBytes){
    // 实际复制的字节数
    vm_size_t bytesCopied = 0;
    // 使用虚拟内存读取覆写函数,可以跨线程读取内存
    return vm_read_overwrite(mach_task_self(), (vm_address_t)src, (vm_size_t)numBytes, (vm_address_t)dst, &bytesCopied);
}

#pragma -mark Symbolicate
// 符号化相关函数:将内存地址转换为可读的符号信息

// 对回溯地址数组进行符号化处理
// 参数:回溯地址缓冲区、符号信息缓冲区、条目数量、跳过的条目数
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) {
        // 第一个条目(当前指令地址)直接符号化,不需要调整
        bs_dladdr(backtraceBuffer[i], &symbolsBuffer[i]);
        // 索引递增
        i++;
    }
    
    // 处理剩余的返回地址
    for(; i < numEntries; i++) {
        // 返回地址需要减1才能得到调用指令的地址,然后进行符号化
        // 因为返回地址指向的是调用后的下一条指令
        bs_dladdr(CALL_INSTRUCTION_FROM_RETURN_ADDRESS(backtraceBuffer[i]), &symbolsBuffer[i]);
    }
}

// 自定义实现的dladdr函数,用于将地址转换为符号信息
// 这是一个手动解析Mach-O文件的符号表来查找符号的过程
bool bs_dladdr(const uintptr_t address, Dl_info* const info) {
    // 初始化符号信息结构体的所有字段为NULL
    info->dli_fname = NULL; // 文件名
    info->dli_fbase = NULL; // 文件基地址
    info->dli_sname = NULL; // 符号名
    info->dli_saddr = NULL; // 符号地址
    
    // 查找包含该地址的镜像索引
    const uint32_t idx = bs_imageIndexContainingAddress(address);
    // 如果没找到对应的镜像,返回false
    if(idx == UINT_MAX) {
        return false;
    }
    // 获取镜像的Mach-O头部
    const struct mach_header* header = _dyld_get_image_header(idx);
    // 获取镜像的虚拟内存地址偏移(ASLR地址随机化偏移)
    const uintptr_t imageVMAddrSlide = (uintptr_t)_dyld_get_image_vmaddr_slide(idx);
    // 计算去除偏移后的地址(文件中的原始地址)
    const uintptr_t addressWithSlide = address - imageVMAddrSlide;
    // 获取段基地址(用于定位符号表在内存中的位置)
    const uintptr_t segmentBase = bs_segmentBaseOfImageIndex(idx) + imageVMAddrSlide;
    // 如果段基地址为0,返回false
    if(segmentBase == 0) {
        return false;
    }
    
    // 填充文件名和基地址信息
    info->dli_fname = _dyld_get_image_name(idx);
    info->dli_fbase = (void*)header;
    
    // 查找符号表并获取最接近该地址的符号
    // 最佳匹配的符号表项
    const BS_NLIST* bestMatch = NULL;
    // 最小距离初始化为最大值
    uintptr_t bestDistance = ULONG_MAX;
    // 获取Mach-O头部后的第一个加载命令地址
    uintptr_t cmdPtr = bs_firstCmdAfterHeader(header);
    // 如果命令指针为0,说明头部损坏
    if(cmdPtr == 0) {
        return false;
    }
    // 遍历所有加载命令
    for(uint32_t iCmd = 0; iCmd < header->ncmds; iCmd++) {
        // 当前加载命令
        const struct load_command* loadCmd = (struct load_command*)cmdPtr;
        // 如果是符号表命令
        if(loadCmd->cmd == LC_SYMTAB) {
            // 转换为符号表命令结构体
            const struct symtab_command* symtabCmd = (struct symtab_command*)cmdPtr;
            // 计算符号表在内存中的地址
            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++) {
                // 如果n_value为0,说明该符号引用外部对象,跳过
                if(symbolTable[iSym].n_value != 0) {
                    // 符号的基地址
                    uintptr_t symbolBase = symbolTable[iSym].n_value;
                    // 计算地址与符号基地址的距离
                    uintptr_t currentDistance = addressWithSlide - symbolBase;
                    // 如果地址大于等于符号基地址,且距离小于等于当前最佳距离
                    if((addressWithSlide >= symbolBase) &&
                       (currentDistance <= bestDistance)) {
                        // 更新最佳匹配
                        bestMatch = symbolTable + iSym;
                        // 更新最小距离
                        bestDistance = currentDistance;
                    }
                }
            }
            // 如果找到了匹配的符号
            if(bestMatch != NULL) {
                // 填充符号地址(需要加上ASLR偏移)
                info->dli_saddr = (void*)(bestMatch->n_value + imageVMAddrSlide);
                // 从字符串表中获取符号名称
                info->dli_sname = (char*)((intptr_t)stringTable + (intptr_t)bestMatch->n_un.n_strx);
                // 如果符号名以下划线开头(C/C++符号的命名约定),跳过下划线
                if(*info->dli_sname == '_') {
                    info->dli_sname++;
                }
                // 如果符号地址等于文件基地址且类型为3,说明符号已被剥离
                if(info->dli_saddr == info->dli_fbase && bestMatch->n_type == 3) {
                    info->dli_sname = NULL;
                }
                // 找到符号后跳出循环
                break;
            }
        }
        // 移动到下一个加载命令
        cmdPtr += loadCmd->cmdsize;
    }
    // 返回成功
    return true;
}

// 获取Mach-O头部后的第一个加载命令地址
uintptr_t bs_firstCmdAfterHeader(const struct mach_header* const header) {
    // 根据魔数判断Mach-O文件的类型和字节序
    switch(header->magic) {
        case MH_MAGIC:    // 32位小端序
        case MH_CIGAM:    // 32位大端序
            // 32位头部后直接跟加载命令(头部大小为mach_header)
            return (uintptr_t)(header + 1);
        case MH_MAGIC_64: // 64位小端序
        case MH_CIGAM_64: // 64位大端序
            // 64位头部后直接跟加载命令(头部大小为mach_header_64)
            return (uintptr_t)(((struct mach_header_64*)header) + 1);
        default:
            // 未知魔数,头部损坏
            return 0;  // Header is corrupt
    }
}

// 查找包含指定地址的镜像索引
// 遍历所有已加载的动态库,找到包含该地址的那一个
uint32_t bs_imageIndexContainingAddress(const uintptr_t address) {
    // 获取当前进程加载的镜像(动态库/可执行文件)数量
    const uint32_t imageCount = _dyld_image_count();
    // Mach-O头部指针
    const struct mach_header* header = 0;
    
    // 遍历所有镜像
    for(uint32_t iImg = 0; iImg < imageCount; iImg++) {
        // 获取镜像的头部
        header = _dyld_get_image_header(iImg);
        // 如果头部有效
        if(header != NULL) {
            // 查找包含该地址的段命令
            // 计算去除ASLR偏移后的地址
            uintptr_t addressWSlide = address - (uintptr_t)_dyld_get_image_vmaddr_slide(iImg);
            // 获取第一个加载命令的地址
            uintptr_t cmdPtr = bs_firstCmdAfterHeader(header);
            // 如果命令指针无效,跳过这个镜像
            if(cmdPtr == 0) {
                continue;
            }
            // 遍历该镜像的所有加载命令
            for(uint32_t iCmd = 0; iCmd < header->ncmds; iCmd++) {
                // 当前加载命令
                const struct load_command* loadCmd = (struct load_command*)cmdPtr;
                // 如果是32位段命令
                if(loadCmd->cmd == LC_SEGMENT) {
                    // 转换为32位段命令结构体
                    const struct segment_command* segCmd = (struct segment_command*)cmdPtr;
                    // 检查地址是否在该段的虚拟地址范围内
                    if(addressWSlide >= segCmd->vmaddr &&
                       addressWSlide < segCmd->vmaddr + segCmd->vmsize) {
                        // 找到了,返回镜像索引
                        return iImg;
                    }
                }
                // 如果是64位段命令
                else if(loadCmd->cmd == LC_SEGMENT_64) {
                    // 转换为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表示失败
    return UINT_MAX;
}

// 获取指定镜像索引的段基地址
// 通过查找__LINKEDIT段来计算文件在内存中的映射基地址
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;
    }
    // 遍历所有加载命令
    for(uint32_t i = 0;i < header->ncmds; i++) {
        // 当前加载命令
        const struct load_command* loadCmd = (struct load_command*)cmdPtr;
        // 如果是32位段命令
        if(loadCmd->cmd == LC_SEGMENT) {
            // 转换为32位段命令结构体
            const struct segment_command* segmentCmd = (struct segment_command*)cmdPtr;
            // 查找__LINKEDIT段(包含符号表、字符串表等链接信息)
            if(strcmp(segmentCmd->segname, SEG_LINKEDIT) == 0) {
                // 返回段基地址:虚拟内存地址 - 文件偏移
                // 这个值用于将文件偏移转换为虚拟内存地址
                return segmentCmd->vmaddr - segmentCmd->fileoff;
            }
        }
        // 如果是64位段命令
        else if(loadCmd->cmd == LC_SEGMENT_64) {
            // 转换为64位段命令结构体
            const struct segment_command_64* segmentCmd = (struct segment_command_64*)cmdPtr;
            // 查找__LINKEDIT段
            if(strcmp(segmentCmd->segname, SEG_LINKEDIT) == 0) {
                // 返回段基地址:虚拟内存地址 - 文件偏移
                return (uintptr_t)(segmentCmd->vmaddr - segmentCmd->fileoff);
            }
        }
        // 移动到下一个加载命令
        cmdPtr += loadCmd->cmdsize;
    }
    // 没有找到__LINKEDIT段,返回0
    return 0;
}

@end

Coding Agent 时代,App 的核心竞争力是什么?

2026年1月29日 08:00

以 Claude Code 为代表的 Coding Agent 对软件行业的重塑已成定局。它们的可用性已然突破临界点,使得代码生成的边际成本显著下降,比如 Claude Code 本身已经已经全部由 Claude Code 编写了。过去需要一周的硬编码工作,现在可能缩短为半天;过去因技术门槛高而不敢涉猎的领域,现在变得触手可及。

效率的提升带来的是竞争规则的改变,当「实现能力」不再是短板,App 的核心竞争力将发生怎样的迁移?

Agent 的强大,本质上意味着功能性复制的成本显著降低。如果你的护城河仅仅是「写了一个别人写不出的功能」,除非这个功能有极高的技术门槛,否则,其他竞争对手可以用 Agent 在短时间内复刻出一个八九不离十的产品,以更低的价格,甚至免费,来吸引用户。

这正是经典的「智猪博弈」升级版:以前是大猪(创新者)踩踏板,一两只小猪(跟随者)在食槽边等;现在是一二十只全副武装的小猪在那等着。你费尽心思设计的复杂功能,可能通过几轮 Prompt 就被对方解构并重现。

在这个局面下,需要重点关注的,是那些 AI 无法生成、无法复制且具有时间复利 的东西。

1. 从功能实现转向用户洞察

代码是可以被复制的显性知识,但关于「为什么要这样做」的隐性知识是 AI 难以窃取的。

产品的初衷是为了解决特定问题。你需要比同行更深刻地理解你的用户群:他们的使用场景、痛点、情绪触发点以及那些「非理性的诉求」。AI 可以完美执行 How,但无法推导出 Why。

这种基于深刻洞察和独特审美提出的解决方案,是单纯的 UI 克隆无法比拟的。

2. 数据资产与迁移壁垒

用户使用你的产品越久,沉淀的历史记录、个性化偏好、内容积累就越多,迁移成本也就越高。即使竞争对手 1:1 复制了你的功能,他们也无法复制用户在你这里留下的数据上下文。

因此,产品的设计逻辑应从「提供工具」转向「沉淀资产」。让产品越用越懂用户,这种基于数据的个性化体验,是冷冰冰的 AI 克隆版无法比拟的。

3. 开发者角色的演进

代码层面,随着代码量的增加,保障代码的可维护性、可演进性和产品质量变得更加重要,这一方面需要加深对 Coding Agent 的理解,提升熟练度,另一方面也需要深厚的软件开发功底,还要非常熟悉业务。

4. 情感连接与分发网络

对于产品,不仅要解决问题,还要带来愉悦感。这种细微的交互体验、情感共鸣,是建立品牌忠诚度的关键,也是用户愿意自发传播的动力。

分发能力也很重要,在行业中积累的信誉、与 KOL 建立的友好关系、在 Apple/Google 生态中建立的信任权重,这些都是 AI 无法通过算法生成的「社会资本」。


Claude Code 并没有让 App 开发这件事变得没有价值,它只是消灭了平庸的重复造轮子,将竞争的维度拉向了两端:一端是更底层的系统架构与质量保障,另一端是更上层的用户洞察与品牌情感。夹在中间单纯靠「写代码」生存的空间,会被挤压地越来越小,甚至消失。

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" 的屏幕,深知这只是开始。下一章,我们将利用这股力量,去触碰更深层的禁忌……

(未完待续...)

理财学习笔记(2):不懂不投

作者 唐巧
2026年1月29日 09:51

这是本系列的第 2 篇,主题是:不懂不投。

我们刚开始投资理财的时候,通常会寻求以下这些方法来找到投资标的。

常见的错误办法

1、问朋友。我们通常会问那些看起来投资理财收益比较高的朋友,问他们应该买什么股票。
对于朋友推荐的股票,我们通常会“无脑”买入。但如果有一天,股票突然大幅回撤,我们通常就会陷入恐慌。我们会怀疑:这个朋友到底靠不靠谱?他之前赚钱是靠运气,还是因为现在判断出了问题?接着,我们就会陷入各种猜忌、焦虑和紧张中,最后甚至睡不着觉。如果股票持续下跌,我们甚至可能割肉离场。所以说,跟着朋友买其实并不那么靠谱。

2、看走势。我们可能会去看某些股票或基金的历史走势。看到它在过去三年或五年涨得很好,我们就买入。这也是理财 App 或者某些理财经理推荐的首选理由:它过去 X 年涨幅 XX,排名 XX。

但这很容易陷入“价值陷阱”,比如:

  1. 周期性误判:有些股票仅仅是在某个周期内表现优秀。比如房地产在过去十年涨得很好,但这并非因为单体公司有多好,而是因为当时整个大环境让所有房企都很赚钱。如果你仅仅因为过去业绩好而买入,一旦遭遇经济下滑或泡沫破裂,就会面临巨大的损失。

  2. 均值回归陷阱:很多股票或基金某年表现出色,仅仅是因为那一年的风格与它匹配。所有行业都有“大小年”之分,未来遇到“小年”时,表现自然就会变差。我把这叫做“均值回归”。

这就好比考试:你的平均水平可能是第三名。发挥好的时候能考第一名,发挥不好则可能掉到第五名,但你始终是在第三名上下徘徊。

很多基金经理或股票的表现也是在自身价值上下震荡。如果你在高点买入,在回撤时就会损失惨重,甚至被深套。

3、跟风。跟风是 A 股散户的常见操作,某个时间什么热,就跟风买什么,涨了就快速卖掉,主打一个击鼓传花,赌谁是最后接盘的大傻子。

这种情况下,我们假设你的胜率是 50%。每次获胜挣 20%,每次赌失败亏 20%。如果你进行了 10 次这样的操作,那你整体的收益期望就是 (1.2^5)*(0.8^5)=0.82,所以你折腾了半天,最后 1 块钱的本金变成了 0.82 元。

当然,如果有人认为自己跟风总是赢,这也是有可能的,但是因为自己不敢长期持有,只要涨一点点就卖,其实每次挣的是一点点收益。但是如果偶尔遇到亏损的时候,自己舍不得卖掉,就会一次亏很多。做这种短线操作的人,需要极强的止损纪律,大部分人也是很难做到的。

不懂不投

所以回到股票投资,我觉得投资理财一定要自己懂才行。如果你完全不懂或一知半解,这些都会成为你的陷阱。因为:

  1. 心理层面:不懂的人往往“拿不住”。当股票大幅下跌时,无论是否割肉,你都会极度焦虑、睡不好觉,担心本金损失。
  2. 投资层面:如果你懂,面对下跌说不定还能逆势加仓;即便不加仓,至少能睡个好觉。

此外,世界上还有很多投资陷阱。有些人甚至专门为“制造陷阱”而生,比如搞资金盘、割韭菜或传销。这些行为有些是非法的,有些则游走在法律边缘。如果大家没有能力分辨这些陷阱,很容易就在投资理财中遭遇严重的亏损。

小结

小结一下,常见的错误投资:

  • 问朋友。其实本质上信的是朋友的业绩,朋友如果业绩下滑,就会怀疑。
  • 看走势。其实本质上是用过去业绩替代未来判断,不靠谱。
  • 跟风。纯投机,50% 胜率下期望是负的。

心理层面,只有懂了,才可能拿得住,睡得着觉。

另外,真正懂也可以避免很多骗局。

以上。

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 操作
  • 使用低质量的视觉效果
  • 暂停后台任务

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

昨天 — 2026年1月28日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

我的笔记系统

2026年1月28日 08:00

笔记大概分为三类:个人相关、工作相关和知识相关。个人向的主体是「我」,通常只对自己有意义;工作向的笔记自然与工作相关;知识向的笔记则致力于形成知识网络,时效性较长,也是本文讨论的重点。

相信大家都有用过大语言模型(LLM),如 ChatGPT,DeepSeek,豆包,Gemini 等等,给一个问题,就能得到不错的答案,那么在大语言模型不断进化,AI 工具愈发强大的当下,是否还有记笔记的必要?我认为:不仅有必要,而且比以往更重要。 但前提是,我们需要重新定义「记笔记」这件事。

笔记是什么?我把它看作 「外化的思考脚手架」。我们的大脑工作内存有限,只能同时处理 3-5 个想法,笔记可以将大脑从「记忆」的负担中解放出来,全力投入到「运算」中。笔记不是最终的目的,而是用于构建更高层建筑的工具,比如写文章,做决策,解决问题,它的价值在于它能支撑你爬得更高。

更形象的比喻或许是:预处理过的「半成品料理包」。当你来到厨房(需要解决问题/写作/决策)时,不需要从洗菜、切菜开始,而是直接拿出切好的配菜、调好的酱汁,就能快速烹饪出一道大餐。

在 AI 时代,有什么不懂直接问 AI 就好了,为什么还要记笔记?因为缺少内化的知识网络,就问不出好问题,没有好问题,就很难得到好答案,就无法最大程度地挖掘 AI 的潜力。大语言模型遵循的是 GIGO(Garbage In Garbage Out)原则,没有好的输入,就很难得到好的输出。笔记系统可以帮助我们构建/强化知识网络,从而问出好问题。

比如前一阵很火的 Dan Koe 的 How to fix your entire life in 1 day 这篇文章,看完之后,可能觉得很有道理,但不一定能问出合适的 follow up,比如文章提到的思想跟斯多葛的消极想象有什么联系?或文章提到的身份认同理论是否与 Atomic Habits 中提到的身份认同概念一致?以这些问题为切入点,可能又能获得到一些不错的新的知识点和看世界的角度,进而丰富自己的知识体系。

工作流概览

一个好的笔记系统不仅仅是工具的堆砌,更是信息的流动。我的工作流包含五个阶段:

  1. 捕获:极低阻力地快速收集。
  2. 存储:将待处理内容归位到合适的介质。
  3. 处理:提炼、消化原始内容。
  4. 回顾:建立连接,内化知识。
  5. 产出:用笔记解决实际问题,形成闭环。

一、捕获阶段 (Capture)

核心原则:极低阻力。灵感和信息稍纵即逝。这个阶段唯一的任务就是把脑子里的想法或外界的信息扔进一个固定的盒子里。此刻不要整理,也不要分类,只要丢进去即可。

我推荐 Apple Notes 的 Quick Note,系统级集成,很方便。Mac 上一键唤出,iPhone Control Center 随时点击。支持富媒体(语音、手绘、链接),就像一张随手可得的便利贴。

我的信息主要来自 Twitter(X)、YouTube、Newsletter、博客以及与 Gemini 的对话。为了解决「想看视频但没时间/效率低」的问题,我还构建了一套自动化流程:用 js 脚本调用 YouTube API 抓取字幕,通过 LLM 进行精简并整理成文章,最后打包成 Epub 电子书。这让我能像阅读文章一样「阅读」视频,大大提升了效率。

Gallery image 1

这里要避免沉迷于「寻找好内容」这种多巴胺陷阱,建议设定特定的「进货时间」(如周末早晨),批量获取信息,然后断连。同时不要试图在捕获阶段去消化内容,那样会打断「狩猎」节奏。

二、存储阶段 (Storage)

捕获的内容通常是链接、书籍或长文。这个阶段的目标是让它们「各归其位」,等待处理。

「链接」我推荐 Goodlinks。它没有订阅制,设计优雅,功能纯粹。我把它当作我的链接「中转站」。

「电子书」我没有使用 Apple Books 或 Calibre,而是直接使用 macOS Finder + Tags。把待看的书扔进文件夹,看完的书打上特定的标签,这样只要 filter by tag,就能看到看过的书和没看的书。这么做的一个原因是不争气的 Apple Books,它不支持 Smart Filter,只能手动创建 Collection,这样就很不方便筛选出没有看的书,我希望它像一个 Queue 或 Stack,随着书一本一本被看完,这个列表里的内容也会逐渐减少。还有一个原因是,书放进去后,再导出来也不太方便。

三、处理阶段 (Processing)

这是整个工作流中最重要,也是最容易被忽视的部分,很多人的笔记系统往往停在了上一步。这一步的目的是蒸馏(Distillation),提炼出有价值的内容,而不是简单地复制粘贴。

这个阶段最重要的,也是最难的部分,是要为它留出时间(比如每天晚上),因为做这件事可能没有那么愉悦,如果不专门留时间,几乎肯定会被其他阻力更小的事情代替。

这个阶段我用到的工具是 Dia 浏览器,没有直接在仓库中处理是不想看着一大堆未处理的内容产生焦虑,选择 Dia 浏览器是因为它的 Vertical Sidebar 和 Split view 很方便,同时因为它是浏览器,对链接天然友好,还能方便地唤出 Gemini。

浏览器可以打开 pdf,但默认不支持 epub,所以我又做了一个浏览器的 epub 插件,可以一边看书,一边与 Gemini 就书的内容进行交流。

Gallery image 1

待处理的内容通常比较长,或者是非母语的内容,为了提高效率,我会先让 Gemini 对内容进行压缩,如果感兴趣,再去看原文,然后与 Gemini 就里面的内容进行深度的交流。这是一个例子。交流完后,通常会有这些产出:

  • 一篇原文的精简版(放到笔记 App 里)
  • 一篇讨论后的笔记(放到笔记 App 里)
  • 一些原文的精彩摘录(放到笔记 App 里)
  • 方便录入到 Anki 的卡片(整理成实体卡片)
  • 相关推荐

Anki 相关的 App 一直用不起来,还是更喜欢实体的卡片,所以会把相关的知识点写到卡片上,顺便加深下印象。

Gallery image 1

处理后的笔记,我选择存放在 Bear 中。

  • 为什么不选 Obsidian? 它确实功能强大且免费,也有丰富的插件系统,但我用起来总觉得不够「舒服」。
  • 为什么不选 Apple Notes?它对 Markdown 的支持不友好,内容也有点封闭,写作体验也不如 Bear。

选择 Bear 还有一个好处,它的笔记可以很方便地导出为 Markdown,方便二次加工和后续迁移。孤立的笔记是死的。让笔记活过来的关键是Link(链接)。因为 Bear 的笔记都存在本地的一个 SQLite 数据库里,所以可以很方便地读取和处理。我写了一个 js 脚本,将 Bear 里的笔记内容向量化(Vectorization),然后计算余弦相似度,自动生成「相关笔记」列表。

四、回顾阶段 (Review)

把笔记存进去如果不看,那意义也不大。为了方便回顾,我做了一个 Web App(notes.limboy.me),每次随机展示一篇笔记作为起点,然后通过「相关笔记」进行漫游。同时也会在碎片时间把上一个阶段生成的卡片拿出来翻一翻,加深印象。

Gallery image 1

五、产出阶段 (Output)

笔记不是目的,它是为了帮助生成洞见(Insight)、新的看事物的角度和强化知识网络而存在,最好的方式就是输出,比如写文章、做分享、做决策等。以写文章为例,如果想写一篇关于「习惯养成」的文章,不再是面对空白文档抓耳挠腮,只需在笔记库里搜索「习惯」、「行为心理学」,把相关的 5-6 个笔记块(料理包)调出来,重新排列组合,加上新的连接词,文章的 80% 就完成了。

结语

如果没有一套运行顺畅的笔记系统,没有为消化笔记专门留出时间,没有输出的压力,那么笔记的价值就会大打折扣,再好的工具也无法做到第二大脑。希望这篇文章能给你带来些帮助和启发,如果你有好的想法和经验,也欢迎分享。

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 的全局并发队列执行;它们只定义任务的组织与同步语义,而线程管理与调度完全由全局队列和系统线程池负责。

❌
❌