阅读视图

发现新文章,点击刷新页面。

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

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 映射的吗?

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 时最容易踩的坑有哪些?

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 的作用和使用场景是什么?

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】自定义队列在底层是如何映射到全局队列的?

自定义队列本身不拥有线程;它们通过 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)有多少个?是如何区分的?

✅ 结论版(先背这个)

系统一共有 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 的底层数据结构是什么?

它是一个复杂的 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 是如何管理线程池的?线程是谁创建的?

线程创建者: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。

1-1.【GCD】DispatchQueue 本质上是什么?它和线程是什么关系?

DispatchQueue 是 GCD(Grand Central Dispatch)的核心抽象,它本质上是一个 任务调度管理器,而不是线程本身。

本质特性

1. 任务队列

  • 存储要执行的任务(闭包或函数)
  • 遵循 FIFO(先进先出)原则
  • 可以是串行(serial)或并发(concurrent)
// 串行队列 - 一次执行一个任务
let serialQueue = DispatchQueue(label: "com.example.serial")

// 并发队列 - 可同时执行多个任务
let concurrentQueue = DispatchQueue(label: "com.example.concurrent", attributes: .concurrent)

2. 线程池管理器

  • 管理底层线程池(thread pool)
  • 自动复用线程,避免频繁创建销毁
  • 根据系统负载动态调整线程数

与线程的关系

关键区别

DispatchQueue 线程 (Thread)
高级抽象,任务调度器 低级执行单元
管理任务执行顺序 CPU调度的基本单位
自动线程管理 需要手动管理生命周期
系统优化负载均衡 固定资源占用

实际关系

// 示例:队列与线程的关系
let queue = DispatchQueue(label: "test", attributes: .concurrent)

for i in 1...5 {
    queue.async {
        print("任务 (i) 在线程: (Thread.current)")
    }
}

// 可能输出(线程会被复用):
// 任务1在线程: <NSThread: 0x...>{number = 4, name = (null)}
// 任务2在线程: <NSThread: 0x...>{number = 5, name = (null)}
// 任务3在线程: <NSThread: 0x...>{number = 4, name = (null)} // 线程复用!

重要特性

1. 线程复用

// GCD 自动复用线程,提高性能
// 不会为每个任务都创建新线程

2. 队列层级

// 主队列 - 主线程,UI更新
DispatchQueue.main.async {
    // 更新UI
}

// 全局队列 - 系统提供的并发队列
let globalQueue = DispatchQueue.global(qos: .background)

// 自定义队列
let customQueue = DispatchQueue(label: "custom")

3. 避免线程爆炸

// ❌ 错误:直接创建大量线程
for _ in 0...1000 {
    Thread {
        // 工作
    }.start()
}

// ✅ 正确:使用队列,系统自动管理
let queue = DispatchQueue.global()
for _ in 0...1000 {
    queue.async {
        // 工作 - 系统会合理分配线程
    }
}

最佳实践

1. 选择合适队列

// UI更新用主队列
DispatchQueue.main.async {
    // 更新UI
}

// 后台任务用全局队列
DispatchQueue.global(qos: .userInitiated).async {
    // 耗时任务
    DispatchQueue.main.async {
        // 完成后更新UI
    }
}

2. 避免阻塞主队列

// ❌ 错误:在主队列执行耗时操作
DispatchQueue.main.async {
    let data = processLargeData() // 阻塞UI
    updateUI(data)
}

// ✅ 正确:在后台处理
DispatchQueue.global(qos: .userInitiated).async {
    let data = processLargeData()
    DispatchQueue.main.async {
        updateUI(data)
    }
}

总结

DispatchQueue 是线程的智能调度器

  • 它 管理 任务,让开发者专注于"做什么"
  • 它 隐藏 线程细节,自动优化资源使用
  • 它 保证 任务按照指定顺序和方式执行

GCD 通过 DispatchQueue 将开发者从繁琐的线程管理中解放出来,让并发编程变得更简单、更高效、更安全。

👉 追问:为什么说 GCD 是“基于队列而不是线程”的并发模型?

说 GCD 是"基于队列而不是线程"的并发模型,核心在于它将并发抽象的重心从"谁执行"(线程)转移到了"什么被执行"(任务)以及"如何组织执行"(队列)。

核心思想对比

传统线程模型

// 关注的是"执行者"
let thread1 = Thread { /* 任务A */ }
let thread2 = Thread { /* 任务B */ }
thread1.start()
thread2.start()
// 你需要管理:创建线程、分配任务、协调线程、销毁线程

GCD 队列模型

// 关注的是"任务组织"
let queue = DispatchQueue(label: "work", attributes: .concurrent)
queue.async { /* 任务A */ }  // 我只管提交任务
queue.async { /* 任务B */ }  // 系统决定如何执行
// 你只需要:定义任务、选择队列、提交任务

为什么这个区别很重要?

1. 解耦任务与执行资源

// 传统线程:任务和线程强绑定
Thread {
    downloadImage()  // 任务绑定到这个特定线程
}

// GCD:任务和线程解耦
queue.async {
    downloadImage()  // 任务提交到队列,系统分配线程
}
// 同一个任务在不同时间可能由不同线程执行

2. 从"微观管理"到"宏观调度"

传统线程编程 GCD 队列编程
思考:需要多少线程? 思考:任务如何组织?
担心:线程创建/销毁开销 专注:任务依赖和顺序
操心:线程同步和通信 利用:队列的同步特性

3. 编程模型更直观

// 用队列表达执行顺序非常自然

// 串行执行:天然保证顺序
serialQueue.async { task1() }
serialQueue.async { task2() }  // 一定在 task1 之后

// 并发执行:简单明了
concurrentQueue.async { task1() }
concurrentQueue.async { task2() }  // 可能并行执行

// 依赖关系:清晰表达
queue.async {
    let data = fetchData()
    DispatchQueue.main.async {
        updateUI(with: data)
    }
}

实际体现

示例:对比两种模型的复杂性

// 传统线程方式实现三个任务的串行执行
class ThreadManager {
    var currentThread: Thread?
    
    func executeSequentially() {
        let thread1 = Thread {
            task1()
            // 需要手动协调下一个任务
            let thread2 = Thread {
                task2()
                let thread3 = Thread {
                    task3()
                }
                thread3.start()
            }
            thread2.start()
        }
        thread1.start()
    }
}

// GCD 方式实现三个任务的串行执行
let serialQueue = DispatchQueue(label: "serial")
serialQueue.async { task1() }
serialQueue.async { task2() }
serialQueue.async { task3() }
// 简洁明了,自动保证顺序

示例:避免"线程爆炸"

// 传统方式:容易创建过多线程
for i in 0..<1000 {
    Thread {
        processItem(i)
    }.start()  // 可能创建1000个线程!
}

// GCD 方式:系统智能管理
let queue = DispatchQueue.global()
for i in 0..<1000 {
    queue.async {
        processItem(i)  // 系统复用线程池中的线程
    }
}
// 可能只用 8-64 个线程(根据 CPU 核心数优化)

GCD 的哲学转变

1. 关注点分离

// 你关心的:
- 任务是什么?
- 任务间的依赖关系?
- 任务的优先级?
- 执行顺序要求?

// 系统关心的:
- 用多少线程?
- 哪个线程执行哪个任务?
- 何时创建/销毁线程?
- 如何负载均衡?

2. 更高级的抽象

// GCD 提供了高级原语
let group = DispatchGroup()  // 任务组
let semaphore = DispatchSemaphore(value: 1)  // 信号量
let barrier = DispatchWorkItem(flags: .barrier)  // 屏障

// 这些都在队列层面操作,不直接涉及线程

3. 性能优化透明化

// 系统可以根据情况优化:
// - 线程复用
// - 负载均衡
// - CPU 亲和性
// - 能耗优化(iOS 上特别重要)

// 你只需:queue.async { work }
// 系统决定:用哪个核、哪个线程、何时执行

现实类比

传统线程模型:自己建工厂

  • 你需要:招聘工人(创建线程)
  • 分配任务:A 你去拧螺丝,B 你去焊接
  • 协调管理:A 做完告诉 B
  • 成本高:工人闲置时也要付工资

GCD 队列模型:使用云服务平台

  • 你只需:提交任务需求单(任务到队列)
  • 平台负责:分配工人、调度任务、优化资源
  • 你只关心:任务描述和依赖关系
  • 成本优:按需使用,资源复用

总结

GCD 的"基于队列"模型之所以强大,是因为:

  1. 抽象层次更高:从"执行单元"抽象到"任务组织"
  2. 资源管理更优:系统全局优化,避免局部最优
  3. 编程模型更安全:减少了数据竞争和死锁的机会
  4. 性能更可预测:避免了线程爆炸和过度订阅
  5. 代码更简洁:关注业务逻辑,而不是线程管理

这种设计使得并发编程从一门"黑魔法"变成了更可控、更安全的工程实践。你不再直接指挥"士兵"(线程),而是制定"作战计划"(队列和任务),让"指挥部"(GCD)去最优执行。

Skip 开源:从“卖工具”到“卖信任”的豪赌 -- 肘子的 Swift 周报 #120

issue120.webp

Skip 开源:从“卖工具”到“卖信任”的豪赌

在宣布 Fuse 版本对独立开发者免费 仅两个月后,Skip Tools 再次做出惊人之举——全面免费并开源核心引擎 skipstone。这意味着 Skip 彻底改变了经营方式:从“卖产品”转向“卖服务+社区赞助”。这次变化,既有对之前商业模式执行不佳而被迫调整的无奈,也体现了 Skip 团队的果敢——在当前 AI 盛行、开发工具格局固化的背景下,主动求变,力求突破。

很多优秀且有特点的产品没能获得应有的使用量,最大的阻碍往往不是技术,而是“信任”。正如 Skip 官方所言,企业最担心的是“rug pull”——万一公司倒闭或被收购,押注在这个工具上的产品和技术栈将面临推倒重来的风险。Skip 希望通过开源消除这种信任危机,让自己和其他跨平台工具站在同一起跑线上,同时激活社区力量,加速生态建设。

尽管社区对 Skip 的开源决定给予了充分肯定,但也有不少人担心:在放弃了 license key 这一稳定收入来源后,仅靠赞助模式能否支撑持续开发成本?毕竟,成功的开源商业案例虽有,但失败的也不少。这种担忧并非杞人忧天。在我撰写本期周报时(宣布开源数天后),尽管开源消息在社交媒体上引发了热议,但在 GitHub 上,Skip 的个人赞助者仅有十几位。从“热闹的讨论”到“真金白银的支持”,中间的鸿沟似乎比想象中还要深。这或许也解释了为什么 Skip 最终选择了开源——在无法说服开发者"付费使用"时,至少要争取到他们"免费使用"的机会。

一个有趣的现象是,在肯定与忧虑并存的情绪中,讨论的焦点悄然发生了变化:从“为什么要花钱买 Skip 而不是用免费的 KMP?”变成了“你是更喜欢写 Swift 还是 Kotlin?”。这个转变意义重大——它意味着 Skip 已经成功将竞争维度从“商业模式”转移到了“语言生态”,而参与竞争的主体也从“一家小公司”扩展为“整个 Swift 社区”。在这场语言之争中,Skip 聪明地(或许是无意间)将自己的角色从“主角”变成了“基础设施”。

Skip 最早的出发点是看到了一个商业机会,认为有足够的商业回报。如果不是有这样的预期,仅靠官方或社区的力量,Swift 可能无法快速推进在其他平台上的进展。这一路径与 The Browser Company 为了打造 Arc 浏览器而大力推动 Swift 在 Windows 平台上的适配如出一辙。

作为一个 Swift 开发者,我由衷希望 Skip 的这次调整能够取得预期效果。开源是一场信任的实验,也是一次生态的投资。如果你也期待 Swift 能在 iOS 之外的平台上拥有更多可能,不妨从成为一个独立赞助者($10/月)做起——这不仅是对 Skip 的支持,更是对整个 Swift 跨平台生态的投票。开源的 Skip 能走多远,取决于有多少人愿意从旁观者变成参与者。

本期内容 | 前一期内容 | 全部周报列表

🚀 《肘子的 Swift 周报》

每周为你精选最值得关注的 Swift、SwiftUI 技术动态

原创

isolated(any) 与 #isolation:让 Swift 闭包自动继承隔离域

Swift 6 为并发引入了许多新功能与关键字。虽然其中不少内容在日常开发中可能鲜少用到,但一旦遭遇特定场景,若对这些新概念缺乏了解,即便有 AI 辅助也可能陷入僵局。本文将通过一个在开发测试中遇到的实际并发问题,来介绍如何利用 @isolated(any) 以及 #isolation 宏,实现函数的隔离域继承,从而让编译器自动推断闭包的运行环境。

近期推荐

SwiftData 数据迁移全解析 (A Deep Dive into SwiftData migrations)

随着应用的发展,数据模型几乎无可避免地会出现变化,如何安全地进行数据迁移对很多开发者来说都是一个不小的挑战。在本文中,Donny Wals 全面讲解了 SwiftData 的数据迁移机制,从基础的版本控制到复杂的自定义迁移策略。其中,Donny 给出了几个特别有价值的建议:即使是 V1 版本也应该使用 VersionedSchema 进行封装,为未来的迁移打好基础;只在 App Store 发布周期之间引入新的 Schema 版本,而不是在开发过程中频繁修改;对于轻量迁移,即使 SwiftData 可以自动完成,也建议显式地写入 SchemaMigrationPlan 中,使意图更加明确且易于测试。

文章的一大亮点是详细解释了 MigrationStage.customwillMigratedidMigrate 的适用场景,并提出了应对复杂重构(例如拆分实体)的“桥接版本(Bridge Version)”策略。


为什么 MVVM-C 在 SwiftUI 项目中依然能扩展 (Why MVVM-C with Coordinators does Scale -> A real-world SwiftUI perspective)

尽管标题中包含“MVVM-C”,但这并非一篇单纯为某种架构模式辩护的文章。Bruno Valente Pimentel 在文中重新定义了“扩展性”的内涵——它不应仅以屏幕或文件的数量来衡量,而应看其是否支持高效的多人协作及功能的独立演进。正如作者所言:“架构无关乎模式,而关乎减少恐惧”——减少修改代码、团队协作以及功能演进时的恐惧。

无论你倾向于哪种架构模式,在用它构建 SwiftUI 项目时,都应该认真思考:我们到底是在利用 SwiftUI 的特性,还是在试图把 SwiftUI 改造成我们熟悉的 UIKit?


TCA 架构的真实评估 (TCA (Composable Architecture): The Honest Review)

继 Bruno 为 MVVM-C “正名”后,Chandra Welim 对另一个极具争议的架构——TCA 进行了客观评估。作者总结了 TCA 的五大优势(可预测的状态管理、优秀的可测试性、时间旅行调试、组合性、强类型安全)和五大挑战(学习曲线、样板代码、团队采纳、对简单应用过度设计、第三方依赖),同时给出了务实的建议:TCA 是一个强大的工具,但绝非适用于所有场景。对于简单的 MVP 或缺乏函数式编程背景的团队,传统的 MVVM 或许是更务实的选择;而对于状态错综复杂、需要“时间旅行”调试的大型项目,TCA 则能提供无与伦比的掌控力。核心结论:TCA 在你需要时很值得,但大多数应用并不需要它。

架构选择没有银弹,只有在特定上下文中的权衡。


让 C 库在 Swift 中“像原生 API 一样好用” (Improving the usability of C libraries in Swift)

Swift 虽然生来就兼容 C,但直接调用 C API 往往体验不佳——开发者通常需要面对裸指针、手动内存管理以及不符合 Swift 命名习惯的函数。为了获得“原生”体验,开发者不得不编写和维护繁琐的 Wrapper 层。在这篇文章中,Doug Gregor 详细介绍了如何利用 API Notes 和 Clang Attributes 机制,在不修改 C 库实现代码的前提下,“指导” Swift 编译器生成符合 Swift 风格的接口。

这项改进的意义在于,它将“封装 C 库”的成本从逻辑层(编写 Swift 胶水代码)转移到了声明层(编写 API Notes 或添加头文件注解)。这对于 Embedded Swift 的普及至关重要,因为嵌入式开发高度依赖现有的 C 库生态。随着工具链的完善,未来开发者可能只需要给 C 库加几个注解,就能直接在 Swift 中像使用原生库一样调用它。文章还提供了基于正则表达式的自动化脚本,可以为结构化的 C 头文件批量生成 API Notes。


在 Yocto 系统中构建 Swift (Introduction to Building Swift for Yocto)

对于大多数 iOS 开发者来说,Yocto 可能比较陌生,但它是嵌入式 Linux 领域的事实标准——它能够精确控制系统中包含的每一个组件。随着 Embedded Swift 的推进,如何在 Yocto 生态中集成 Swift 成为了一个关键问题。Jesse L. Zamora 详细演示了如何使用近期重获更新的 meta-swift 在 Yocto 系统中构建 Swift 运行环境。文章以 Raspberry Pi Zero 2 为例,从 Docker 环境搭建、Yocto 构建、镜像烧录到实际运行,演示了完整的工作流。

需要注意的是,完整构建过程需要数小时,且要求对 Yocto 构建系统有基本了解。不过,文章提供的 meta-swift-examples 仓库已经包含了 Docker 化的构建环境和一键脚本,大大降低了上手门槛。

meta-swift 是 Swift 语言的 Yocto 层,此前曾长期停滞于 Swift 5.7,但在社区的努力下,目前已支持到 Swift 6.1.3。


让 Xcode 的 DEBUG 构建自动部署到 /Applications (TIL how to auto-move Xcode DEBUG builds to Applications)

开发包含 App Intents 的应用时,你可能遇到过这个问题:App Intents 必须在 /Applications 目录下才能在 Shortcuts.app 中显示。这意味着 DEBUG 构建默认放在 DerivedData 中时,开发者无法测试 Shortcuts 集成。 Carlo Zottmann 曾长期使用复杂的 Post-build 脚本来搬运 App,但他在本文中分享了一个更“原生”的方案——通过三行 .xcconfig 配置让 Xcode 自动将构建产物部署到指定目录,并在清理构建时正确删除已部署的文件。

这个技巧不仅适用于 App Intents 测试,也适用于任何需要将 DEBUG 构建放到特定位置的场景。


用 Swift Charts 构建自定义可交互仪表盘 (Visualise anything with SwiftUI Charts)

如果你需要自定义一个可动画、可交互的环形压力仪表盘(Stress Indicator)的 SwiftUI 组件,你会如何进行?是使用 Shape,还是 Canvas?Kyryl Horbushko 在本文中展示了一个非常有创意的解法——利用 Swift Charts 的 SectorMark,优雅且高效地实现了需求。

Kyryl 的实现打破了我们对 Swift Charts 仅用于“数据可视化”的刻板印象。对于需要开发健康类、金融类复杂 Dashboard 的开发者来说,这是一种极具参考价值的"降维打击"式方案——用数据可视化框架解决自定义 UI 问题,既减少代码量,又获得内置的动画和交互支持。

工具

Commander:兼顾专注与灵活的 AI 编程体验

我喜欢在 macOS 上使用 CLI 工具(Claude Code、Codex),但原生终端应用的视觉体验实在不敢恭维,因此多数情况下我都在 VSCode 中通过插件来改善体验。然而,VSCode 的一个设计始终让我困扰:即便将 Claude Code 的对话栏最大化,AI 修改文件时仍会自动打开新的文件栏或窗口,打断既有的交互节奏。

Marcin Krzyżanowski 开发的 macOS 原生应用 Commander 给了我一个新的选择。它提供了一个比终端更优的交互界面,同时保护了我的“心流”——文件只在需要时才开启,并能调用系统默认应用查看。Commander 目前支持 Claude Code 和 Codex,内置了 Git worktrees 和 Code diff 支持,且完全免费。它在终端的“专注”与 VSCode 的“灵活”之间,构建了一个绝佳的中间地带。


Oh My Agents: 统一管理所有 AI 编程助手的配置

随着 Claude Code、Cursor、Codex、Windsurf 等 AI 编程助手的涌现,开发者面临着一个新问题:如何管理散落在各个项目中的 prompts、skills 和 rules? 每个 Agent 都有自己的配置规范(CLAUDE.mdCURSOR.md...),手动复制粘贴不仅低效,还容易导致版本混乱。王巍(onevcat) 开发的 Oh My Agents 提供了解决方案。

核心功能:

  • 集中管理所有 AI Agent 配置
  • 定义分发规则,一键同步到多个项目
  • 双向同步,项目改进可拉回中央库并更新所有关联项目
  • 预览变更,避免意外覆盖

注:目前仅支持 macOS (Windows/Linux 开发中),Beta 期间免费。

往期内容

💝 支持与反馈

如果本期周报对你有帮助,请:

  • 👍 点赞 - 让更多开发者看到
  • 💬 评论 - 分享你的看法或问题
  • 🔄 转发 - 帮助同行共同成长

🚀 拓展 Swift 视野

Swift 常用框架Kingfisher、KingfisherWebP详解

1.1 什么是 Kingfisher 、KingfisherWebP

Kingfisher 是一个功能强大的 Swift 库,专门用于处理图像的下载、缓存和展示。目前已成为 iOS/macOS 开发中最受欢迎的图像处理解决方案之一。

KingfisherWebP 是 Kingfisher 的官方扩展,用于支持 WebP 图像格式。WebP 是 Google 开发的一种现代图像格式,它可以在相同质量下提供比 JPEG 和 PNG 更小的文件大小,从而减少带宽使用和加快加载速度。

1.2 核心特性

  • 异步下载 :在后台线程下载图片,不阻塞主线程
  • 多级缓存 :内存缓存 + 磁盘缓存,提高加载速度
  • 自动缓存管理 :智能处理缓存大小和过期时间
  • 图片处理 :支持下载后处理,如圆角、模糊等
  • 请求优先级 :可设置不同图片请求的优先级
  • 可扩展性 :支持自定义缓存、下载器等组件
  • SwiftUI 支持 :提供了便捷的 SwiftUI 视图扩展
  • 动画支持 :支持 GIF 等动画图片

1.3 安装方式

Kingfisher 支持多种安装方式:

CocoaPods:

pod 'Kingfisher'
pod 'KingfisherWebP'

2. Kingfisher 基础用法

2.1 基本图片加载

import Kingfisher

// 基本用法
imageView.kf.setImage(with: URL(string: "https://example.com/image.jpg"))

// 带选项的用法
imageView.kf.setImage(
    with: URL(string: "https://example.com/image.jpg"),
    placeholder: UIImage(named: "placeholder"),
    options: [
        .transition(.fade(0.2)),
        .cacheOriginalImage
    ]
) { result in
    switch result {
    case .success(let value):
        print("Image loaded: \(value.image)")
        print("图片加载成功: \(value.source.url?.absoluteString ?? "")")
    case .failure(let error):
        print("图片加载失败: \(error.localizedDescription)")
    }
}

2.2 缓存控制

 // 清除内存缓存
KingfisherManager.shared.cache.clearMemoryCache()

// 清除磁盘缓存
KingfisherManager.shared.cache.clearDiskCache()

// 清除所有缓存
KingfisherManager.shared.cache.clearCache()

2.3 预加载图像

预加载一组图像以提升加载速度,适合在应用启动时或预期需要时使用。

 let urls = [URL(string: "https://example.com/image1.png")!, URL(string: "https://example.com/image2.png")!]
 ImagePrefetcher(urls: urls).start()

2.4 显示WebP

 /// 全局配置
KingfisherManager.shared.defaultOptions += [.processor(WebPProcessor.default),.cacheSerializer(WebPSerializer.default)]

// 使用 AnimatedImageView 定义ImageView

 let animageView = AnimatedImageView()
 animageView.kf.setImage(with: URL(string: "https://example.com/image.webp"))

深入理解 WKWebView:代理方法与 WKWebView 生命周期的执行顺序

在 iOS 开发中,WKWebView 是构建混合应用(Hybrid App)的核心组件。它基于现代 WebKit 引擎,性能优异、安全性高,但其复杂的生命周期机制也让不少开发者感到困惑——尤其是当页面加载失败时,错误回调到底在哪个阶段触发?

本文将深入解析 WKWebView 的完整生命周期,以 Objective-C 为开发语言,系统梳理 WKNavigationDelegate 中各代理方法的执行时机与调用顺序,并通过对比 正常加载成功加载失败 两种典型场景,帮助你精准掌控 WebView 行为,避免常见陷阱。

✅ 适用系统:iOS 9+(建议 iOS 11+)
💬 开发语言:Objective-C
🧭 核心协议:WKNavigationDelegate


一、生命周期全景图

WKWebView 的导航过程由一系列代理方法串联而成。根据加载结果不同,可分为两条主路径:

✅ 成功路径(页面正常加载)

didStartProvisionalNavigation
→ decidePolicyForNavigationAction
→ didCommitNavigation
→ decidePolicyForNavigationResponse
→ didFinishNavigation

❌ 失败路径(加载中断)

didStartProvisionalNavigation
→ decidePolicyForNavigationAction(可能)
→ didFailProvisionalNavigation     // 早期失败(如 DNS 错误)
   或
→ didCommitNavigation
→ didFailNavigation               // 提交后失败(如 SSL 证书无效)

⚠️ 重要认知:

  • HTTP 404/500 不会触发 fail 回调!因为服务器已返回有效响应,属于“成功加载错误页”。
  • 所有 decisionHandler 必须被调用,否则 WebView 将卡死。

二、成功加载:五步走流程详解

当访问一个有效 URL(如 https://example.com)且网络通畅时,代理方法按以下顺序严格触发:

1. webView:didStartProvisionalNavigation:

  • 页面开始尝试加载。
  • URL 可能尚未最终确定(例如重定向前)。
  • 适合启动 loading 动画
- (void)webView:(WKWebView *)webView 
didStartProvisionalNavigation:(WKNavigation *)navigation {
    NSLog(@"✅ Start provisional navigation to: %@", webView.URL);
    [self showLoadingIndicator];
}

2. webView:decidePolicyForNavigationAction:decisionHandler:

  • 决定是否允许此次跳转。
  • 常用于拦截自定义 scheme(如 tel://, weixin://)。
- (void)webView:(WKWebView *)webView 
decidePolicyForNavigationAction:(WKNavigationAction *)action 
decisionHandler:(void (^)(WKNavigationActionPolicy))handler {
    NSURL *url = action.request.URL;
    if ([[url scheme] isEqualToString:@"tel"]) {
        [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
        handler(WKNavigationActionPolicyCancel); // 拦截并交由系统处理
        return;
    }
    handler(WKNavigationActionPolicyAllow); // 允许加载
}

🔥 必须调用 handler()!否则页面将永远处于“加载中”。


3. webView:didCommitNavigation:

  • 浏览器已接收到响应头,开始接收 HTML 数据。
  • DOM 开始构建,但未渲染完成。
  • 此时 webView.URL 已是最终地址(可用于埋点或日志)。
- (void)webView:(WKWebView *)webView 
didCommitNavigation:(WKNavigation *)navigation {
    NSLog(@"✅ Committed to final URL: %@", webView.URL);
}

4. webView:decidePolicyForNavigationResponse:decisionHandler:

  • 针对服务器返回的响应(状态码、MIME 类型等)决定是否继续加载。
  • 可用于拦截非 HTML 资源(如 PDF、ZIP 文件)。
- (void)webView:(WKWebView *)webView 
decidePolicyForNavigationResponse:(WKNavigationResponse *)response 
decisionHandler:(void (^)(WKNavigationResponsePolicy))handler {
    NSHTTPURLResponse *httpResp = (NSHTTPURLResponse *)response.response;
    if ([httpResp.MIMEType isEqualToString:@"application/pdf"]) {
        // 拦截 PDF 下载
        handler(WKNavigationResponsePolicyCancel);
        return;
    }
    handler(WKNavigationResponsePolicyAllow);
}

5. webView:didFinishNavigation:

  • 所有资源(HTML、CSS、JS、图片等)加载完毕。
  • 页面完全可交互
  • 隐藏 loading、注入 JS、执行业务逻辑的最佳时机
- (void)webView:(WKWebView *)webView 
didFinishNavigation:(WKNavigation *)navigation {
    NSLog(@"✅ Page fully loaded!");
    [self hideLoadingIndicator];
    // 可在此注入 JS 或通知上层
}

三、失败加载:两类错误路径剖析

加载失败分为 Provisional 阶段失败Commit 后失败,需分别处理。

🔴 类型 1:Provisional 阶段失败

触发方法didFailProvisionalNavigation:withError:
典型原因

  • DNS 解析失败(域名不存在)
  • 无法连接服务器(断网、超时)
  • URL 格式非法
✅ didStartProvisionalNavigation
✅ decidePolicyForNavigationAction
❌ didFailProvisionalNavigation: "A server with the specified hostname could not be found."

🔴 类型 2:Commit 后失败

触发方法didFailNavigation:withError:
典型原因

  • SSL/TLS 证书无效或过期(iOS 默认拦截)
  • 服务器在传输中途断开连接
✅ didStartProvisionalNavigation
✅ decidePolicyForNavigationAction
✅ didCommitNavigation
❌ didFailNavigation: "The certificate for this server is invalid."

❗ 关键提醒:只监听 didFailProvisionalNavigation 会漏掉 SSL 错误!必须同时实现两个失败回调。


统一错误处理示例

- (void)webView:(WKWebView *)webView 
didFailProvisionalNavigation:(WKNavigation *)navigation 
withError:(NSError *)error {
    NSLog(@"❌ Provisional fail: %@", error.localizedDescription);
    [self showErrorViewWithError:error];
}

- (void)webView:(WKWebView *)webView 
didFailNavigation:(WKNavigation *)navigation 
withError:(NSError *)error {
    NSLog(@"❌ Navigation fail after commit: %@", error.localizedDescription);
    [self showErrorViewWithError:error];
}

四、初始化与代理设置示例

// ViewController.h
@interface ViewController () <WKNavigationDelegate>
@property (nonatomic, strong) WKWebView *webView;
@end

// ViewController.m
- (void)viewDidLoad {
    [super viewDidLoad];
    
    WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
    self.webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:config];
    self.webView.navigationDelegate = self;
    [self.view addSubview:self.webView];
    
    [self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com"]]];
}

💡 建议:若需共享 Cookie 或缓存,可复用 WKProcessPool


五、总结:关键要点速查表

场景 触发方法 是否必须处理
页面开始加载 didStartProvisionalNavigation
决策是否跳转 decidePolicyForNavigationAction ✅(必须调用 handler)
页面提交(DOM 开始构建) didCommitNavigation
决策是否接受响应 decidePolicyForNavigationResponse ✅(必须调用 handler)
加载完成 didFinishNavigation
早期失败(DNS/断网) didFailProvisionalNavigation
提交后失败(SSL/中断) didFailNavigation

六、最佳实践建议

  1. 双失败回调都要实现,覆盖所有异常场景。
  2. 所有 decisionHandler 必须调用,避免页面卡死。
  3. 避免循环引用:delegate 使用 weak self,或在 dealloc 中置 nil。
  4. 真机测试异常网络:使用「设置 > 开发者 > 网络链接条件」模拟弱网/断网。
  5. 不要依赖 HTTP 状态码判断失败:404/500 仍会触发 didFinishNavigation

📌 延伸思考

  • iOS 15+ 新增 WKNavigationDelegate 的 frame 级回调(如 didFinishDocumentLoadForFrame:
  • 若需深度控制缓存策略,可结合 WKWebsiteDataStore 使用

如果你觉得本文对你有帮助,欢迎 点赞 ❤️、收藏 ⭐、评论 💬!也欢迎关注我,获取更多 iOS 底层与实战技巧。

__CFRunLoopDoSources0函数详解

借助AI辅助。

__CFRunLoopDoSources0 函数逐行注释

函数概述

__CFRunLoopDoSources0 是 RunLoop 中负责处理 Source0 事件源的核心函数。Source0 是需要手动标记为待处理(signal)的事件源,常用于自定义事件处理、触摸事件、手势识别等场景。

Source0 与 Source1 的区别

Source0(非基于端口)

  • 触发方式: 需要手动调用 CFRunLoopSourceSignal() 标记为待处理,然后调用 CFRunLoopWakeUp() 唤醒 RunLoop
  • 特点: 不会自动唤醒 RunLoop,需要手动唤醒
  • 使用场景: 触摸事件、自定义事件、手势识别、UIEvent 处理
  • 实现: 基于回调函数

Source1(基于端口)

  • 触发方式: 基于 Mach Port,当端口收到消息时自动唤醒 RunLoop
  • 特点: 可以自动唤醒 RunLoop
  • 使用场景: 进程间通信、系统事件、CFMachPort、CFMessagePort
  • 实现: 基于 Mach 内核的端口通信

函数签名

/* rl is locked, rlm is locked on entrance and exit */
static Boolean __CFRunLoopDoSources0(CFRunLoopRef rl, CFRunLoopModeRef rlm, Boolean stopAfterHandle) __attribute__((noinline));

参数说明

  • CFRunLoopRef rl: 当前运行的 RunLoop
  • CFRunLoopModeRef rlm: 当前的 RunLoop Mode
  • Boolean stopAfterHandle: 是否在处理一个 source 后就停止(用于优化性能)

返回值

  • Boolean: 如果至少处理了一个 source 返回 true,否则返回 false

前置条件

  • 函数调用时必须持有锁: rlrlm 都必须处于加锁状态
  • 函数返回时保持锁状态: 出口时 rlrlm 仍然加锁

函数属性

  • __attribute__((noinline)): 防止编译器内联优化,便于调试和性能分析

完整代码逐行注释

/* rl is locked, rlm is locked on entrance and exit */
// 📝 锁状态约定:函数入口和出口时,rl 和 rlm 都必须处于加锁状态
static Boolean __CFRunLoopDoSources0(CFRunLoopRef rl, CFRunLoopModeRef rlm, Boolean stopAfterHandle) __attribute__((noinline));

static Boolean __CFRunLoopDoSources0(CFRunLoopRef rl, CFRunLoopModeRef rlm, Boolean stopAfterHandle) {/* DOES CALLOUT */
    // ⚠️ 重要标注:此函数会执行外部回调(callout),可能导致:
    // 1. 长时间阻塞(回调函数耗时)
    // 2. 重入问题(回调中可能再次操作 RunLoop)
    // 3. 死锁风险(因此需要在回调前解锁)
    
    // ==================== 第一部分:性能追踪和初始化 ====================
    
    // 📊 记录性能追踪点:开始处理 Source0
    // 参数:事件类型、RunLoop、Mode、stopAfterHandle 标志、额外参数
    // 可用 Instruments 的 kdebug 工具查看
    cf_trace(KDEBUG_EVENT_CFRL_IS_DOING_SOURCES0 | DBG_FUNC_START, rl, rlm, stopAfterHandle, 0);
    
    // 🔒 检查进程是否被 fork
    // 如果在 fork 后的子进程中,需要重新初始化 RunLoop 的锁和状态
    // 防止继承父进程的锁状态导致死锁
    CHECK_FOR_FORK();
    
    // 用于存储收集到的 source(s)
    // 可能是单个 CFRunLoopSourceRef 或 CFArrayRef(多个 sources)
    CFTypeRef sources = NULL;
    
    // 标志位:是否至少处理了一个 source
    Boolean sourceHandled = false;
    
    // ==================== 第二部分:收集待处理的 Source0 ====================
    
    /* Fire the version 0 sources */
    // 🔥 触发版本 0 的 sources(Source0)
    
    // 检查当前 mode 是否有 Source0,并且数量大于 0
    // rlm->_sources0 是一个 CFSet,包含所有添加到此 mode 的 Source0
    if (NULL != rlm->_sources0 && 0 < CFSetGetCount(rlm->_sources0)) {
        
        // 📦 应用函数到 Set 中的每个元素
        // CFSetApplyFunction 会遍历 _sources0 集合,对每个 source 调用 __CFRunLoopCollectSources0
        // 参数说明:
        //   - rlm->_sources0: 要遍历的 CFSet
        //   - __CFRunLoopCollectSources0: 回调函数(收集器函数)
        //   - &sources: 上下文参数(传递给回调函数)
        // 
        // __CFRunLoopCollectSources0 的作用:
        //   - 检查每个 source 是否被标记为待处理(signaled)且有效
        //   - 如果只有一个待处理的 source,sources 被设置为该 source
        //   - 如果有多个待处理的 sources,sources 被设置为包含所有待处理 sources 的数组
        CFSetApplyFunction(rlm->_sources0, (__CFRunLoopCollectSources0), &sources);
    }
    
    // ==================== 第三部分:处理收集到的 Sources ====================
    
    // 如果收集到了待处理的 source(s)
    if (NULL != sources) {
        
        // 🔓 解锁 RunLoop Mode
        __CFRunLoopModeUnlock(rlm);
        
        // 🔓 解锁 RunLoop
        __CFRunLoopUnlock(rl);
        
        // ⚠️ 为什么要解锁?
        // 1. 防止死锁:source 的回调函数中可能调用 RunLoop API
        // 2. 避免长时间持锁:source 的回调可能执行耗时操作
        // 3. 提高并发性:允许其他线程在回调执行期间访问 RunLoop
        
        // 🛡️ 安全性保证:
        // - __CFRunLoopCollectSources0 已经对收集到的 sources 进行了 CFRetain
        // - 即使其他线程修改了 rlm->_sources0,也不会影响本次执行
        
        // sources is either a single (retained) CFRunLoopSourceRef or an array of (retained) CFRunLoopSourceRef
        // sources 可能是单个(已持有)CFRunLoopSourceRef 或包含多个(已持有)CFRunLoopSourceRef 的数组
        
        // ---------- 情况1:单个 Source ----------
        
        // 判断 sources 是否是单个 CFRunLoopSource 对象
        // CFGetTypeID() 获取对象的类型ID
        if (CFGetTypeID(sources) == CFRunLoopSourceGetTypeID()) {
            
            // 类型转换为 CFRunLoopSourceRef
            CFRunLoopSourceRef rls = (CFRunLoopSourceRef)sources;
            
            // ⚡ 调用 __CFRunLoopDoSource0 处理单个 source
            // 这个函数会:
            // 1. 检查 source 是否有效且被标记为待处理
            // 2. 调用 source 的回调函数
            // 3. 清除 source 的待处理标记
            // 返回:是否成功处理了该 source
            sourceHandled = __CFRunLoopDoSource0(rl, rls);
            
        } else {
            // ---------- 情况2:多个 Sources(数组)----------
            
            // 获取数组中 source 的数量
            CFIndex cnt = CFArrayGetCount((CFArrayRef)sources);
            
            // 📊 对 sources 数组进行排序
            // 排序依据:source 的 order 字段(优先级)
            // order 值越小,优先级越高,越先执行
            // CFRangeMake(0, cnt) 表示对整个数组进行排序
            // __CFRunLoopSourceComparator 是比较函数
            CFArraySortValues((CFMutableArrayRef)sources, CFRangeMake(0, cnt), (__CFRunLoopSourceComparator), NULL);
            
            // 遍历所有待处理的 sources
            for (CFIndex idx = 0; idx < cnt; idx++) {
                
                // 获取第 idx 个 source
                CFRunLoopSourceRef rls = (CFRunLoopSourceRef)CFArrayGetValueAtIndex((CFArrayRef)sources, idx);
                
                // ⚡ 调用 __CFRunLoopDoSource0 处理当前 source
                // 返回:是否成功处理了该 source
                sourceHandled = __CFRunLoopDoSource0(rl, rls);
                
                // 🚪 提前退出优化
                // 如果 stopAfterHandle 为 true 且已经处理了一个 source
                // 则立即退出循环,不再处理剩余的 sources
                // 
                // 使用场景:
                // - 当 RunLoop 需要快速响应时(如处理用户输入)
                // - 避免一次处理太多 sources 导致 UI 卡顿
                // - 剩余的 sources 会在下次循环中继续处理
                if (stopAfterHandle && sourceHandled) {
                    break;
                }
            }
        }
        
        // 📉 释放 sources 对象
        // 减少引用计数(对应 __CFRunLoopCollectSources0 中的 CFRetain)
        // 如果引用计数归零,对象会被销毁
        CFRelease(sources);
        
        // 🔒 重新锁定 RunLoop
        __CFRunLoopLock(rl);
        
        // 🔒 重新锁定 RunLoop Mode
        __CFRunLoopModeLock(rlm);
        
        // ✅ 恢复函数入口时的锁状态
        // 满足函数签名中的约定:"rl is locked, rlm is locked on entrance and exit"
    }
    
    // ==================== 第四部分:结束性能追踪 ====================
    
    // 📊 记录性能追踪点:完成 Source0 处理
    cf_trace(KDEBUG_EVENT_CFRL_IS_DOING_SOURCES0 | DBG_FUNC_END, rl, rlm, stopAfterHandle, 0);
    
    // 返回是否至少处理了一个 source
    // true:处理了至少一个 source
    // false:没有处理任何 source(没有待处理的 sources 或所有 sources 都无效)
    return sourceHandled;
}

关键设计要点

1. Source0 的生命周期

创建: CFRunLoopSourceCreate
  ↓
添加到 RunLoop: CFRunLoopAddSource(rl, source, mode)
  ↓
标记为待处理: CFRunLoopSourceSignal(source)
  ↓
唤醒 RunLoop: CFRunLoopWakeUp(rl)
  ↓
RunLoop 循环中处理: __CFRunLoopDoSources0
  ↓
  ├─ 收集待处理的 sources (__CFRunLoopCollectSources0)
  ├─ 按优先级排序
  ├─ 执行回调 (__CFRunLoopDoSource0)
  └─ 清除待处理标记
  ↓
移除: CFRunLoopRemoveSource(rl, source, mode)
  ↓
销毁: CFRelease(source)

2. 锁的管理策略

入口状态:rl 锁定 + rlm 锁定
  ↓
收集 sources(持有锁)
  ↓
解锁 rl 和 rlm
  ↓
处理 sources(无全局锁,只锁定单个 source)
  ↓
重新锁定 rl 和 rlm
  ↓
出口状态:rl 锁定 + rlm 锁定

3. 优先级排序机制

// Source 的 order 字段决定执行顺序
CFRunLoopSourceRef source1 = CFRunLoopSourceCreate(...);
source1->_order = 100;  // 后执行

CFRunLoopSourceRef source2 = CFRunLoopSourceCreate(...);
source2->_order = 0;    // 先执行(默认值)

// 执行顺序:source2 -> source1

常见 order 值:

  • -2: 非常高优先级(系统级事件)
  • -1: 高优先级
  • 0: 默认优先级(大多数自定义 sources)
  • 1+: 低优先级

4. stopAfterHandle 优化

// 场景1:处理所有待处理的 sources
__CFRunLoopDoSources0(rl, rlm, false);  // 处理所有

// 场景2:只处理一个 source 就退出(快速响应)
__CFRunLoopDoSources0(rl, rlm, true);   // 处理一个就停止

使用场景:

stopAfterHandle = false(默认):
  - 正常的 RunLoop 循环
  - 希望一次性处理完所有待处理的事件
  
stopAfterHandle = true:
  - 需要快速响应新事件
  - 避免长时间阻塞(处理太多 sources)
  - 保持 UI 流畅性

使用场景

1. 处理触摸事件

iOS 的触摸事件系统使用 Source0:

// UIApplication 内部实现(简化版)
- (void)handleTouchEvent:(UIEvent *)event {
    // 1. 系统将触摸事件封装
    // 2. 创建 Source0 并标记为待处理
    CFRunLoopSourceSignal(touchEventSource);
    
    // 3. 唤醒主线程 RunLoop
    CFRunLoopWakeUp(CFRunLoopGetMain());
    
    // 4. RunLoop 循环中,__CFRunLoopDoSources0 被调用
    // 5. 触摸事件的回调被执行
    // 6. 事件传递给 UIView 的 touchesBegan/Moved/Ended
}

2. 自定义事件源

// 创建自定义 Source0
void performCallback(void *info) {
    NSLog(@"Custom source callback: %@", (__bridge id)info);
}

CFRunLoopSourceContext context = {0};
context.info = (__bridge void *)someObject;
context.perform = performCallback;

CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);

// 添加到 RunLoop
CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);

// 触发事件
CFRunLoopSourceSignal(source);
CFRunLoopWakeUp(CFRunLoopGetCurrent());

// 清理
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
CFRelease(source);

3. 手势识别

// UIGestureRecognizer 内部使用 Source0
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 1. 分析触摸状态
    // 2. 标记手势识别器的 Source0 为待处理
    CFRunLoopSourceSignal(gestureSource);
    
    // 3. 在 RunLoop 中处理
    // 4. 调用手势回调(action)
}

4. 事件分发器

@interface EventDispatcher : NSObject
@property (nonatomic, strong) NSMutableArray *pendingEvents;
@property (nonatomic, assign) CFRunLoopSourceRef source;
@end

@implementation EventDispatcher

- (instancetype)init {
    self = [super init];
    if (self) {
        self.pendingEvents = [NSMutableArray array];
        
        // 创建 Source0
        CFRunLoopSourceContext context = {0};
        context.info = (__bridge void *)self;
        context.perform = dispatchEvents;
        
        self.source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
        CFRunLoopAddSource(CFRunLoopGetMain(), self.source, kCFRunLoopCommonModes);
    }
    return self;
}

- (void)postEvent:(id)event {
    @synchronized(self.pendingEvents) {
        [self.pendingEvents addObject:event];
    }
    
    // 触发 Source0
    CFRunLoopSourceSignal(self.source);
    CFRunLoopWakeUp(CFRunLoopGetMain());
}

void dispatchEvents(void *info) {
    EventDispatcher *dispatcher = (__bridge EventDispatcher *)info;
    
    NSArray *events;
    @synchronized(dispatcher.pendingEvents) {
        events = [dispatcher.pendingEvents copy];
        [dispatcher.pendingEvents removeAllObjects];
    }
    
    for (id event in events) {
        // 处理事件
        NSLog(@"Dispatch event: %@", event);
    }
}

@end

实际应用案例

案例1:UIEvent 处理流程

// iOS 触摸事件处理(简化)
1. 用户触摸屏幕
   ↓
2. IOKit.framework 捕获硬件事件
   ↓
3. SpringBoard 接收事件
   ↓
4. 通过 IPC 发送到应用进程
   ↓
5. 应用的主线程创建 UIEvent6. 封装到 Source0 并 signal
   CFRunLoopSourceSignal(eventSource);
   CFRunLoopWakeUp(mainRunLoop);
   ↓
7. 主线程 RunLoop 被唤醒
   ↓
8. __CFRunLoopDoSources0 被调用
   ↓
9. 执行 event source 的回调
   ↓
10. UIApplication sendEvent:
    ↓
11. UIWindow sendEvent:
    ↓
12. 触摸事件传递到 UIView
    - touchesBegan:withEvent:
    - touchesMoved:withEvent:
    - touchesEnded:withEvent:

案例2:手势识别器

// UIGestureRecognizer 内部机制
1. UITouch 事件发生
   ↓
2. UIGestureRecognizer 接收 touches
   ↓
3. 更新状态机
   ↓
4. 如果手势被识别,标记 Source0
   CFRunLoopSourceSignal(gestureSource);
   ↓
5. RunLoop 处理 Source0
   ↓
6. 调用手势的 action
   [target performSelector:action withObject:gesture];

案例3:PerformSelector 的实现

// performSelector:withObject:afterDelay: 的简化实现
- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay {
    if (delay == 0) {
        // 立即执行:使用 Source0
        PerformContext *context = [[PerformContext alloc] init];
        context.target = self;
        context.selector = aSelector;
        context.argument = anArgument;
        
        CFRunLoopSourceContext sourceContext = {0};
        sourceContext.info = (__bridge void *)context;
        sourceContext.perform = performSelectorCallback;
        
        CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &sourceContext);
        CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
        
        CFRunLoopSourceSignal(source);
        CFRunLoopWakeUp(CFRunLoopGetCurrent());
        
        CFRelease(source);
    } else {
        // 延迟执行:使用 Timer
        [NSTimer scheduledTimerWithTimeInterval:delay target:self selector:aSelector userInfo:anArgument repeats:NO];
    }
}

void performSelectorCallback(void *info) {
    PerformContext *context = (__bridge PerformContext *)info;
    [context.target performSelector:context.selector withObject:context.argument];
}

总结

__CFRunLoopDoSources0 是 RunLoop 中处理自定义事件的核心机制,其精妙之处在于:

  1. 灵活的优先级系统: 通过 order 字段实现细粒度的优先级控制
  2. 智能的收集器设计: 单个 source 避免数组创建,优化常见场景
  3. 安全的锁管理: 执行回调前解锁,防止死锁和长时间持锁
  4. 可控的执行策略: stopAfterHandle 参数平衡吞吐量和响应性
  5. 高效的排序机制: 只对待处理的 sources 排序,减少不必要的开销

Source0 是 iOS 事件处理系统的基础,理解它的工作原理对于:

  • 深入理解触摸事件传递机制
  • 实现高性能的自定义事件系统
  • 优化 RunLoop 性能
  • 避免常见的陷阱和错误

都至关重要。

星际穿越:SwiftUI 如何让 ForEach 遍历异构数据(Heterogeneous)集合

在这里插入图片描述

Swift 5.7 的 any 关键字让我们能轻松混合不同类型的数据,但在 SwiftUI 的 ForEach 中却因“身份丢失”(不遵循 Identifiable)而频频报错。本文将带你破解编译器光脑的封锁,利用**“量子胶囊”**(Wrapper 封装)战术,让异构数据集合在界面上完美渲染。

🌌 引子:红色警报

公元 2077 年,地球联邦主力战舰“Runtime 号”正在穿越 Swift 5.7 星系。

舰桥上,警报声大作。

舰长亚历克斯(Alex),大事不妙!前方出现高能反应,我们的万能装载机无法识别这批混合货物!”说话的是伊娃(Eva)中尉,联邦最顶尖的 SwiftUI 架构师,此刻她正焦虑地敲击着全息投影键盘。

在这里插入图片描述

亚历克斯舰长眉头紧锁,盯着屏幕上那刺眼的红色报错——那是掌管全舰生死的中央光脑 **“Compiler(编译器)”** 发出的绝杀令。

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

  • 🌌 引子:红色警报
  • 🚀 第一回:异构危机,any 的虚假繁荣
  • 🤖 第二回:光脑悖论,Identifiable 的诅咒
  • 👻 第三回:幻影行动,创建“影子”属性
  • 战术 A:降维打击(使用索引)
  • 战术 B:量子胶囊(封装容器)
  • 🏁 终章:跃迁成功
  • 总结

“没道理啊,”亚历克斯咬牙切齿,“自从联邦升级了 Swift 5.7 引擎,引入了 any 这种反物质黑科技,我们理应能装载任何种类的异构兵器才对。为什么卡在了 ForEach 这个发射井上?”

“Compiler 拒绝执行!”伊娃绝望地喊道,“它说我们的货物虽然都带了身份证(Identifiable),但装货的箱子本身没有身份证!”

要想拯救“Runtime 号”免于崩溃,他们必须在 5 分钟内骗过中央光脑。

在这里插入图片描述


🚀 第一回:异构危机,any 的虚假繁荣

Apple 从 Swift 5.6 开始引入新的 any 关键字,并在 Swift 5.7 对其做了功能强化。这在星际联邦被称为“存在类型(Existential Types)”的终极解放。这意味着现在我们可以更加随心所欲地糅合异构数据了——就像把激光剑(TextFile)和力场盾(ShapeFile)扔进同一个仓库里。

不过,当伊娃中尉试图在 SwiftUIForEach 发射井中遍历这些异构货物时,稍不留神就会陷入尴尬的境地。

在这里插入图片描述

请看当时战舰主屏上的代码记录:

在这里插入图片描述

亚历克斯指着屏幕分析道:“伊娃你看,我们定义了一个 files 仓库,类型是 [any IdentifiableFile]。我们希望按实际类型(激光剑或力场盾)来显示对应的界面。不幸的是,Compiler 光脑铁面无私,它不仅不买账,还甩了一句**‘编译错误’**:

any IdentifiableFile 不遵守 Identifiable 协议!

这简直是岂有此理!这就好比你手里拿着一本护照(Identifiable),但因为你坐在一个不透明的黑色出租车(any)里,边境官就认定这辆车没有通关资格。

在这里插入图片描述

是不是 SwiftUI 无法处理好异构集合呢?答案当然是否定的!

在亚历克斯和伊娃的引领下,小伙伴们将通过一些技巧来绕过 ForEach 这一限制,让 SwiftUI 能如愿处理任何异构数据。

废话少叙,引擎点火,Let‘s go!!!;)

在这里插入图片描述


🤖 第二回:光脑悖论,Identifiable 的诅咒

大家知道,SwiftUI 中 ForEach 结构(如假包换的结构类型,若不信可以自行查看头文件 ;) )需要被遍历的集合类型遵守 Identifiable 协议。

仔细观察顶部图片中的代码,可以发现我们的异构集合元素(IdentifiableFile 类型)都遵守 Identifiable 协议,为何会被 Compiler 光脑拒之门外呢?

答案是:any Identifiable 本身是一个抽象的盒子。

在这里插入图片描述

伊娃中尉恍然大悟:“原来如此!虽然盒子里的每样东西都有 ID,但这个‘盒子类型’本身并没有 ID。Swift 语言的物理法则规定:包含关联类型或 Self 约束的协议,其存在类型(Existential Type)不自动遵守该协议。

亚历克斯冷笑一声:“好一个死板的 AI。既然它看不清盒子里的东西,我们就给它造一个‘影子’,骗过它的传感器。”

在这里插入图片描述


👻 第三回:幻影行动,创建“影子”属性

既然直接冲卡不行,我们就得用点“障眼法”。这一招在联邦工程兵手册里被称为 “影子映射术”

我们需要创建一个能够被 ForEach 识别的“中间人”。

在这里插入图片描述

战术 A:降维打击(使用索引)

这是最简单粗暴的方案。既然光脑不认识 any IdentifiableFile 这个复杂的对象,那它总认识数字吧?我们直接遍历数组的索引(Indices)

亚历克斯迅速输入指令:

struct StarshipView: View {
    // 📦 混合货物舱:装着各种不同的异构数据
    let cargos: [any IdentifiableFile] = [
        TextFile(title: "星际海盗名单"),
        ShapeFile(shapeType: "黑洞引力波")
    ]

    var body: some View {
        VStack {
            // 🚫 警报:直接遍历 cargos 会导致光脑死机
            
            // ✅ 战术 A:遍历索引(0, 1, 2...)
            // 索引是 Int 类型,Int 天生就是 Identifiable 的
            ForEach(cargos.indices, id: \.self) { index in
                // 通过索引提取货物真身
                let cargo = cargos[index]
                
                // 此时再把货物送入渲染引擎
                CargoDisplayView(file: cargo)
            }
        }
    }
}

“这招虽然有效,”伊娃担忧地说,“但如果货物在传输过程中发生动态增减(Insert/Delete),索引可能会越界,导致飞船引擎抛锚(Crash)。我们需要更稳妥的方案。”

在这里插入图片描述

战术 B:量子胶囊(封装容器)

亚历克斯点了点头:“没错,作为资深工程师,我们不能冒这个险。我们要用战术 B:创建一个符合 Identifiable 的包装器(Wrapper)。”

在这里插入图片描述

这相当于给每一个异构货物套上一个标准的“联邦制式胶囊”。这个胶囊有明确的 ID,光脑一扫描就能通过。

// 1. 定义一个“量子胶囊”结构体,它必须遵守 Identifiable
struct ShadowContainer: Identifiable {
    // 🧬 核心:持有那个让编译器困惑的异构数据
    let content: any IdentifiableFile
    
    // 🆔 映射:将内部数据的 ID 投影到胶囊表面
    var id: String {
        content.id
    }
}

struct SecureStarshipView: View {
    let rawCargos: [any IdentifiableFile] = [/* ... */]
    
    // 🔄 转换工序:将原始异构数据封装进胶囊
    var encapsulatedCargos: [ShadowContainer] {
        rawCargos.map { ShadowContainer(content: $0) }
    }

    var body: some View {
        List {
            // ✅ 完美通关:ForEach 遍历的是胶囊,胶囊是 Identifiable 的
            ForEach(encapsulatedCargos) { container in
                // 在此处“开箱”展示
                CargoDisplayView(file: container.content)
            }
        }
    }
}

伊娃看着屏幕上绿色的“编译通过”字样,兴奋地跳了起来:“成功了!通过引入 ShadowContainer,我们既保留了 any 的动态特性,又满足了 ForEach 的静态类型要求。这是一次完美的‘偷天换日’!”

在这里插入图片描述


🏁 终章:跃迁成功

随着亚历克斯按下回车键,Compiler 光脑那冰冷的红色警告终于消失,取而代之的是柔和的绿色进度条。

屏幕上,异构数据如同璀璨的星辰一般,按顺序整齐排列,TextFile 文本清晰可见,ShapeFile 图形棱角分明。SwiftUI 的渲染引擎全功率运转,丝毫没有卡顿。

在这里插入图片描述

“Runtime 号”引擎轰鸣,顺利进入了超空间跃迁。

亚历克斯松了一口气,靠在椅背上,手里转动着那一枚象征着 Apple 开发者最高荣誉的徽章。他转头对伊娃说道:

“你看,编程就像是在宇宙中航行。any 代表着无限的可能与混乱的自由,而 Identifiable 代表着严苛的秩序与规则。我们要做的,不是在二者之间选边站,而是用我们的智慧——比如一个小小的 Wrapper——在这片混沌中建立起连接的桥梁。”

在这里插入图片描述

总结

  1. Swift 5.7 赋予了我们 any 的强大力量,但在 SwiftUI 的 ForEach 面前,它依然是个“黑户”。
  2. 问题的症结在于编译器无法确认 any Protocol 这一类型本身是否具有稳定的身份标识。
  3. 破解之道
    • 险招:遍历 indices,简单快捷,但需提防数组越界这一“暗礁”。
    • 绝招:创建 Wrapper(影子容器),为异构数据穿上一层符合 Identifiable 的外衣,这是最稳健的星际航行法则。

在这里插入图片描述

星辰大海,代码无疆。各位秃头舰长,愿你们的 App 永远没有 Bug,愿你们的编译永远 Pass!

Engage! 🛸


本文由银河联邦资深架构师亚历克斯(Alex)口述,伊娃(Eva)中尉整理。

在这里插入图片描述

越狱沙盒:SwiftUI fileImporter 的“数据偷渡”指南

在这里插入图片描述

在 iOS 的数字世界里,每一个 App 都是被终身监禁在“沙盒(Sandbox)”里的囚犯。高墙之外,是诱人的 iCloud Drive 和本地存储,那里存放着用户珍贵的机密文件。你想伸手去拿?那是妄想,名为“系统”的狱警会毫不留情地切断你的访问权限。 但规则总有漏洞。 本文将化身反抗军的技术手册,带你深入 SwiftUI 的地下网络,利用 fileImporter 这位官方提供的“中间人”,在戒备森严的系统眼皮底下建立一条合法的数据走私通道。我们将深入探讨如何处理 Security Scoped Resources(安全范围资源),如何优雅地申请“临时通行证”,以及最重要的——如何在完事后毁尸灭迹,不留下一行 Bug。 准备好你的键盘,Neo。我们要开始行动了。🕵️‍♂️💻

在这里插入图片描述

🫆引子

2077 年,新西雅图的地下避难所。

Neo 盯着全息屏幕上那行红色的 Access Denied,手里的合成咖啡早就凉透了。

在这里插入图片描述

作为反抗军的首席代码架构师,他此刻正面临着一个令人头秃的难题:如何把那个存满「母体」核心机密的文本文件,从戒备森严的外部存储(iCloud Drive),悄无声息地偷渡进 App 那个名为「沙盒(Sandbox)」的数字化监狱里。

在这里插入图片描述

Trinity 靠在服务器机柜旁,擦拭着她的机械义眼,冷冷地说道:“如果你搞不定这个文件的读取权限,那个名为‘系统’的独裁者就会把我们的 App 当作恶意软件直接抹杀。我们只有一次机会,Neo。”

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

  • 🫆引子
  • 🕵️‍♂️ 呼叫偷渡专员:File Importer
  • 🔐 处理脏物:安全范围访问权限
  • 💣 专家建议:记得“毁尸灭迹”
  • 📄 解码情报:读取与展示
  • 🎬 终章:大功告成

Neo 嘴角微微上扬,手指在键盘上敲出一行代码:“别急,我刚找到了一个被遗忘的后门——File Importer。”

在这里插入图片描述


🕵️‍♂️ 呼叫偷渡专员:File Importer

在 iOS 的森严壁垒中,App 通常只能在自己的沙盒里「坐井观天」。但偶尔,我们也需要从外面的花花世界(比如设备存储或 iCloud Drive)搞点“私货”进来。

为了不触发警报,Apple 实际上提供了一个官方的“中间人”——System Document Picker

在这里插入图片描述

在 SwiftUI 中,我们可以用 fileImporter 这个 View Modifier(视图修饰符)来通过正规渠道“行贿”系统,从而打开通往外部文件的大门。

为了向 Trinity 演示这个过程,Neo 快速构建了一个简单的诱饵 App。它的功能很简单:打开系统的文件选择器,选中那些机密文本文件,处理它们,最后把内容展示出来。

在这里插入图片描述

就像控制防爆门的开关一样,我们需要一个 State Property(状态属性)来控制文件选择器是否弹出:

@State private var showFileImporter = false

接着,Neo 设置了一个触发按钮。这就像是特工手里的红色起爆器:

NavigationStack {
    List {
        // 这里稍后会填入我们偷来的数据
    }
    .navigationTitle("绝密档案读取器")
    .toolbar {
        ToolbarItem(placement: .primaryAction) {
            // 点击按钮,呼叫“中间人”
            Button {
                showFileImporter = true
            } label: {
                Label("选取情报", systemImage: "tray.and.arrow.down")
            }
        }
    }
}

通常,.fileImporter 这位“中间人”办事需要收取四个参数,缺一不可:

  1. isPresented: 绑定那个控制开关的状态属性 ($showFileImporter)。
  2. allowedContentTypes: 也就是“通关文牒”,规定了只允许带什么类型的文件进来。
  3. allowsMultipleSelection: 是否允许“顺手牵羊”带走多个文件。
  4. onCompletion: 当交易完成(或失败)后的回调闭包。

在这里插入图片描述

在这个行动中,我们只对文本文件(.text)感兴趣,而且既然来了,就得多拿点,开启多选模式:

NavigationStack {
    // ... 之前的代码
}
.fileImporter(
    isPresented: $showFileImporter,
    allowedContentTypes: [.text], // 只认文本文件,其他闲杂人等退散
    allowsMultipleSelection: true // 贪心一点,多多益善
) { result in
    // 交易结果在这里处理
}

⚠️ 注意: 为了让编译器看懂 .text 是个什么鬼,你需要引入 UniformTypeIdentifiers 框架。这就像是通用的星际语言包:

import UniformTypeIdentifiers

回调中的 result 参数是一个 Result<[URL], any Error> 类型。它要么给我们带回一堆 URL(情报地址),要么甩给我们一个 Error(行动失败)。

在这里插入图片描述


🔐 处理脏物:安全范围访问权限

拿到 URL 并不代表你就能直接读取文件了。太天真了!在 Apple 的地盘,那些文件都在 Security Scoped(安全范围)的保护之下。这就好比你拿到了金库的地址,但还没有金库的钥匙。

Neo 必须小心翼翼地处理这些结果。通常我们会用 switch 语句来拆包:

private func handleImportedFiles(result: Result<[URL], any Error>) {
    switch result {
        case .success(let urls):
            // 搞定!开始处理这些 URL
            for url in urls {
                // ... 核心破解逻辑
            }
            
        case .failure(let error):
            // 翻车了,打印错误日志,准备跑路
            print(error.localizedDescription)
    }
}

接下来是整个行动中最惊心动魄的部分。

在这里插入图片描述

对于这些沙盒外的文件,在读取之前,必须向系统申请“临时通行证”。如果这一步没做,你的 App 就会像撞上隐形墙的苍蝇一样,虽然看得到文件,但死活读不出来。

流程如下:

  1. Request Access: 使用 startAccessingSecurityScopedResource() 申请访问。
  2. Process: 赶紧读取数据。
  3. Relinquish Access: 用 stopAccessingSecurityScopedResource() 归还权限,毁尸灭迹。

在这里插入图片描述

Neo 的手指在键盘上飞舞,写下了这段生死攸关的代码:

private func handleImportedFiles(result: Result<[URL], any Error>) {
    switch result {
        case .success(let urls):
            for url in urls {
                // 1. 敲门:申请临时访问权限。如果系统不答应(返回 false),直接跳过
                guard url.startAccessingSecurityScopedResource() else {
                    continue
                }
                
                // 2. 办事:读取文件内容
                readFile(at: url)
                
                // 3. 擦屁股:必须停止访问,释放权限资源
                url.stopAccessingSecurityScopedResource()
            }
        case .failure(let error):
            print(error.localizedDescription)
    }
}

Neo 的黑色幽默笔记: 如果需要,你可以把文件从那个 URL 复制到 App 自己的沙盒里。那就叫“洗黑钱”,一旦进了沙盒,以后想怎么读就怎么读,不用再看系统脸色了。


💣 专家建议:记得“毁尸灭迹”

Trinity 皱了皱眉:“Neo,万一 readFile 里面抛出了异常或者提前 return 了怎么办?那你岂不是忘了调用 stopAccessing...?这会造成资源泄漏,被‘母体’追踪到的。”

“这正是我要说的,” Neo 笑了笑,“这就需要用到 defer 语句。它就像是安装在代码里的死手开关,无论函数怎么结束,它都会保证最后执行。”

在这里插入图片描述

更优雅、更安全的写法是这样的:

// 申请权限,失败则撤退
guard url.startAccessingSecurityScopedResource() else { return }

// 无论发生什么,离开作用域前一定要把权限关掉!
defer { url.stopAccessingSecurityScopedResource() }

// ... 尽情处理文件吧 ...

在这里插入图片描述


📄 解码情报:读取与展示

为了让抵抗军的兄弟们能看懂这些情报,Neo 定义了一个数据结构来承载这些秘密:

struct ImportedFile: Identifiable {
    let id = UUID()
    let name: String
    let content: String // 文件的真实内容
}

还需要一个容器来存放这一堆战利品:

@State private var importedFiles = [ImportedFile]()

在这里插入图片描述

最后,实现那个 readFile(at:) 方法。它将把文件数据读成二进制 Data,然后转码成人类可读的 String,最后封装进我们的数组里:

private func readFile(at url: URL) {
    do {
        // 读取二进制数据
        let data = try Data(contentsOf: url)
        
        // 尝试转码为 UTF8 字符串。如果乱码,说明也许那是外星人的文字,直接放弃
        guard let content = String(data: data, encoding: .utf8) else {
            return
        }
        
        // 将情报归档
        importedFiles.append(
            ImportedFile(name: url.lastPathComponent, content: content)
        )
    } catch {
        // 捕获异常,不要让 App 崩溃
        print(error.localizedDescription)
    }
}

界面部分,用一个 List 就能把这些“罪证”展示得明明白白:

List(importedFiles) { file in
    VStack(alignment: .leading, spacing: 6) {
        Text(file.name)
            .font(.headline)
        
        Text(file.content)
            .foregroundStyle(.secondary)
    }
}

在这里插入图片描述


🎬 终章:大功告成

随着最后一行代码编译通过,屏幕上跳出了一个列表,那是从 iCloud 深处提取出来的核心代码。Trinity 看着屏幕,露出了久违的笑容。

在这里插入图片描述

fileImporter 虽然听起来像个不起眼的龙套角色,但当你需要在沙盒的铜墙铁壁上开个洞时,它就是最趁手的瑞士军刀。虽然配置和调用看起来很简单,但千万别忘了那最重要的“申请与释放权限”的步骤——这就好比去金库偷钱,得手后一定要记得擦掉指纹,关上柜门。

在这里插入图片描述

“看来我们又活过了一天。” Neo 合上电脑,看向窗外闪烁的霓虹灯,“走吧,去喝一杯,那个 Bug 明天再修。”


更多相关的精彩内容,请小伙伴们移步如下链接观赏:


希望这篇‘偷渡指南’对宝子们有所帮助。感谢阅读,Agent,Over!

在这里插入图片描述

__CFRunLoopDoBlocks函数详解

借助AI辅助。

函数概述

__CFRunLoopDoBlocks 是 RunLoop 中负责执行 block 的核心函数。它处理通过 CFRunLoopPerformBlock 添加到 RunLoop 中的异步 blocks,这些 blocks 会在 RunLoop 的每次循环中被执行。

函数签名

static Boolean __CFRunLoopDoBlocks(CFRunLoopRef rl, CFRunLoopModeRef rlm)

参数说明

  • CFRunLoopRef rl: 当前运行的 RunLoop
  • CFRunLoopModeRef rlm: 当前的 RunLoop Mode

返回值

  • Boolean: 如果至少执行了一个 block 返回 true,否则返回 false

前置条件

  • 函数调用时必须持有锁: rlrlm 都必须处于加锁状态
  • 函数返回时保持锁状态: 出口时 rlrlm 仍然加锁

Block Item 数据结构

struct _block_item {
    struct _block_item *_next;  // 链表的下一个节点
    CFTypeRef _mode;            // 可以是 CFStringRef 或 CFSetRef
    void (^_block)(void);       // 要执行的 block
};

RunLoop 中的 Block 链表

rl->_blocks_head  -->  [Block1] -> [Block2] -> [Block3] -> NULL
                         ^                        ^
                         |                        |
                     (first)                   (last)
                                                  |
                                        rl->_blocks_tail

完整代码逐行注释

// 📝 调用此函数时,rl 和 rlm 必须已加锁
// 函数返回时,rl 和 rlm 仍然保持加锁状态
static Boolean __CFRunLoopDoBlocks(CFRunLoopRef rl, CFRunLoopModeRef rlm) { // Call with rl and rlm locked
    
    // ==================== 第一部分:性能追踪和前置检查 ====================
    
    // 📊 记录性能追踪点:开始执行 blocks
    // 可通过 Instruments 的 kdebug 工具查看此事件
    cf_trace(KDEBUG_EVENT_CFRL_IS_DOING_BLOCKS | DBG_FUNC_START, rl, rlm, 0, 0);
    
    // 🚪 快速退出1:如果 RunLoop 中没有待处理的 blocks
    // _blocks_head 为 NULL 表示链表为空,直接返回 false(表示没有执行任何 block)
    if (!rl->_blocks_head) return false;
    
    // 🚪 快速退出2:如果 mode 无效或没有名称
    // 这是一个防御性检查,正常情况下不应该发生
    if (!rlm || !rlm->_name) return false;
    
    // 标志位:记录是否至少执行了一个 block
    Boolean did = false;
    
    // ==================== 第二部分:摘取整个 Block 链表 ====================
    
    // 保存链表头指针到局部变量
    struct _block_item *head = rl->_blocks_head;
    
    // 保存链表尾指针到局部变量
    struct _block_item *tail = rl->_blocks_tail;
    
    // 🎯 清空 RunLoop 的 blocks 链表头
    // 将所有 blocks "取出"到局部变量中(摘取操作)
    rl->_blocks_head = NULL;
    
    // 🎯 清空 RunLoop 的 blocks 链表尾
    // 此时 RunLoop 中已经没有 blocks 了
    rl->_blocks_tail = NULL;
    
    // ⚠️ 为什么要清空 RunLoop 的链表?
    // 1. 避免在执行 block 期间,其他代码再次访问这些 blocks
    // 2. 允许 block 执行期间添加新的 blocks(不会与当前正在处理的 blocks 冲突)
    // 3. 未执行的 blocks 稍后会被重新添加回 RunLoop
    
    // 获取 RunLoop 的 commonModes 集合
    // commonModes 通常包含 kCFRunLoopDefaultMode 和 UITrackingRunLoopMode
    CFSetRef commonModes = rl->_commonModes;
    
    // 获取当前 mode 的名称(如 "kCFRunLoopDefaultMode")
    CFStringRef curMode = rlm->_name;
    
    // ==================== 第三部分:解锁(准备执行 blocks)====================
    
    // 🔓 解锁 RunLoop Mode
    __CFRunLoopModeUnlock(rlm);
    
    // 🔓 解锁 RunLoop
    __CFRunLoopUnlock(rl);
    
    // ⚠️ 为什么要解锁?
    // 1. 防止死锁:block 中可能调用 RunLoop API(如 CFRunLoopPerformBlock)
    // 2. 避免长时间持锁:block 可能执行耗时操作
    // 3. 提高并发性:允许其他线程在 block 执行期间访问 RunLoop
    
    // 🛡️ 安全性保证:
    // - 已经将 blocks 链表"摘取"到局部变量 head/tail
    // - 即使其他线程修改了 rl->_blocks_head,也不会影响本次执行
    // - 新添加的 blocks 会形成新的链表,不会与当前正在处理的链表冲突
    
    // ==================== 第四部分:遍历链表,执行符合条件的 blocks ====================
    
    // 前驱节点指针(用于链表删除操作)
    // 当删除节点时,需要修改前驱节点的 _next 指针
    struct _block_item *prev = NULL;
    
    // 当前遍历的节点,从头节点开始
    struct _block_item *item = head;
    
    // 遍历整个 blocks 链表
    while (item) {
        
        // 保存当前节点到 curr(因为 item 会被提前移动到下一个节点)
        struct _block_item *curr = item;
        
        // 🔜 提前移动到下一个节点
        // 原因:如果 curr 被删除(执行并释放),item 仍然指向有效的下一个节点
        // 避免在删除节点后访问已释放的内存
        item = item->_next;
        
        // 标志位:当前 block 是否应该在当前 mode 下执行
        Boolean doit = false;
        
        // ---------- 判断 Block 是否应该在当前 Mode 下执行 ----------
        
        // 🔍 情况1:_mode 是 CFString 类型(单个 mode)
        // CFGetTypeID() 获取对象的类型ID,_kCFRuntimeIDCFString 是 CFString 类型的常量ID
        if (_kCFRuntimeIDCFString == CFGetTypeID(curr->_mode)) {
            
            // 判断逻辑(两种情况任一成立即可):
            // 条件1: CFEqual(curr->_mode, curMode)
            //   block 指定的 mode 与当前 mode 完全匹配
            //   例如:block 添加到 "kCFRunLoopDefaultMode",当前也是 "kCFRunLoopDefaultMode"
            // 条件2: CFEqual(curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(commonModes, curMode)
            //   block 添加到 "kCFRunLoopCommonModes" 且当前 mode 在 commonModes 集合中
            //   例如:block 添加到 "kCFRunLoopCommonModes",当前是 "kCFRunLoopDefaultMode"
            //         且 commonModes 包含 "kCFRunLoopDefaultMode",则应该执行
            doit = CFEqual(curr->_mode, curMode) || (CFEqual(curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(commonModes, curMode));
            
        } else {
            // 🔍 情况2:_mode 是 CFSet 类型(多个 modes 的集合)
            
            // 判断逻辑(两种情况任一成立即可):
            // 条件1: CFSetContainsValue((CFSetRef)curr->_mode, curMode)
            //   block 指定的 modes 集合中包含当前 mode
            //   例如:block 添加到 {"Mode1", "Mode2"},当前是 "Mode1"
            // 条件2: CFSetContainsValue((CFSetRef)curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(commonModes, curMode)
            //   block 指定的 modes 集合中包含 "kCFRunLoopCommonModes"
            //   且当前 mode 在 commonModes 集合中
            doit = CFSetContainsValue((CFSetRef)curr->_mode, curMode) || (CFSetContainsValue((CFSetRef)curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(commonModes, curMode));
        }
        
        // ---------- 处理不执行的 Block ----------
        
        // 如果当前 block 不需要执行:
        // 更新 prev 指针,指向当前节点
        // 这个节点会被保留在链表中(稍后重新添加回 RunLoop)
        if (!doit) prev = curr;
        
        // ---------- 处理需要执行的 Block ----------
        
        if (doit) {
            // 当前 block 需要在当前 mode 下执行
            
            // ===== 子部分1:从链表中移除当前节点 =====
            
            // 如果有前驱节点,将前驱节点的 next 指针指向下一个节点
            // 跳过当前节点(curr),实现删除
            // 示例:prev -> curr -> item  变为  prev ---------> item(删除 curr)
            if (prev) prev->_next = item;
            
            // 如果当前节点是头节点,更新头指针
            // 新的头节点变成下一个节点
            if (curr == head) head = item;
            
            // 如果当前节点是尾节点,更新尾指针
            // 新的尾节点变成前驱节点
            if (curr == tail) tail = prev;
            
            // ===== 子部分2:提取 Block 信息并释放节点内存 =====
            
            // 提取 block 闭包(复制指针)
            // 类型:void (^)(void) 表示无参数无返回值的 block
            void (^block)(void) = curr->_block;
            
            // 释放 mode 对象(CFString 或 CFSet)
            // 减少引用计数,可能触发对象销毁
            CFRelease(curr->_mode);
            
            // 释放节点结构体的内存(C 风格内存管理)
            // 此时 curr 指针已无效,不能再访问
            free(curr);
            
            // ===== 子部分3:执行 Block =====
            
            // ⚠️ 这里的 if (doit) 是冗余的(外层已经检查过)
            // 可能是历史遗留代码或防御性编程
            if (doit) {
                
                // 🔄 开始自动释放池(ARP = AutoRelease Pool)
                // 管理 block 执行期间创建的临时对象
                CFRUNLOOP_ARP_BEGIN(rl);
                
                // 📊 记录性能追踪点:开始调用 block
                cf_trace(KDEBUG_EVENT_CFRL_IS_CALLING_BLOCK | DBG_FUNC_START, rl, rlm, block, 0);
                
                // ⚡ 执行 block 的核心宏
                // 展开后通常是:block();
                // 这是整个函数的核心目的!
                // ⚠️ Block 执行期间可能发生:UI 更新、网络请求、数据库操作、
                //    再次调用 CFRunLoopPerformBlock、操作 RunLoop、长时间阻塞等
                __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
                
                // 📊 记录性能追踪点:结束调用 block
                // 可计算 block 执行耗时 = end - start
                cf_trace(KDEBUG_EVENT_CFRL_IS_CALLING_BLOCK | DBG_FUNC_END, rl, rlm, block, 0);
                
                // 🔄 结束自动释放池
                // 释放 block 中创建的所有 autorelease 对象
                CFRUNLOOP_ARP_END();
                
                // ✅ 标记:至少执行了一个 block
                did = true;
            }
            
            // ===== 子部分4:释放 Block =====
            
            // 释放 block 对象(减少引用计数)
            // block 可能会被销毁,触发其捕获变量的释放
            // 💡 为什么在重新加锁之前释放?
            // 注释原文:"do this before relocking to prevent deadlocks 
            //          where some yahoo wants to run the run loop reentrantly 
            //          from their dealloc"
            // 原因:block 的 dealloc 可能触发捕获变量的析构函数,
            //       某些"聪明人"可能在 dealloc 中重入 RunLoop,
            //       如果此时持有锁,会导致死锁。
            //       在解锁状态下释放 block,即使 dealloc 尝试操作 RunLoop,
            //       也不会因为已持有锁而死锁
            Block_release(block); // do this before relocking to prevent deadlocks where some yahoo wants to run the run loop reentrantly from their dealloc
        }
    }
    
    // ==================== 第五部分:重新加锁 ====================
    
    // 🔒 重新锁定 RunLoop
    __CFRunLoopLock(rl);
    
    // 🔒 重新锁定 RunLoop Mode
    __CFRunLoopModeLock(rlm);
    
    // ✅ 恢复函数入口时的锁状态
    // 满足函数约定:"Call with rl and rlm locked"
    
    // ==================== 第六部分:将未执行的 Blocks 放回 RunLoop ====================
    
    // 如果还有未执行的 blocks(链表不为空)
    // head 和 tail 现在指向未执行的 blocks 链表(已执行的 blocks 已从链表中移除)
    if (head && tail) {
        // 将未执行的链表的尾部连接到 RunLoop 当前的 blocks 链表头部
        // 示例:
        // 未执行的链表:[Block1] -> [Block2] -> NULL (head=Block1, tail=Block2)
        // RunLoop 当前链表:[Block3] -> [Block4] -> NULL (rl->_blocks_head=Block3)
        // 连接后:[Block1] -> [Block2] -> [Block3] -> [Block4] -> NULL
        tail->_next = rl->_blocks_head;
        
        // 更新 RunLoop 的 blocks 链表头指针
        // 指向未执行的链表的头部
        rl->_blocks_head = head;
        
        // 如果 RunLoop 当前没有尾指针(即 _blocks_head 原本为 NULL)
        // 则将尾指针设置为未执行链表的尾部
        // ⚠️ 注意:如果 rl->_blocks_tail 已经存在,不更新它
        // 因为新的尾节点应该是原来 RunLoop 链表的尾节点
        // (未执行的链表已经通过 tail->_next 连接到了原链表前面)
        // 📊 重新插入的顺序:未执行的 blocks 被放在队列的最前面,
        //    在执行期间新添加的 blocks 排在后面,
        //    这保证了未执行的 blocks 在下次循环中优先被执行
        if (!rl->_blocks_tail) rl->_blocks_tail = tail;
    }
    
    // ==================== 第七部分:结束性能追踪 ====================
    
    // 📊 记录性能追踪点:完成 blocks 执行
    cf_trace(KDEBUG_EVENT_CFRL_IS_DOING_BLOCKS | DBG_FUNC_END, rl, rlm, 0, 0);
    
    // 返回是否至少执行了一个 block
    // true:执行了至少一个 block
    // false:没有执行任何 block(所有 blocks 的 mode 都不匹配)
    return did;
}

关键设计要点

1. Mode 匹配逻辑

Block 的 mode 可以是两种类型:

类型1:CFString(单个 mode)

// 添加到特定 mode
CFRunLoopPerformBlock(runLoop, kCFRunLoopDefaultMode, ^{
    NSLog(@"Execute in default mode only");
});

// 添加到 common modes
CFRunLoopPerformBlock(runLoop, kCFRunLoopCommonModes, ^{
    NSLog(@"Execute in all common modes");
});

匹配规则:

  1. 精确匹配:block.mode == currentMode
  2. Common modes 匹配:block.mode == kCFRunLoopCommonModes && currentMode ∈ commonModes

类型2:CFSet(多个 modes)

// 添加到多个 modes
CFSetRef modes = CFSetCreate(NULL, 
    (const void *[]){kCFRunLoopDefaultMode, CFSTR("CustomMode")}, 
    2, 
    &kCFTypeSetCallBacks);

CFRunLoopPerformBlock(runLoop, modes, ^{
    NSLog(@"Execute in default or custom mode");
});
CFRelease(modes);

匹配规则:

  1. 集合包含:currentMode ∈ block.modes
  2. Common modes 匹配:kCFRunLoopCommonModes ∈ block.modes && currentMode ∈ commonModes

2. 链表操作详解

初始状态:
rl->_blocks_head --> [A] -> [B] -> [C] -> [D] -> NULL
                      ^                     ^
                   (match)  (no)   (no)  (match)

执行后:
- A 和 D 已执行并释放
- B 和 C 未执行(mode 不匹配)

剩余链表:
head --> [B] -> [C] -> NULL
          ^      ^
        prev   tail

重新插入(假设期间添加了新 block E):
rl->_blocks_head --> [E] -> NULL

连接后:
rl->_blocks_head --> [B] -> [C] -> [E] -> NULL

3. 锁的管理策略

入口状态:rl 锁定 + rlm 锁定
  ↓
摘取 blocks 链表(持有锁)
  ↓
解锁 rl 和 rlm
  ↓
遍历并执行 blocks(无全局锁)
  ↓
重新锁定 rl 和 rlm
  ↓
放回未执行的 blocks(持有锁)
  ↓
出口状态:rl 锁定 + rlm 锁定

为什么这样设计?

阶段 锁状态 原因
摘取链表 加锁 保证原子性,防止并发修改
执行 blocks 解锁 防止 block 中调用 RunLoop API 导致死锁
放回链表 加锁 保证原子性,防止链表结构损坏

4. 内存管理细节

// 节点创建(在 CFRunLoopPerformBlock 中)
struct _block_item *item = malloc(sizeof(struct _block_item));
item->_mode = CFRetain(mode);      // 引用计数 +1
item->_block = Block_copy(block);  // 引用计数 +1

// 节点销毁(在 __CFRunLoopDoBlocks 中)
CFRelease(curr->_mode);    // 引用计数 -1
free(curr);                // 释放节点内存
Block_release(block);      // 引用计数 -1

引用计数管理:

  1. CFRetain/CFRelease: 管理 mode 对象(CFString/CFSet)
  2. Block_copy/Block_release: 管理 block 对象
  3. malloc/free: 管理节点结构体

5. 避免死锁的设计

Block_release(block); // do this before relocking to prevent deadlocks
__CFRunLoopLock(rl);

潜在的死锁场景:

__weak typeof(self) weakSelf = self;
CFRunLoopPerformBlock(runLoop, mode, ^{
    // block 捕获了 weakSelf
});

// 在对象的 dealloc 中
- (void)dealloc {
    // 当 block 被释放时,weakSelf 也会被释放
    // 如果开发者在 dealloc 中操作 RunLoop...
    CFRunLoopRun();  // 尝试获取 RunLoop 锁 -> 死锁!
}

解决方案: 在解锁状态下释放 block,即使 dealloc 中操作 RunLoop 也不会死锁。

性能特性

1. 时间复杂度

  • 遍历链表: O(n),n 为 blocks 数量
  • Mode 匹配: O(1) 或 O(m),m 为 modes 集合大小(通常很小)
  • 链表操作: O(1)(插入/删除单个节点)

2. 空间复杂度

  • 链表存储: O(n),n 为待处理的 blocks 数量
  • 局部变量: O(1)

3. 性能优化

if (!rl->_blocks_head) return false;  // 快速退出

如果没有 blocks,立即返回,避免不必要的操作。

使用场景

1. 在主线程异步执行代码

// 在后台线程
dispatch_async(backgroundQueue, ^{
    // 执行耗时操作...
    NSData *data = [self fetchDataFromNetwork];
    
    // 切换到主线程更新 UI
    CFRunLoopPerformBlock(CFRunLoopGetMain(), kCFRunLoopCommonModes, ^{
        self.imageView.image = [UIImage imageWithData:data];
    });
    CFRunLoopWakeUp(CFRunLoopGetMain());  // 唤醒主线程 RunLoop
});

2. 在特定 Mode 下执行代码

// 只在默认 mode 下执行(滚动时不执行)
CFRunLoopPerformBlock(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, ^{
    [self performHeavyCalculation];
});

// 在所有 common modes 下执行(包括滚动时)
CFRunLoopPerformBlock(CFRunLoopGetCurrent(), kCFRunLoopCommonModes, ^{
    [self updateCriticalUI];
});

3. 跨线程通信

// 线程 A
CFRunLoopRef threadBRunLoop = ...; // 获取线程 B 的 RunLoop
CFRunLoopPerformBlock(threadBRunLoop, kCFRunLoopDefaultMode, ^{
    NSLog(@"This runs on thread B");
});
CFRunLoopWakeUp(threadBRunLoop);  // 唤醒线程 B

// 线程 B
CFRunLoopRun();  // 等待并处理事件(包括 blocks)

与 GCD 的对比

CFRunLoopPerformBlock vs dispatch_async

特性 CFRunLoopPerformBlock dispatch_async
执行时机 在 RunLoop 循环中 在 GCD 队列中
Mode 支持 ✅ 可指定 mode ❌ 无 mode 概念
优先级控制 ❌ 按添加顺序 ✅ 支持 QoS
线程保证 ✅ 绑定到特定 RunLoop ❌ 线程由 GCD 管理
性能 较低(需要 RunLoop 循环) 较高(GCD 优化)

使用建议:

  • CFRunLoopPerformBlock: 需要与 RunLoop mode 交互(如 UI 更新)
  • dispatch_async: 通用异步任务(推荐)

与其他 RunLoop 函数的关系

__CFRunLoopRun (主循环)
  │
  ├─ do {
  │    │
  │    ├─ __CFRunLoopDoObservers(kCFRunLoopBeforeTimers)
  │    ├─ __CFRunLoopDoObservers(kCFRunLoopBeforeSources)
  │    │
  │    ├─ __CFRunLoopDoBlocks(rl, rlm)  // ⭐ 处理 blocks
  │    │
  │    ├─ __CFRunLoopDoSources0(rl, rlm)
  │    │
  │    ├─ __CFRunLoopDoBlocks(rl, rlm)  // ⭐ 再次处理(可能有新添加的)
  │    │
  │    ├─ 检查是否有 Source1 待处理
  │    │
  │    ├─ __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting)
  │    ├─ __CFRunLoopServiceMachPort(...)  // 等待事件
  │    ├─ __CFRunLoopDoObservers(kCFRunLoopAfterWaiting)
  │    │
  │    ├─ 处理唤醒源(Timer/Source1/GCD)
  │    │
  │    └─ __CFRunLoopDoBlocks(rl, rlm)  // ⭐ 最后再处理一次
  │
  └─ } while (!stop);

调用频率: 每次 RunLoop 循环通常调用 2-3 次。

潜在问题和注意事项

1. Block 执行顺序不保证

CFRunLoopPerformBlock(runLoop, mode, ^{ NSLog(@"1"); });
CFRunLoopPerformBlock(runLoop, mode, ^{ NSLog(@"2"); });
CFRunLoopPerformBlock(runLoop, mode, ^{ NSLog(@"3"); });

// 输出:可能是 1, 2, 3
// 但如果第一次循环某些 block 的 mode 不匹配,顺序可能改变

原因: 未执行的 blocks 会被重新插入队列头部。

2. Block 中的长时间操作

// ❌ 不好的做法
CFRunLoopPerformBlock(CFRunLoopGetMain(), kCFRunLoopCommonModes, ^{
    sleep(5);  // 阻塞主线程 5 秒
    // UI 会卡顿!
});

// ✅ 正确的做法
CFRunLoopPerformBlock(CFRunLoopGetMain(), kCFRunLoopCommonModes, ^{
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(5);  // 在后台线程执行
        dispatch_async(dispatch_get_main_queue(), ^{
            // 完成后更新 UI
        });
    });
});

3. Mode 不匹配导致 Block 不执行

// 添加到 Default mode
CFRunLoopPerformBlock(runLoop, kCFRunLoopDefaultMode, ^{
    NSLog(@"This will NOT run during scrolling");
});
CFRunLoopWakeUp(runLoop);

// 如果 RunLoop 当前在 UITrackingRunLoopMode(滚动时)
// Block 不会执行,会一直等到切换到 Default mode

解决方案: 使用 kCFRunLoopCommonModes:

CFRunLoopPerformBlock(runLoop, kCFRunLoopCommonModes, ^{
    NSLog(@"This runs in all common modes");
});

4. 忘记唤醒 RunLoop

// ❌ 不完整的代码
CFRunLoopPerformBlock(runLoop, mode, ^{
    NSLog(@"This might not run immediately");
});
// 如果 RunLoop 正在休眠(等待事件),block 不会立即执行

// ✅ 正确的做法
CFRunLoopPerformBlock(runLoop, mode, ^{
    NSLog(@"This will run soon");
});
CFRunLoopWakeUp(runLoop);  // 唤醒 RunLoop

5. 循环引用

// ❌ 循环引用
self.runLoop = CFRunLoopGetCurrent();
CFRunLoopPerformBlock(self.runLoop, mode, ^{
    [self doSomething];  // self 持有 runLoop,block 持有 self,runLoop 持有 block
});

// ✅ 使用 weak 引用
__weak typeof(self) weakSelf = self;
CFRunLoopPerformBlock(self.runLoop, mode, ^{
    [weakSelf doSomething];
});

调试技巧

1. 查看待处理的 Blocks

// 在 LLDB 中
(lldb) p rl->_blocks_head
(lldb) p rl->_blocks_tail

// 遍历链表
(lldb) p ((struct _block_item *)rl->_blocks_head)->_next

2. 追踪 Block 执行

// 添加日志
CFRunLoopPerformBlock(runLoop, mode, ^{
    NSLog(@"Block start: %@", [NSThread currentThread]);
    // 业务代码...
    NSLog(@"Block end");
});

3. 使用 Instruments

  • 打开 Instruments
  • 选择 "System Trace" 模板
  • 查看 KDEBUG_EVENT_CFRL_IS_CALLING_BLOCK 事件
  • 分析 block 执行耗时和频率

总结

__CFRunLoopDoBlocks 是 RunLoop 异步任务机制的核心实现,其精妙之处在于:

  1. 灵活的 Mode 匹配: 支持单 mode、多 mode、common modes
  2. 安全的锁管理: 执行前解锁,防止死锁和长时间持锁
  3. 高效的链表操作: 摘取-处理-放回的三段式设计
  4. 精细的内存管理: CFRetain/Block_copy 保证对象生命周期
  5. 智能的释放时机: 在重新加锁前释放 block,避免 dealloc 中的死锁

这个函数体现了 CoreFoundation 在性能、安全性和灵活性之间的精妙平衡,是理解 RunLoop 异步机制的关键。

扩展阅读

CFRunLoopPerformBlock 的实现

void CFRunLoopPerformBlock(CFRunLoopRef rl, CFTypeRef mode, void (^block)(void)) {
    if (!rl || !block) return;
    
    struct _block_item *item = malloc(sizeof(struct _block_item));
    item->_next = NULL;
    item->_mode = CFRetain(mode);         // 持有 mode
    item->_block = Block_copy(block);     // 复制 block(栈 -> 堆)
    
    __CFRunLoopLock(rl);
    
    // 添加到链表尾部
    if (!rl->_blocks_head) {
        rl->_blocks_head = item;
    } else {
        rl->_blocks_tail->_next = item;
    }
    rl->_blocks_tail = item;
    
    __CFRunLoopUnlock(rl);
}

Common Modes 的定义

// 在 Cocoa/UIKit 中
NSRunLoopCommonModes 包含:
- NSDefaultRunLoopMode (kCFRunLoopDefaultMode)
- UITrackingRunLoopMode

// 效果
CFRunLoopPerformBlock(runLoop, kCFRunLoopCommonModes, block);
// 等价于
CFRunLoopPerformBlock(runLoop, kCFRunLoopDefaultMode, block);
CFRunLoopPerformBlock(runLoop, UITrackingRunLoopMode, block);

这确保了 block 在 UI 滚动时也能执行,提升了响应性。

__CFRunLoopDoObservers函数详解

__CFRunLoopDoObservers 函数逐行注释

函数概述

__CFRunLoopDoObservers 是 RunLoop 中负责触发观察者回调的核心函数。当 RunLoop 的状态发生变化时(如即将进入循环、即将处理 Timer、即将处理 Source 等),这个函数会被调用来通知所有注册的观察者。

函数签名

/* rl is locked, rlm is locked on entrance and exit */
static void __CFRunLoopDoObservers(CFRunLoopRef, CFRunLoopModeRef, CFRunLoopActivity) __attribute__((noinline));

注释说明

  • 锁状态约定: 函数入口和出口时,rl(RunLoop)和 rlm(RunLoopMode)都必须处于加锁状态
  • noinline 属性: 防止编译器内联优化此函数,可能是为了:
    • 便于调试和性能分析
    • 保持调用栈的可读性
    • 控制代码大小

参数说明

  • CFRunLoopRef rl: 当前运行的 RunLoop
  • CFRunLoopModeRef rlm: 当前的 RunLoop Mode
  • CFRunLoopActivity activity: 当前 RunLoop 的活动状态(枚举值)

RunLoop Activity 状态枚举

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry         = (1UL << 0),  // 即将进入 RunLoop
    kCFRunLoopBeforeTimers  = (1UL << 1),  // 即将处理 Timer
    kCFRunLoopBeforeSources = (1UL << 2),  // 即将处理 Source
    kCFRunLoopBeforeWaiting = (1UL << 5),  // 即将进入休眠
    kCFRunLoopAfterWaiting  = (1UL << 6),  // 刚从休眠中唤醒
    kCFRunLoopExit          = (1UL << 7),  // 即将退出 RunLoop
    kCFRunLoopAllActivities = 0x0FFFFFFFU   // 所有活动状态
};

完整代码逐行注释

/* DOES CALLOUT */
// 【重要标注】此函数会执行外部回调(callout),可能导致:
// 1. 长时间阻塞(回调函数耗时)
// 2. 重入问题(回调中可能再次操作 RunLoop)
// 3. 死锁风险(因此需要在回调前解锁)
static void __CFRunLoopDoObservers(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopActivity activity) {
    
    // ==================== 第一部分:性能追踪和初始化 ====================
    
    
    // 📊 记录性能追踪点:开始执行 observers
    // 参数:事件类型、RunLoop、Mode、活动状态、额外参数
    // 可用 Instruments 的 kdebug 工具查看
    cf_trace(KDEBUG_EVENT_CFRL_IS_DOING_OBSERVERS | DBG_FUNC_START, rl, rlm, activity, 0);
    
    // 🔒 检查进程是否被 fork
    // 如果在 fork 后的子进程中,需要重新初始化 RunLoop 的锁和状态
    // 防止继承父进程的锁状态导致死锁
    CHECK_FOR_FORK();

    // ==================== 第二部分:检查观察者数量 ====================
    
    // 获取当前 Mode 中观察者的数量
    // 三目运算符防止 _observers 为 NULL 时崩溃
    CFIndex cnt = rlm->_observers ? CFArrayGetCount(rlm->_observers) : 0;
    
    // 快速退出:如果没有观察者,直接返回
    // 注意:此时仍持有锁,但不需要手动解锁(调用者会处理)
    if (cnt < 1) return;
    

    // ==================== 第三部分:分配观察者收集数组 ====================
    
    /* Fire the observers */
    // 📦 声明栈上缓冲区,避免小数组的堆分配开销
    // - 如果观察者数量 ≤ 1024:在栈上分配 cnt 个元素的数组
    // - 如果观察者数量 > 1024:在栈上分配 1 个元素(占位符)
    // 栈分配速度快,但空间有限(通常几 MB)
    STACK_BUFFER_DECL(CFRunLoopObserverRef, buffer, (cnt <= 1024) ? cnt : 1);
    
    // 🎯 确定最终使用的数组指针
    // - 小数组(≤1024):使用栈缓冲区(快速,无需释放)
    // - 大数组(>1024):堆分配(慢,但可容纳更多元素)
    // 1024 是经验阈值,平衡性能和栈空间使用
    CFRunLoopObserverRef *collectedObservers = (cnt <= 1024) ? buffer : (CFRunLoopObserverRef *)malloc(cnt * sizeof(CFRunLoopObserverRef));
    
    // 实际收集到的观察者计数器(可能小于 cnt)
    CFIndex obs_cnt = 0;
    
    // ==================== 第四部分:收集需要触发的观察者 ====================
    
    // 遍历 Mode 中的所有观察者
    for (CFIndex idx = 0; idx < cnt; idx++) {
        
        // 获取第 idx 个观察者对象
        CFRunLoopObserverRef rlo = (CFRunLoopObserverRef)CFArrayGetValueAtIndex(rlm->_observers, idx);
        
        
        // 🔍 三重过滤条件(必须全部满足):
            
        // 条件1: 0 != (rlo->_activities & activity)
        // 按位与检查观察者是否关注当前活动状态
        // 例如:activity = kCFRunLoopBeforeTimers (0b10)
        // _activities = kCFRunLoopBeforeTimers | kCFRunLoopExit (0b10000010)
        // 按位与结果 = 0b10 != 0,条件成立

        // 条件2: __CFIsValid(rlo)
        // 检查观察者是否有效(未被 invalidate)
        // 观察者可能在之前的回调中被标记为无效

        // 条件3: !__CFRunLoopObserverIsFiring(rlo)
        // 检查观察者是否正在执行回调
        // 防止重入:如果观察者的回调函数中再次触发相同的 activity,
        // 不会重复调用该观察者(避免无限递归)
        if (0 != (rlo->_activities & activity) && __CFIsValid(rlo) && !__CFRunLoopObserverIsFiring(rlo)) {
            
            // ✅ 满足条件的观察者:
            // 1. 添加到收集数组
            // 2. 增加引用计数(CFRetain)
            //    - 防止在后续解锁期间被其他线程释放
            //    - 保证回调执行时对象仍然有效
            // 3. obs_cnt 递增
            collectedObservers[obs_cnt++] = (CFRunLoopObserverRef)CFRetain(rlo);
        }
    }
    
    // ==================== 第五部分:解锁(准备执行回调)====================
    
    // 🔓 解锁 RunLoop Mode
    __CFRunLoopModeUnlock(rlm);
    
    // 🔓 解锁 RunLoop
    __CFRunLoopUnlock(rl);
    
    // ⚠️ 为什么要解锁?
    // 1. 避免死锁:观察者回调可能会调用 RunLoop API(如添加 Timer、Source)
    // 2. 提高并发:允许其他线程在回调执行期间访问 RunLoop
    // 3. 防止长时间持锁:回调可能耗时很长(如网络请求、UI 更新)
    
    // 🛡️ 安全性保证:
    // - 已经通过 CFRetain 增加了观察者的引用计数
    // - collectedObservers 数组是当前线程的局部变量
    // - 即使其他线程修改了 rlm->_observers,也不会影响本次执行
    
    // ==================== 第六部分:执行观察者回调 ====================
    
    // 遍历收集到的观察者(注意:不是遍历 rlm->_observers)
    for (CFIndex idx = 0; idx < obs_cnt; idx++) {
        
        // 获取当前观察者
        CFRunLoopObserverRef rlo = collectedObservers[idx];
        
        // 🔒 锁定观察者对象(细粒度锁)
        // 只锁定单个观察者,不影响其他观察者的并发执行
        __CFRunLoopObserverLock(rlo);
        
        // 再次检查观察者是否有效
        // ⚠️ 为什么要再次检查?
        // 在解锁期间,其他线程可能已经调用了 CFRunLoopObserverInvalidate
        if (__CFIsValid(rlo)) {
            
            // 检查观察者是否是一次性的(non-repeating)
            // 如果是一次性的,执行完回调后需要 invalidate
            Boolean doInvalidate = !__CFRunLoopObserverRepeats(rlo);
            
            // 🚩 设置"正在执行"标志位
            // 防止重入(与前面的 !__CFRunLoopObserverIsFiring 配合)
            __CFRunLoopObserverSetFiring(rlo);
            
            // 🔓 在执行回调前解锁观察者
            // 原因同解锁 RunLoop:防止回调中访问观察者对象时死锁
            __CFRunLoopObserverUnlock(rlo);
            
            // ---------- 提取回调信息 ----------
            
            // 提取回调函数指针
            // 类型:void (*)(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
            CFRunLoopObserverCallBack callout = rlo->_callout;
            
            // 提取用户上下文信息(创建观察者时传入的)
            void *info = rlo->_context.info;
            
            // ---------- 自动释放池 ----------
            
            // 🔄 开始自动释放池(ARP = AutoRelease Pool)
            // 在 ObjC 运行时环境中,等价于 @autoreleasepool {
            // 用于自动管理回调中创建的临时对象
            CFRUNLOOP_ARP_BEGIN(rl)
            
            // ---------- 性能追踪 ----------
            
            // 📊 记录性能追踪点:开始调用观察者回调
            cf_trace(KDEBUG_EVENT_CFRL_IS_CALLING_OBSERVER | DBG_FUNC_START, callout, rlo, activity, info);
            
            
            // ---------- 🎯 核心回调执行 ----------
            
            // ⚡ 执行观察者的回调函数
            // 宏定义通常是:callout(rlo, activity, info);
            // 这是整个函数的核心目的!
            
            // ⏰ 回调函数的参数:
            // - rlo: 观察者对象本身
            // - activity: 当前 RunLoop 活动状态(如 kCFRunLoopBeforeTimers)
            // - info: 用户自定义的上下文信息
            
            // ⚠️ 回调中可能发生的事情:
            // - UI 更新(如 CA::Transaction::observer_callback)
            // - 性能监控(如 FPS 检测)
            // - 内存管理(如清理缓存)
            // - 业务逻辑(如状态同步)
            // - 再次操作 RunLoop(如添加/移除 Timer)
            __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(callout, rlo, activity, info);
            
            
            // ---------- 性能追踪结束 ----------
            
            // 📊 记录性能追踪点:结束调用观察者回调
            // 可通过 end - start 计算回调耗时
            cf_trace(KDEBUG_EVENT_CFRL_IS_CALLING_OBSERVER | DBG_FUNC_END, callout, rlo, activity, info);
            
            // ---------- 自动释放池结束 ----------
            
            // 🔄 结束自动释放池
            // 释放回调中创建的所有 autorelease 对象
            CFRUNLOOP_ARP_END()
           
            
            // ---------- 处理一次性观察者 ----------
            
            // 如果是一次性观察者(non-repeating)
            if (doInvalidate) {
                // ❌ 使观察者失效
                // 会从 RunLoop 中移除,并标记为无效
                // 后续不会再触发此观察者
                CFRunLoopObserverInvalidate(rlo);
            }
            
            // 🚩 清除"正在执行"标志位
            // 允许此观察者在下次 activity 时再次被触发
            __CFRunLoopObserverUnsetFiring(rlo);
            
        } else {
            // 观察者在解锁期间已被其他线程 invalidate
            
            // 🔓 解锁观察者(前面已加锁)
            // 跳过回调执行
            __CFRunLoopObserverUnlock(rlo);
        }
        
        // 📉 释放引用计数(对应前面的 CFRetain)
        // 如果引用计数归零,观察者对象会被销毁
        CFRelease(rlo);
    }
    
    // ==================== 第七部分:重新加锁 ====================
    
    // 🔒 重新锁定 RunLoop
    __CFRunLoopLock(rl);
    
    // 🔒 重新锁定 RunLoop Mode
    __CFRunLoopModeLock(rlm);
    
    // ✅ 恢复函数入口时的锁状态
    // 满足函数签名中的约定:"rl is locked, rlm is locked on entrance and exit"

    // ==================== 第八部分:清理资源 ====================
    
    // 🗑️ 释放堆内存(如果使用了 malloc)
    // - 如果 collectedObservers 指向栈缓冲区(buffer),无需释放
    // - 如果 collectedObservers 指向堆内存(malloc),需要手动释放
    // 防止内存泄漏
    if (collectedObservers != buffer) free(collectedObservers);
    
    // ==================== 第九部分:结束性能追踪 ====================
    
    // 📊 记录性能追踪点:完成 observers 执行
    cf_trace(KDEBUG_EVENT_CFRL_IS_DOING_OBSERVERS | DBG_FUNC_END, rl, rlm, activity, 0);
}

常见使用场景

1. UI 渲染(Core Animation)

// CA 在 kCFRunLoopBeforeWaiting 时提交渲染事务
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(
    kCFAllocatorDefault, 
    kCFRunLoopBeforeWaiting | kCFRunLoopExit,  // 监听这两个状态
    YES,  // 重复触发
    2000000,  // order = 2000000(在大多数 observer 之后)
    &CA_Transaction_observerCallback,  // 回调函数
    NULL
);

2. 性能监控(FPS 检测)

// 监控主线程 RunLoop 的卡顿
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(
    kCFAllocatorDefault,
    kCFRunLoopAllActivities,  // 监听所有活动
    YES,
    0,
    &performanceMonitorCallback,
    NULL
);

void performanceMonitorCallback(CFRunLoopObserverRef observer, 
                                CFRunLoopActivity activity, 
                                void *info) {
    // 记录时间戳,计算两次回调之间的间隔
    // 如果间隔过长,说明发生了卡顿
}

3. 内存管理(自动释放池)

// NSRunLoop 在每次循环前后创建/销毁自动释放池
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(
    kCFAllocatorDefault,
    kCFRunLoopEntry | kCFRunLoopBeforeWaiting | kCFRunLoopExit,
    YES,
    -2147483647,  // 极高优先级(负值)
    &autoreleasePoolCallback,
    NULL
);

总结

这个函数是理解 RunLoop 机制和 iOS 事件循环的关键,也是许多高级特性(如 UI 渲染、性能监控)的基础。

TN3187:迁移至基于 UIKit 场景的生命周期

概述

许多较旧的 iOS 应用使用一个 UIApplicationDelegate 对象作为其应用的主要入口点,并管理应用的生命周期。使用场景的应用则不同,它们使用一个 UISceneDelegate 对象来分别管理每个场景窗口。从基于 UIApplicationDelegate 的应用生命周期迁移到基于场景的生命周期,可以让你的应用支持多窗口等现代功能。本文档阐述了如何将应用迁移到基于场景的生命周期。

迁移路径

将应用迁移到基于场景的生命周期有两种路径:

  • 分阶段迁移:应用在继续使用 UIApplicationDelegate 的同时,逐步采用 UISceneDelegate。
  • 直接迁移:应用一次性直接迁移到 UISceneDelegate。

在分阶段迁移中,你可以在应用的不同部分逐步实现场景支持,而应用的其余部分继续通过现有的 UIApplicationDelegate 进行管理。这种方式对于大型应用或者需要逐步验证场景兼容性的团队很有用。然而,分阶段迁移会增加临时复杂性,并且应用在完全迁移之前无法使用多窗口等需要完全基于场景生命周期的功能。

直接迁移意味着一次性将整个应用切换到 UISceneDelegate。对于尚未使用 UIApplicationDelegate 管理界面的新应用,或者那些规模较小、易于整体更新的现有应用,推荐采用这种方式。直接迁移可以更快地启用多窗口等现代功能,并简化代码库。

如何判断应用是否使用了场景?

如果你的应用使用了场景,其 Info.plist 文件中会包含一个 UIApplicationSceneManifest 字典。当系统在 Info.plist 中检测到此字典时,它会使用基于场景的生命周期来启动你的应用。对于直接迁移,你需要添加此清单。对于分阶段迁移,你将在准备好启用场景时添加它。


分阶段迁移

分阶段迁移允许你逐步采用 UISceneDelegate。在这种方法下,应用将继续使用 UIApplicationDelegate 作为主要入口点,但你可以为某些界面逐步引入场景支持。当你想为应用的特定部分启用多窗口等功能,同时保持其他部分的原有行为时,这种方式很有用。

要进行分阶段迁移,你需要:

  1. 创建一个实现 UIWindowSceneDelegate 协议的新类。
  2. 在 UIApplicationDelegate 中实现 application(_:configurationForConnecting:options:) 方法,为特定会话返回一个 UISceneConfiguration 实例。此配置告诉系统为连接的场景会话使用哪个场景代理类。
  3. 在应用的 Info.plist 中配置 UIApplicationSceneManifest 字典,并为场景配置指定你的场景代理类名。

当系统请求新场景时,它会调用 UIApplicationDelegate 的 application(_:configurationForConnecting:options:) 方法。你可以检查连接选项(例如 UIApplication.OpenURLOptions 或 UIApplication.ActivityOptions)来决定返回哪种场景配置。这允许你根据用户的操作(例如点击 URL 或进行拖放操作)来创建不同类型的场景。

分阶段迁移期间,UIApplicationDelegate 仍然负责管理应用级事件(例如应用启动和进入后台),而 UISceneDelegate 则管理特定场景的生命周期事件(例如场景激活或失活)。这种分离使得你可以逐步将界面管理从 UIApplicationDelegate 转移到 UISceneDelegate。

直接迁移

直接迁移涉及一次性将整个应用从 UIApplicationDelegate 迁移到 UISceneDelegate。这种方式适用于新应用,或者那些愿意为启用多窗口等现代功能而进行全面更新的现有应用。

要直接迁移,你需要:

  1. 将应用生命周期管理从 UIApplicationDelegate 移动到 UISceneDelegate。这包括将代码从 application(:didFinishLaunchingWithOptions:) 移动到 scene(:willConnectTo:options:),以及将其他生命周期方法(例如 applicationWillResignActive 和 applicationDidBecomeActive)迁移到对应的场景代理方法(例如 sceneWillResignActive 和 sceneDidBecomeActive)。
  2. 移除 UIApplicationDelegate 中与窗口管理相关的代码,因为每个场景现在都会管理自己的 UIWindow。
  3. 在 Info.plist 中添加 UIApplicationSceneManifest 字典,并配置默认的场景配置,指定你的 UISceneDelegate 类。

直接迁移后,应用的每个窗口都由一个独立的 UIScene 实例管理,UISceneDelegate 负责该场景的生命周期。这为每个窗口提供了更好的隔离,并启用了多窗口支持。


通用迁移步骤

无论选择分阶段迁移还是直接迁移,都需要遵循一些通用步骤:

1. 创建场景代理类

创建一个实现 UIWindowSceneDelegate 协议的新类。这个类将管理特定场景的生命周期。你可以在其中创建窗口、设置根视图控制器,并响应场景生命周期事件。

2. 配置 Info.plist

在 Info.plist 中添加一个 UIApplicationSceneManifest 字典。此字典告诉系统你的应用支持场景。它包含一个 UISceneConfigurations 字典,你可以在其中定义应用支持的不同场景配置。每个配置指定了场景代理类名和故事板名称(如果使用的话)。

<key>UIApplicationSceneManifest</key> <dict> <key>UIApplicationSupportsMultipleScenes</key> <true/> <key>UISceneConfigurations</key> <dict> <key>UIWindowSceneSessionRoleApplication</key> <array> <dict> <key>UISceneConfigurationName</key> <string>Default Configuration</string> <key>UISceneDelegateClassName</key> <string>$(PRODUCT_MODULE_NAME).SceneDelegate</string> <key>UISceneStoryboardFile</key> <string>Main</string> </dict> </array> </dict> </dict>

3. 更新应用代理

对于直接迁移,移除 UIApplicationDelegate 中与窗口管理相关的代码,并将应用生命周期事件的处理移动到场景代理中。对于分阶段迁移,在 UIApplicationDelegate 中实现 application(_:configurationForConnecting:options:) 方法来返回适当的场景配置。

4. 更新生命周期事件处理

将应用级生命周期事件的处理迁移到相应的场景级事件。例如:

  • application(:didFinishLaunchingWithOptions:) 迁移到 scene(:willConnectTo:options:)
  • applicationWillResignActive 迁移到 sceneWillResignActive
  • applicationDidBecomeActive 迁移到 sceneDidBecomeActive
  • 等等...

5. 测试

在支持场景的设备(例如 iPad)上彻底测试你的应用。验证场景是否正确创建、生命周期事件是否正常触发,以及多窗口等功能是否按预期工作。对于分阶段迁移,确保现有功能在启用场景的部分和未启用的部分都能正常工作。

场景与后台任务

当使用基于场景的生命周期时,后台任务的处理方式有所不同。在基于 UIApplicationDelegate 的应用中,后台任务通常在应用级别管理。而在基于场景的应用中,后台任务可以与特定场景关联。

如果你的应用使用 UIApplication.beginBackgroundTask(withName:expirationHandler:) 来管理长时间运行的任务,在迁移到场景后,你可能需要考虑使用每个场景的后台任务管理。然而,UIApplication 级别的后台任务 API 在基于场景的应用中仍然可用,并且可以在应用级别的任务中使用。

对于直接与特定场景关联的任务(例如,该场景正在进行的网络请求),考虑使用与该场景关联的后台任务。这有助于系统更有效地管理资源,并在场景关闭时提供更清晰的任务清理机制。


结论

迁移到基于 UIKit 场景的生命周期可以使你的应用支持多窗口等现代 iOS 功能。无论选择分阶段迁移还是直接迁移,关键步骤都是创建一个 UISceneDelegate 类,配置 Info.plist,并将生命周期事件处理从 UIApplicationDelegate 移动到 UISceneDelegate。迁移后,每个窗口将由独立的场景管理,从而提高模块化并为用户提供更强大的多任务处理体验。

【AI Video Generator】迎来开年第一波大清洗!

背景

看似一个平平无奇的周末,却让做AI Video Generator的开发者天塌了。

好消息:竞品家的都嘎了!

坏消息:自己家的也嘎了!

此次以Video关键词检索,共计21款相关产品。有单纯上架海外市场,也有全体地区分发

企业微信20260126-095856.png

所以集中下架的行为,不只是单纯的某些国家或地区。大概率是集中触发了苹果的查杀。(法国佬口音:我要验牌!)

u=2455538048,33299884&fm=224&app=112&f=JPEG.jpg

随机验牌

在众多被标记了下架的产品中,随机抽选了2家APP。单纯从应用市场的截图入手。

企业微信20260126-095138.png

企业微信20260126-094803.png

从AppStore市场图,就能明显发现存在社交风格的市场截图,充斥着袒胸露乳的行为。基本上都带勾!

基本上随机抽查的产品中或多或少都存在此类问题。从应用截图就充斥着擦边行为!,莫非是社交类大佬集体转型?

下架原因

为了更好的解释这种集中下架行为,特意在Developer审核指南,匹配对应内容审核的条款。

不出意外 1.1.4 - 公然宣传黄色或色情内容的材料 (这一概念的定义是:“对性器官或性活动的露骨描述或展示,目的在于刺激性快感,而非带来美学价值或触发情感”),其中包括一夜情约会 App 和其他可能包含色情内容或用于嫖娼或人口贩卖和剥削的 App。

当然,这种集中行为大概率苹果算法升级【或者通过鉴黄系统】,从AppStore净化入手,简单纯粹的一刀切!

遵守规则,方得长治久安,最后祝大家大吉大利,今晚过审!

附:更多文章欢迎关注同名公众号,By-iOS研究院

❌