普通视图

发现新文章,点击刷新页面。
今天 — 2026年1月28日iOS

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

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

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

核心数据结构:dispatch_queue_s

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

详细结构解析

1. 多态设计:dispatch_object

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

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

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

2. 任务队列:双向链表

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

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

3. 队列层次结构

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

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

4. 性能优化结构

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

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

不同队列类型的内部差异

1. 全局队列(Global Queue)

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

2. 主队列(Main Queue)

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

3. 并发队列 vs 串行队列

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

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

内存布局示例

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

队列创建过程

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

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

任务执行流程数据结构

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

调试信息

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

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

性能优化特性

1. 内联缓存

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

2. 优先级继承

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

总结

DispatchQueue 的底层数据结构:

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

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

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

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

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

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

核心数据结构关系

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

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

1. 入队过程(Enqueue)

异步任务入队(dispatch_async)

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

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

详细入队步骤

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

屏障任务特殊处理

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

2. 出队过程(Dequeue)

工作线程取任务

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

详细出队实现

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

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

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

3. 任务执行流程

任务执行函数

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

屏障任务的特殊执行

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

4. 性能优化机制

任务批处理

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

工作窃取(Work Stealing)

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

5. 同步任务特殊处理

dispatch_sync 的实现

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

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

6. 优先级处理

QoS 传播和提升

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

完整流程示例

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

7. 内存管理优化

任务缓存池

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

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

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

总结

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

入队过程

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

出队过程

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

关键优化

  1. 无锁设计:使用原子操作避免锁竞争
  2. 缓存复用:重用任务结构体减少内存分配
  3. 批量处理:减少上下文切换开销
  4. 优先级继承:高优先级任务能更快执行
  5. 工作窃取:提高CPU利用率

简单比喻

  • 入队 像"快递站收件":你提交包裹(任务),快递站记录到清单(队列),通知快递员(线程)来取
  • 出队 像"快递员派件":快递员从清单取包裹,派送(执行),完成后标记完成
  • GCD 像"智能调度系统":动态分配快递员,优化路线,处理急件优先

这种设计使得 GCD 能够以极低的延迟处理数百万级的任务调度,同时保持高吞吐量和低CPU占用。

1-2.【GCD】GCD 是如何管理线程池的?线程是谁创建的?

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

线程创建者:GCD 自己

GCD 是线程的实际创建者,但不是随意创建,而是通过高度优化的机制:

// 开发者层面:不直接创建线程
DispatchQueue.global().async {
    // GCD 内部决定:
    // 1. 从池中取出空闲线程
    // 2. 或创建新线程(如果必要)
    // 3. 或将任务排队等待
}

GCD 线程池的智能管理

1. 按需创建,延迟销毁

// GCD 线程生命周期:
// 初始状态:线程池为空
// 第一次提交任务 → 创建1个线程
// 持续提交任务 → 逐渐增加线程数
// 空闲一段时间 → 自动销毁多余线程(节省资源)

2. 线程复用策略

// 类似数据库连接池的模式
class GCDThreadPool {
    private var activeThreads: Set<Thread> = []
    private var idleThreads: Set<Thread> = []
    private var maxThreads: Int
    
    func getThread() -> Thread {
        if let thread = idleThreads.popFirst() {
            // 复用空闲线程
            activeThreads.insert(thread)
            return thread
        } else if activeThreads.count < maxThreads {
            // 创建新线程
            let thread = createThread()
            activeThreads.insert(thread)
            return thread
        } else {
            // 等待线程可用
            return waitForAvailableThread()
        }
    }
}

线程池的关键参数和策略

1. 线程数量限制

// 基于系统资源动态调整
class GCDThreadManager {
    // 主要考虑因素:
    // 1. CPU 核心数(决定最大并发度)
    let maxConcurrentThreads = ProcessInfo.processInfo.processorCount * 2
    
    // 2. 队列类型
    // 串行队列:通常1个线程
    // 并发队列:多个线程,但有限制
    
    // 3. 系统负载
    // 高负载时:减少线程数
    // 低负载时:增加线程数(更快响应)
}

2. QoS(服务质量)影响

// 不同优先级的任务使用不同线程池
DispatchQueue.global(qos: .userInteractive) // 最高优先级,更快获取线程
DispatchQueue.global(qos: .background)      // 最低优先级,可能等待更久

// 内部实现简化:
class QoSThreadPool {
    var highPriorityPool: ThreadPool  // .userInteractive, .userInitiated
    var defaultPool: ThreadPool        // .default
    var lowPriorityPool: ThreadPool    // .utility, .background
    
    func getThread(for qos: QoSClass) -> Thread {
        // 优先从对应优先级的池中获取
        // 高优先级可"借用"低优先级的线程(反之不行)
    }
}

具体工作机制示例

场景:处理大量任务

let queue = DispatchQueue.global()

// 模拟100个任务
for i in 1...100 {
    queue.async {
        sleep(1)  // 模拟1秒工作
        print("任务 (i) 完成,线程: (Thread.current)")
    }
}

// GCD 内部行为:
// 1. 前几个任务:创建新线程(比如4个,基于CPU核心数)
// 2. 继续提交:复用现有线程
// 3. 如果所有线程都忙:排队等待
// 4. 如果长时间排队:可能创建更多线程(但不超过上限)
// 5. 任务完成后:线程空闲,可用于新任务
// 6. 长时间空闲:销毁多余线程

避免的问题和优化

1. 防止线程爆炸

// 传统方式的问题
for _ in 0..<1000 {
    Thread { /* 工作 */ }.start()  // 可能创建1000个线程!
}

// GCD 的优化
let queue = DispatchQueue.global()
for _ in 0..<1000 {
    queue.async { /* 工作 */ }  // 可能只用8-16个线程处理
}

2. 负载均衡

// GCD 内部的工作窃取(Work Stealing)机制
class WorkStealingScheduler {
    var threadPools: [ThreadPool]
    
    func scheduleTask(_ task: Task) {
        // 1. 先尝试分配到最空闲的线程
        // 2. 如果所有线程都忙,但其他队列有闲置线程
        // 3. "窃取"闲置线程来执行任务
    }
}

3. 能耗优化(特别在移动设备)

// iOS/macOS 的能耗管理
class EnergyAwareThreadManager {
    func manageThreads() {
        // 考虑因素:
        // - CPU 频率调整(降频时减少线程)
        // - 电池状态(低电量时减少并发)
        // - 温度控制(过热时限制线程)
        // - 后台状态(后台模式用更少线程)
    }
}

线程生命周期管理

// 简化版 GCD 线程管理逻辑
class GCDThread {
    var state: ThreadState = .idle
    var lastUsed: TimeInterval = 0
    
    enum ThreadState {
        case idle      // 空闲,等待任务
        case active    // 正在执行任务
        case sleeping  // 休眠(可能被销毁)
    }
    
    func run() {
        while true {
            if let task = getTaskFromQueue() {
                state = .active
                task.execute()
                state = .idle
                lastUsed = currentTime()
            } else {
                // 空闲处理
                if shouldDestroyThread() {
                    cleanupAndExit()
                } else {
                    sleepForInterval()
                }
            }
        }
    }
}

特殊情况处理

1. 主线程的特殊性

// 主队列绑定到主线程
DispatchQueue.main.async {
    // 总是运行在主线程
    // GCD 不会为 main queue 创建新线程
    // 而是将任务提交到主线程的 RunLoop
}

// 主线程不是由 GCD 创建的
// 它是应用启动时由系统创建的

2. 同步执行的优化

// sync 调用时的线程优化
queue.sync {
    // 可能的优化:
    // 1. 如果当前线程就是目标队列的执行线程
    // 2. 直接在当前线程执行(避免线程切换)
    // 3. 这称为"队列归属检测"
}

实际调试观察

// 查看线程创建情况
let concurrentQueue = DispatchQueue(label: "test", attributes: .concurrent)

print("初始线程数: (Thread.getAll().count)")

for i in 0..<20 {
    concurrentQueue.async {
        print("任务 (i): (Thread.current)")
        usleep(100_000)  // 100ms
    }
}

// 观察结果:
// 1. 线程数不会无限增长
// 2. 线程会被复用(看到相同的 thread number)
// 3. 最终稳定在一定数量(如 CPU 核心数*2)

总结

GCD 线程管理的关键特点:

  1. 线程创建者:GCD 自己(通过系统调用),开发者不直接创建
  2. 按需创建:首次需要时创建,不是预先创建大量线程
  3. 智能复用:线程执行完任务后回到池中,等待下次使用
  4. 动态调整:根据系统负载、CPU核心数、任务类型动态调整线程数
  5. 上限控制:有最大线程数限制,防止线程爆炸
  6. 自动清理:空闲线程会被销毁,节省资源
  7. 优先级感知:不同 QoS 的任务可能使用不同的线程子池

这种设计使得 GCD 能够:

  • 最大化 CPU 利用率
  • 最小化线程创建/销毁开销
  • 自动适应不同硬件和负载
  • 为开发者隐藏复杂的线程管理细节

简单比喻:GCD 就像一个"智能线程管家",你只需要告诉它"要做什么工作"和"工作的重要程度",它会自动安排最合适的"工人"(线程)来完成,同时保证不雇佣过多工人浪费资源。

👉 追问:GCD 线程池和 pthread / NSThread 的关系?

GCD 线程池、pthread 和 NSThread 之间有清晰的层次关系,pthread 是底层基础,GCD 和 NSThread 都是基于它的高层抽象

层级关系

// 从高到低的抽象层次:
┌─────────────────────────┐
│   GCD (DispatchQueue)   │ ← 最高抽象:任务队列,自动线程管理
├─────────────────────────┤
│        NSThread         │ ← 中级抽象:面向对象的线程封装
├─────────────────────────┤
│       pthread_t         │ ← 底层抽象:POSIX线程,C语言API
└─────────────────────────┘

详细关系解析

1. pthread_t:最底层的基础

// 这是所有线程的根基(包括GCD创建的线程)
#include <pthread.h>

pthread_t thread;
pthread_create(&thread, NULL, worker_func, NULL);  // 创建线程

// GCD 内部最终会调用这个函数来创建线程
// 实际上,macOS/iOS 中的所有线程都是 pthread

2. NSThread:Objective-C 的封装

// NSThread 是对 pthread 的面向对象包装
class NSThread {
    // 内部持有 pthread_t
    private var _pthread: pthread_t
    
    // 创建线程时,内部调用 pthread_create()
    init(block: @escaping () -> Void) {
        _pthread = pthread_create(...)
    }
}

// 验证关系:
Thread.current // 返回当前线程的 NSThread 对象
pthread_self() // 返回当前线程的 pthread_t

// 实际上,Thread.current.pthread 可以获取底层 pthread_t
// (虽然这个属性不公开)

3. GCD 线程池的实现

// GCD 内部结构示意(简化版)
class GCDThreadPool {
    private var threads: [pthread_t] = []
    private var taskQueue: Queue<Task>
    
    func createThreadIfNeeded() {
        // 需要新线程时,创建 pthread
        var thread: pthread_t
        pthread_create(&thread, nil, { context in
            // 线程函数:不断从队列取任务执行
            while let task = taskQueue.dequeue() {
                task.execute()
            }
            return nil
        }, nil)
        
        threads.append(thread)
    }
    
    func execute(_ task: Task) {
        taskQueue.enqueue(task)
        // 如果没有空闲线程且未达上限,创建新线程
        if idleThreads.isEmpty && threads.count < maxThreads {
            createThreadIfNeeded()
        }
    }
}

实际运行时关系示例

场景:观察三者关系

// 创建一个 GCD 并发队列
let queue = DispatchQueue(label: "test", attributes: .concurrent)

// 提交任务
queue.async {
    // 获取三个层面的线程信息
    let nsThread = Thread.current        // NSThread 对象
    let pthreadId = pthread_self()       // pthread_t 标识
    let threadNumber = nsThread.value(forKeyPath: "private.seqNum")  // 内部编号
    
    print("""
    层级关系:
    1. NSThread: (nsThread)
    2. pthread_t: (pthreadId)
    3. 是否GCD创建: (nsThread.name?.contains("com.apple.root") == true)
    """)
    
    // 实际输出可能类似:
    // 1. NSThread: <NSThread: 0x600003d6c040>{number = 7, name = (null)}
    // 2. pthread_t: 0x70000a1000
    // 3. 是否GCD创建: true
}

核心区别对比

特性 GCD 线程池 NSThread pthread_t
抽象级别 最高(队列) 中(对象) 最低(句柄)
创建方式 自动管理 手动创建 手动创建
线程复用 ✅ 自动复用 ❌ 一对一 ❌ 一对一
内存管理 自动 ARC 管理 手动(pthread_join/exit)
跨平台 Apple 生态 Apple 生态 POSIX标准

具体实现细节

1. GCD 如何创建线程

// GCD 内部源码简化示意(libdispatch)
void _dispatch_worker_thread(void *context) {
    // 1. 注册为GCD工作线程
    _dispatch_thread_setspecific(dispatch_queue_key, context);
    
    // 2. 设置线程名字(便于调试)
    pthread_setname_np("com.apple.root.default-qos");
    
    // 3. 进入工作循环
    while (1) {
        // 从队列获取任务
        task = _dispatch_queue_get_task(queue);
        
        if (task) {
            _dispatch_worker_execute(task);
        } else {
            // 空闲处理
            if (should_terminate()) {
                pthread_exit(NULL);
            }
        }
    }
}

// 创建线程的函数
void _dispatch_thread_create(pthread_t *thread, dispatch_queue_t queue) {
    pthread_attr_t attr;
    pthread_attr_init(&attr);
    
    // 配置线程属性
    pthread_attr_set_qos_class_np(&attr, QOS_CLASS_DEFAULT, 0);
    
    // 最终调用 pthread_create
    pthread_create(thread, &attr, _dispatch_worker_thread, (void *)queue);
}

2. NSThread 的 pthread 包装

// NSThread 内部实现示意
@implementation NSThread {
    pthread_t _pthread;
    NSMutableDictionary *_threadDictionary;
}

- (void)start {
    if (_pthread != NULL) return;
    
    // 创建 pthread
    pthread_attr_t attr;
    pthread_attr_init(&attr);
    
    // 存储 self 以便在 C 函数中访问
    NSThread *threadSelf = self;
    
    int err = pthread_create(&_pthread, &attr,
                           _NSThread__start__, (__bridge void *)threadSelf);
    
    pthread_attr_destroy(&attr);
}

static void *_NSThread__start__(void *arg) {
    // 获取 NSThread 对象
    NSThread *thread = (__bridge NSThread *)arg;
    
    // 设置线程特定数据
    pthread_setspecific(NSThreadKey, (__bridge void *)thread);
    
    // 执行目标方法
    [thread main];
    
    return NULL;
}
@end

实际开发中的交互

1. 从 NSThread 获取 pthread

// 方法1:直接获取当前 pthread
let currentPthread = pthread_self()

// 方法2:从 NSThread(不推荐,使用私有API)
extension Thread {
    var pthread: pthread_t? {
        // 注意:这是私有API,App Store审核可能不通过
        let key = "pthread"  // 实际是私有字段
        return self.value(forKey: key) as? pthread_t
    }
}

2. 在 GCD 任务中操作线程

DispatchQueue.global().async {
    // 当前运行在 GCD 管理的 pthread 上
    
    // 可以设置线程名字(用于调试)
    pthread_setname_np("MyWorkerThread")
    
    // 可以获取并操作当前 pthread
    let pthread = pthread_self()
    
    // 可以设置线程优先级(但通常用 QoS 更好)
    var policy: Int32 = 0
    var param = sched_param()
    pthread_getschedparam(pthread, &policy, &param)
    param.sched_priority = 45  // 设置优先级
    pthread_setschedparam(pthread, policy, &param)
    
    // 注意:直接操作 pthread 可能干扰 GCD 的管理
}

3. 混合使用的陷阱

// ❌ 危险的混合使用
let thread = Thread {
    // 在线程中使用 GCD 同步到主队列
    DispatchQueue.main.sync {
        // 如果这个线程恰好是主线程 → 死锁!
        // 因为 Thread 创建的不是主线程,
        // 但理论上如果是主线程调用就会死锁
    }
}

// ✅ 安全的做法
let queue = DispatchQueue.global()
queue.async {
    // 在 GCD 管理的线程中使用 GCD
    DispatchQueue.main.async {
        // 总是安全的
    }
}

性能与开销对比

// 性能测试示例
func measureOverhead() {
    // 1. 直接创建 pthread(最快,但最底层)
    measure("pthread_create") {
        var thread: pthread_t
        pthread_create(&thread, nil, { _ in return nil }, nil)
        pthread_join(thread, nil)
    }
    
    // 2. 创建 NSThread(中等开销)
    measure("NSThread") {
        let thread = Thread { /* do nothing */ }
        thread.start()
        // 需要同步机制确保线程结束
    }
    
    // 3. GCD 提交任务(最低开销,因为复用线程)
    measure("GCD async") {
        let semaphore = DispatchSemaphore(value: 0)
        DispatchQueue.global().async {
            semaphore.signal()
        }
        semaphore.wait()
    }
}

// 典型结果(相对时间):
// pthread_create: 100ms(基准)
// NSThread: 120-150ms(额外对象开销)
// GCD async: 10-20ms(线程复用,极低开销)

调试技巧

1. 查看线程信息

// 在调试器中查看所有线程
(lldb) thread list
// 可以看到:
// * thread #1: tid = 0x1a2b3, 0x0000000100000000, 
//   queue = 'com.apple.main-thread'
// * thread #4: tid = 0x1a2b4, 0x0000000100001000,
//   queue = 'com.apple.root.default-qos'

// 查看线程堆栈
(lldb) bt all
// 会显示所有线程的调用栈,包括GCD工作线程

2. 识别线程来源

// 在代码中判断
if Thread.isMainThread {
    print("这是主线程")
}

// 判断是否为GCD线程
let threadName = Thread.current.name ?? ""
if threadName.hasPrefix("com.apple.root") {
    print("这是GCD线程池中的线程")
}

// 获取线程的QoS级别
let qos = Thread.current.qualityOfService
print("线程QoS: (qos)")

总结

关系链

应用层:GCD (DispatchQueue) → 使用并管理 → NSThread/pthread
          ↓
框架层:NSThread → 封装 → pthread_t
          ↓
系统层:pthread_t → 内核线程

核心要点

  1. pthread 是基石:所有线程最终都是 pthread,包括 GCD 创建的线程

  2. NSThread 是包装:提供面向对象的线程操作,底层仍是 pthread

  3. GCD 是管理者:基于 pthread 构建线程池,提供队列抽象

  4. 线程复用是关键:GCD 复用线程,而直接使用 pthread/NSThread 通常是一对一

  5. 抽象层级不同

    • pthread:关心"如何创建线程"
    • NSThread:关心"如何表示线程对象"
    • GCD:关心"如何组织任务执行"

简单比喻

  • pthread 就像"砖块"(基本单元)
  • NSThread 就像"有标签的砖块"(易于识别和管理)
  • GCD 就像"智能建筑系统"(自动调度砖块完成任务)

在实践中,优先使用 GCD,它提供最优的线程管理和性能。只有在需要特殊线程控制(如长时间后台计算、实时操作)时才考虑使用 NSThread 或 pthread。

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

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

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)去最优执行。

昨天 — 2026年1月27日iOS

[转载] 一文读懂 Skills|从概念到实操的完整指南

作者 wyanassert
2026年1月27日 11:05

转载 一文读懂 Skills|从概念到实操的完整指南

原文地址

Agent 正在经历从“聊天机器人”到“得力干将”的进化,而 Skills 正是这场进化的关键催化剂。

你是否曾被 Agent 的“不听话”、“执行乱”和“工具荒”搞得焦头烂额?

本文将带你一文弄懂 Skills ——这个让 Agent 变得可靠、可控、可复用的“高级技能包”。

我们将从 Skills 是什么、如何工作,一路聊到怎样写好一个 Skills,并为你推荐实用的社区资源,带领大家在 TRAE 中实际使用 Skills 落地一个场景。

无论你是开发者还是普通用户,都能在这里找到让你的 Agent “开窍”的秘诀。


你是否也经历过或者正在经历这样的“ Agent 调教”崩溃时刻?

  • 规则失效: 在 Agent.md 里写下千言万语,Agent 却视若无睹,完全“已读不回”。
  • 执行失控: 精心打磨了无数 Prompt,Agent 执行起来依旧像无头苍蝇,混乱无序。
  • 工具迷失: 明明集成了强大的 MCP 工具库,Agent 却两手一摊说“没工具”,让人摸不着头脑。

如果这些场景让你感同身受,别急着放弃。终结这场混乱的答案,可能就是 Skills。

什么是 Skills


“Skills” 这个概念最早由 Anthropic公司提出,作为其大模型 Claude的一种能力扩展机制。简单来说,它允许用户为 Claude 添加自定义的功能和工具。随着这套做法越来越成熟,并被社区广泛接受,Skills 如今已成为大多数 Agent 开发工具和 IDE 都支持的一种标准扩展规范。

一个 Skills 通常以一个文件夹的形式存在,里面主要装着三样东西:一份说明书(SKILL.md)、一堆操作脚本(Script)、以及一些参考资料(Reference)。

你可以把一个 Skill 想象成一个打包好的“技能包”。它把完成某个特定任务所需的领域知识、操作流程、要用到的工具、以及最佳实践全都封装在了一起。当 AI 面对相应请求时,就能像一位经验丰富的专家那样,有条不紊地自主执行。

一句话总结: 要是把 Agent 比作一个有很大潜力的大脑,那 Skills 就像是给这个大脑的一套套能反复用的“高级武功秘籍”。 有了它,Agent 能从一个“什么都略知一二”的通才,变成在特定领域“什么都擅长”的专家。

Skill 原理介绍


📚 官方解释:Agent Skills

Skill 的架构原理:渐进式加载

Skill 的设计很巧妙,它运行在一个沙盒环境里,这个环境允许大模型访问文件系统和执行 bash 命令(可以理解为一种电脑操作指令)。在这个环境里,一个个 Skill 就像一个个文件夹。Agent 就像一个熟悉电脑操作的人,通过命令行来读取文件、执行脚本,然后利用结果去完成你交代的任务。这种“按需取用”的架构,让 Skill 成为一个既强大又高效的“工具箱”。

为了平衡效果和效率,Skill 设计了一套聪明的三层分级加载机制:

Level 1:元数据(始终加载)

元数据就像是 Skill 的“名片”,里面有名称(name)和描述(description),是用 YAML 格式来定义的。Claude 在启动的时候,会把所有已经安装的 Skill 的元数据都加载进来,这样它就能知道每个 Skill 有什么用、什么时候该用。因为元数据很轻量,所以你可以安装很多 Skill,不用担心把上下文占满。

Level 2:说明文档(触发时加载)

SKILL.md 文件的正文就是说明文档,里面有工作流程、最佳实践和操作指南。只有用户的请求和 Skills 元数据里的描述相符时,Claude 才会用 bash 指令读取这份文档,把内容加载到上下文里。这种“触发式加载”能保证只有相关的详细指令才会消耗 Token。

Level 3:资源与代码(按需加载)

Skills 还能打包一些更深入的资源,比如更详细的说明文档(FORMS.md)、可执行脚本(.py)或者参考资料(像 API 文档、数据库结构等)。Claude 只有在需要的时候,才会通过 bash 去读取或执行这些文件,而且脚本代码本身不会进入上下文。这样一来,Skills 就能捆绑大量信息,几乎不会增加额外的上下文成本。

Skills 的调用逻辑:从理解意图到稳定执行

那么,Agent 是如何智能地选择并执行一个 Skill 的呢?整个过程就像一位经验丰富的助理在处理工作:

  1. 意图匹配(找到对的人): Agent 首先聆听你的需求,然后快速扫一眼自己手头所有 Skill 的“名片夹”(元数据),寻找最匹配的那一张。
  2. 读取手册(看懂怎么干): 找到合适的 Skills 后,Agent 会像模像样地翻开它的“操作手册”(SKILL.md),仔细研究详细的执行步骤和注意事项。
  3. 按需执行(动手开干): 根据手册的指引,Agent 开始工作。如果需要,它会随时从“工具箱”里拿出脚本或工具来完成具体操作。
  4. 反馈结果(事毕复命): 任务完成后,Agent 向你汇报最终结果,或者在遇到困难时,及时向你请教。

Skills vs. 其他概念的区别


为了更清晰地理解 Skills 的独特价值,我们不妨把它和另外两个容易混淆的概念——快捷指令(Command)原子工具(MCP)——放在一起做个对比。用一个厨房的例子就很好懂了:

我们也列举了几个大家容易混淆的其他功能,一起来对比看看。

📚 官方博客解释:Skills explained: How Skills compares to prompts, Projects, MCP, and subagents

什么是好的 Skills:从“能用”到“好用”


Good Skills vs Bad Skills

如何写好 Skills

  1. 原子性(Atomicity): 坚持单一职责,让每个 Skill 都像一块积木,小而美,专注于解决一个具体问题,便于日后的复用和组合。
  2. 给例子(Few-Shot Prompting):这是最关键的一点,与其费尽口舌解释,不如直接给出几个清晰的输入输出示例。榜样的力量是无穷的,模型能通过具体例子,秒懂你想要的格式、风格和行为。
  3. 立规矩(Structured Instructions):
    1. 定角色:给它一个明确的专家人设,比如“你现在是一个资深的市场分析师”。
    2. 拆步骤:把任务流程拆解成一步步的具体指令,引导它“思考”。
    3. 画红线:明确告诉它“不能做什么”,防止它天马行空地“幻觉”
  4. 造接口(Interface Design):像设计软件 API 一样,明确定义 Skill 的输入参数和输出格式(比如固定输出 JSON 或 Markdown)。这让你的 Skill 可以被其他程序稳定调用和集成。
  5. 勤复盘(Iterative Refinement):把 Skills 当作一个产品来迭代。在实际使用中留心那些不尽如人意的“Bad Case”,然后把它们变成新的规则或反例,补充到你的 Skills 定义里,让它持续进化,越来越聪明、越来越靠谱。

📚 一些官方最佳实践指南:技能创作最佳实践

社区热门 Skills 推荐


刚开始接触 Skills,不知从何下手?不妨从社区沉淀的这些热门 Skills 开始,寻找灵感,或直接在你的工作流中复用它们。

Claude 官方提供的 Skills

📚 官方 Skills 仓库:https://github.com/anthropics/skills

学习 Claude 官方的 Skills 仓库可以帮助我们最快的了解 Skills 的最佳实践,便于我们沉淀出自己的 Skills。

如何快速使用官方 Skills?
大多数官方 Skills 都能直接下载,或者通过 Git 克隆到本地。在 TRAE 等工具里,一般只需把这些 Skills 的文件夹放到指定的 Skills 目录,接着重启或刷新 Agent,它就会自动识别并加载这些新能力。具体操作可参考工具的使用文档。
更多细节可参考下面这部分内容:如何在 TRAE 里快速用起来

Claude 官方提供的 Skills 列表

社区其他最佳实践

如何在 TRAE 里快速使用


理论说再多,不如亲手一试。我们先讲一下如何在 TRAE SOLO 中创建并应用一个 Skill 并以基于飞书文档的 Spec Coding为例讲解一下如何利用 Skills 快速解决一个实际问题。

Skill 创建

方式一:设置中直接创建

TRAE 支持在设置页面可以快速创建一个 Skill

按下快捷键 Cmd +/ Ctrl + 通过快捷键打开设置面板。

在设置面板左侧找到「规则技能」选项

找到技能板块,点击右侧的「创建」按钮。

你会看到一个简洁的创建界面,包含三要素:Skill 名称、Skill 描述、Skill 主体。我们以创建一个“按规范提交 git commit”的 Skill 为例,填入相应内容后点击「确认」即可。

填入我们需要的内容「确认」即可

方式二:直接解析 SKILL.md

在当前项目目录下,新增目录.trae/Skills/xxx 导入你需要文件夹,和 TRAE 进行对话,即可使用。

可以在「设置 - 规则技能」中看到已经成功导入

方式三:在对话中创建

目前 TRAE 中内置了 Skills-creator Skills ,你可以在对话中直接和 TRAE 要求创建需要的 Skills

Skill 使用

在 TRAE 里使用技能很容易,你加载好需要的技能后,只需在对话框中用日常语言说明你的需求就行。

  • 例如,输入“帮我设计一个有科技感的登录页面”,系统就会自动调用“frontend-design”技能。
  • 例如,输入“帮我提取这个 PDF 里的所有表格”,系统会自动调用“document-Skills/pdf”技能。
  • 例如,输入“帮我把这片技术文档转为飞书文档”,系统会自动调用“using-feishu-doc”技能。

系统会自动分析你的需求,加载技能文档,还会一步步指导你完成任务!

实践场景举例

还记得引言里提到的那些问题吗?比如说,项目规则文件(project\_rules)有字符数量的限制;又或者,就算你在根规则文件里明确写好了“在什么情况下读取哪个文件”,Agent 在执行任务时也不会按照要求来做。

这些问题的根本原因是,规则(Rules)对于 Agent 而言是固定不变的,它会在任务开始时就把所有规则一次性加载到上下文中,这样既占用空间,又不够灵活。而 技能(Skill)采用的是“逐步加载”的动态方式,刚好可以解决这个问题。所以,我们可以把之前那些复杂的规则场景,重新拆分成一个个独立的技能。

接下来,我们通过一个基于飞书文档的“Spec Coding”简单流程,来实际操作一下如何用技能解决问题。

什么是 Spec Coding?

Spec Coding 提倡“先思考后行动”,也就是通过详细定义可以执行的需求规范(Specification)来推动 AI 开发。它的流程包含“需求分析、技术设计、任务拆解”的文档编写过程,最后让 AI 根据规范来完成编码。这种一步步的工作流程能保证每一步都有依据,实现从需求到代码的准确转化。

让我来分析一下这个场景

上面提到将开发过程划分为四个关键阶段,所以要完成 “需求分析、技术设计、任务拆解” 的飞书文档撰写,还有最终的代码实现。为此,我们需要不同的技能来满足不同场景下的文档编写需求,并且要教会 Agent 如何使用飞书工具进行创作协同。

下面我们就一起完成上面提到的 Skills 的设计实现。

多角色专家 Skills

通过实现多角色 Skills 通过创建多个交付物过程文档,约束后续的编码,为编码提供足够且明确的上下文,每个Skill 专注完成一件事

  • 下面让我们进一步详细设计

​按照上述的表格我们就可以大致明确我们需要的 Skills 该如何实现了。

  • 本次只作为一个例子大家可以参考上面创建 Skill 的教程自己完成一下这个多角色 Skills 的创建和调试,当然正如上面所述好的 Skill 需要在实践中逐渐优化并通过场景调用不断进行优化的

飞书文档使用 Skill

飞书文档的格式是 markdown 的超集,我们 Skill 的目的则是教会 Agent 飞书文档的语法,便于 Agent 写出符合格式的 md 文件。并通过约束 Agent 行为,充分利用飞书文档的评论的读写完成多人协作审阅的过程,用户通过在飞书文档评论完成相关建议的提出,Agent 重新阅读文档和评论,根据建议进一步优化文档,实现文档协作工作流。

Spec Coding Skill

上面我们实现了多个角色 Skills 和一个功能 Skill,但实际使用时,还需要有一个能统筹全局的技能,来实现分工协作。把上述多个技能组合起来,告诉智能体(agent)整体的规格编码(spec coding)流程,完成工具技能和角色技能的组合与调度。

如此我们就能快速搭建一个规格编码工作流程,完成基础开发。当然也可以参考上面的逻辑,用技能来重新复刻社区里的规格编码实践(如 SpecKit、OpenSpec 等)。

总结

上述场景提到了两种不同风格的 Skill(角色型,工具型),利用 Skill 的动态加载机制(取代固定规则的一次性加载方式),完成了复杂场景下的任务分解;通过 不同角色技能的分工协作(避免 Agent 什么都做导致执行混乱);尝试借助飞书文档形成协作闭环(打通人机交互的最后一步),有效解决了 Agent “不听话、执行乱、工具少” 的问题,让 AI 从 “对话助手” 真正转变为 “可信赖的实干家”,实现从需求提出到代码产出的高效、精准、协作式交付。

Q & A | 一些常见问题


为什么我写的 Skills 不生效,或者效果不符合预期?

那十有八九是你的“名片”(Description)没写好。

记住,Agent 是通过读取 Skills 的 Description 来判断“什么时候该用哪个 Skill”的。要是你的描述写得含糊不清、太专业或者太简单,Agent 就很难明白你的意思,自然在需要的时候就不会调用这个 Skill。所以,用大白话写的清晰、准确的Description,对 Skill 能否起作用至关重要。

使用 Skills 的效果,会受到我选择的大语言模型(LLM)的影响吗?

会有影响,不过影响的方面不一样。

  • 一个更强大的模型,主要影响“挑选”和“安排”技能的能力。 它能更准确地明白你的真实想法,然后从一堆技能里挑出最适合的一个或几个来解决问题。它的优势体现在制定策略方面。
  • 而技能本身,决定了具体任务执行的“最低水平”和“稳定性”。 一旦某个技能被选中,它里面设定好的流程和代码是固定的,会稳定地运行。所以,技能编写得好不好,直接决定了具体任务能不能出色完成。。

Skills 是不是万能的?有什么它不擅长做的事情吗?

当然不是万能的。 Skills 的主要优势是 处理那些流程明确、边界清晰的任务。 在下面这些情况中,它可能就不是最好的选择了:

  • 需要高度创造力的任务: 像写一首饱含情感的诗,或者设计一个全新的品牌标志。这类工作更需要大模型本身的“灵感”。
  • 需要实时、动态做决策的复杂策略游戏: 比如在变化极快的金融市场中做交易决策。
  • 单纯的知识问答或开放式闲聊: 如果你只是想问“文艺复兴三杰是谁?”,直接问大模型就可以,不用动用 Skills 这个“大杀器”。

我发现一个社区的 Skills 很好用,但我可以修改它以适应我的特殊需求吗?

当然可以,我们强烈建议你这么做!

大多数共享的 Skill 都支持用户“Fork”(也就是“复制一份”)并进行二次开发。你可以把通用的 Skill 当作模板,在自己的工作空间里复制一份,然后修改里面的逻辑或参数,以适应你自己的业务需求。这对整个生态的共建和知识复用很重要。

结语|让 Agent 成为你真正的“行动派”


Skill 的出现,为 AI 从“对话式助手”转变为“可信赖的执行者”搭建了关键的技术桥梁。它用结构化的方法把领域知识、操作流程和工具调用逻辑封装起来,解决了 Agent 规则失效、执行失控的混乱问题,让 AI 的能力输出变得可以控制、值得信赖且高效。

Skill 的核心价值在于:

  • 精准实际痛点: 通过巧妙的三级加载机制(元数据→说明文档→资源)平衡上下文效率与功能深度,在功能深度和上下文效率之间找到了一个绝佳的平衡点,既避免了宝贵 Token 的浪费,又确保了任务执行的精准性,实现了 Agent 上下文的动态加载能力。
  • 生态赋能,降低门槛: 无论是官方还是社区,都提供了丰富的资源(如 Claude 官方仓库、SkillsMP 市场等),让普通用户也能轻松站在巨人的肩膀上,快速复用各种成熟的能力。

虽然 Skill 不是万能的,但它在“确定性流程任务”上的优势无可替代。未来,随着 AI 模型能力的提升与 Skill 生态的进一步完善,我们有望看到更多跨领域、可组合的 Skill 出现——让 AI 从“样样懂一点”的通才,真正进化为“事事做得好”的专家协作伙伴。

不妨从今天开始,尝试创建你的第一个 Skill:将你最擅长的领域经验封装成可复用的能力,让 AI 成为你延伸专业价值的放大器。

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

作者 东坡肘子
2026年1月27日 08:13

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 视野

昨天以前iOS

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

作者 Fatbobman
2026年1月26日 22:00

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

Swift 常用框架Kingfisher、KingfisherWebP详解

作者 Haha_bj
2026年1月26日 18:00

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 生命周期的执行顺序

2026年1月26日 14:58

在 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函数详解

作者 iOS在入门
2026年1月26日 14:11

借助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)集合

2026年1月26日 13:39

在这里插入图片描述

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 的“数据偷渡”指南

2026年1月26日 13:34

在这里插入图片描述

在 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函数详解

作者 iOS在入门
2026年1月26日 12:41

借助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函数详解

作者 iOS在入门
2026年1月26日 11:27

__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 场景的生命周期

作者 Meicy
2026年1月26日 10:48

概述

许多较旧的 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】迎来开年第一波大清洗!

作者 iOS研究院
2026年1月26日 10:30

背景

看似一个平平无奇的周末,却让做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研究院

Long branches in compilers, assemblers, and linkers

作者 MaskRay
2026年1月25日 16:00

Branch instructions on most architectures use PC-relative addressingwith a limited range. When the target is too far away, the branchbecomes "out of range" and requires special handling.

Consider a large binary where main() at address 0x10000calls foo() at address 0x8010000-over 128MiB away. OnAArch64, the bl instruction can only reach ±128MiB, so thiscall cannot be encoded directly. Without proper handling, the linkerwould fail with an error like "relocation out of range." The toolchainmust handle this transparently to produce correct executables.

This article explores how compilers, assemblers, and linkers worktogether to solve the long branch problem.

  • Compiler (IR to assembly): Handles branches within a function thatexceed the range of conditional branch instructions
  • Assembler (assembly to relocatable file): Handles branches within asection where the distance is known at assembly time
  • Linker: Handles cross-section and cross-object branches discoveredduring final layout

Branch range limitations

Different architectures have different branch range limitations.Here's a quick comparison of unconditional / conditional branchranges:

Architecture Cond Uncond Call Notes
AArch64 ±1MiB ±128MiB ±128MiB Thunks
AArch32 (A32) ±32MiB ±32MiB ±32MiB Thunks, interworking
AArch32 (T32) ±1MiB ±16MiB ±16MiB Thunks, interworking
LoongArch ±128KiB ±128MiB ±128MiB Linker relaxation
M68k (68020+) ±2GiB ±2GiB ±2GiB Assembler picks size
MIPS (pre-R6) ±128KiB ±128KiB (b offset) ±128KiB (bal offset) In -fno-pic code, pseudo-absolutej/jal can be used for a 256MiB region.
MIPS R6 ±128KiB ±128MiB ±128MiB
PowerPC64 ±32KiB ±32MiB ±32MiB Thunks
RISC-V ±4KiB ±1MiB ±1MiB Linker relaxation
SPARC ±1MiB ±8MiB ±2GiB No thunks needed
SuperH ±256B ±4KiB ±4KiB Use register-indirect if needed
x86-64 ±2GiB ±2GiB ±2GiB Large code model changes call sequence
Xtensa ±2KiB ±128KiB ±512KiB Linker relaxation
z/Architecture ±64KiB ±4GiB ±4GiB No thunks needed

The following subsections provide detailed per-architectureinformation, including relocation types relevant for linkerimplementation.

AArch32

In A32 state:

  • Branch (b/b<cond>), conditionalbranch and link (bl<cond>)(R_ARM_JUMP24): ±32MiB
  • Unconditional branch and link (bl/blx,R_ARM_CALL): ±32MiB

Note: R_ARM_CALL is for unconditionalbl/blx which can be relaxed to BLX inline;R_ARM_JUMP24 is for branches which require a veneer forinterworking.

In T32 state (Thumb state pre-ARMv8):

  • Conditional branch (b<cond>,R_ARM_THM_JUMP8): ±256 bytes
  • Short unconditional branch (b,R_ARM_THM_JUMP11): ±2KiB
  • ARMv5T branch and link (bl/blx,R_ARM_THM_CALL): ±4MiB
  • ARMv6T2 wide conditional branch (b<cond>.w,R_ARM_THM_JUMP19): ±1MiB
  • ARMv6T2 wide branch (b.w,R_ARM_THM_JUMP24): ±16MiB
  • ARMv6T2 wide branch and link (bl/blx,R_ARM_THM_CALL): ±16MiB. R_ARM_THM_CALL can berelaxed to BLX.

AArch64

  • Test bit and branch (tbz/tbnz,R_AARCH64_TSTBR14): ±32KiB
  • Compare and branch (cbz/cbnz,R_AARCH64_CONDBR19): ±1MiB
  • Conditional branches (b.<cond>,R_AARCH64_CONDBR19): ±1MiB
  • Unconditional branches (b/bl,R_AARCH64_JUMP26/R_AARCH64_CALL26):±128MiB

The compiler's BranchRelaxation pass handlesout-of-range conditional branches by inverting the condition andinserting an unconditional branch. The AArch64 assembler does notperform branch relaxation; out-of-range branches produce linker errorsif not handled by the compiler.

LoongArch

  • Conditional branches(beq/bne/blt/bge/bltu/bgeu,R_LARCH_B16): ±128KiB (18-bit signed)
  • Compare-to-zero branches (beqz/bnez,R_LARCH_B21): ±4MiB (23-bit signed)
  • Unconditional branch/call (b/bl,R_LARCH_B26): ±128MiB (28-bit signed)
  • Medium range call (pcaddu12i+jirl,R_LARCH_CALL30): ±2GiB
  • Long range call (pcaddu18i+jirl,R_LARCH_CALL36): ±128GiB

M68k

  • Short branch(Bcc.B/BRA.B/BSR.B): ±128 bytes(8-bit displacement)
  • Word branch(Bcc.W/BRA.W/BSR.W): ±32KiB(16-bit displacement)
  • Long branch(Bcc.L/BRA.L/BSR.L, 68020+):±2GiB (32-bit displacement)

GNU Assembler provides pseudoopcodes (jbsr, jra, jXX) that"automatically expand to the shortest instruction capable of reachingthe target". For example, jeq .L0 emits one ofbeq.b, beq.w, and beq.l dependingon the displacement.

With the long forms available on 68020 and later, M68k doesn't needlinker range extension thunks.

MIPS

  • Conditional branches(beq/bne/bgez/bltz/etc,R_MIPS_PC16): ±128KiB
  • PC-relative jump (b offset(bgez $zero, offset)): ±128KiB
  • PC-relative call (bal offset(bgezal $zero, offset)): ±128KiB
  • Pseudo-absolute jump/call (j/jal,R_MIPS_26): branch within the current 256MiB region, onlysuitable for -fno-pic code. Deprecated in R6 in favor ofbc/balc

16-bit instructions removed in Release 6:

  • Conditional branch (beqz16,R_MICROMIPS_PC7_S1): ±128 bytes
  • Unconditional branch (b16,R_MICROMIPS_PC10_S1): ±1KiB

MIPS Release 6:

  • Unconditional branch, compact (bc16, unclear toolchainimplementation): ±1KiB
  • Compare and branch, compact(beqc/bnec/bltc/bgec/etc,R_MIPS_PC16): ±128KiB
  • Compare register to zero and branch, compact(beqzc/bnezc/etc,R_MIPS_PC21_S2): ±4MiB
  • Branch (and link), compact (bc/balc,R_MIPS_PC26_S2): ±128MiB

Compiler long branch handling: Both GCC(mips_output_conditional_branch) and LLVM(MipsBranchExpansion) handle out-of-range conditionalbranches by inverting the condition and inserting an unconditionaljump:

LLVM's MipsBranchExpansion pass handles out-of-rangebranches.

lld implements LA25 thunks for MIPS PIC/non-PIC interoperability, butnot range extension thunks. GNU ld also does not implement rangeextension thunks for MIPS.

GCC's mips port ported added-mlong-calls in 1993-03. In -mno-abicallsmode, GCC's -mlong-calls option (addedin 1993) generates indirect call sequences that can reach anyaddress.

PowerPC

  • Conditional branch (bc/bcl,R_PPC64_REL14): ±32KiB
  • Unconditional branch (b/bl,R_PPC64_REL24/R_PPC64_REL24_NOTOC):±32MiB

GCC-generated code relies on linker thunks. However, the legacy-mlongcall can be used to generate long code sequences.

RISC-V

  • Compressed c.beqz: ±256 bytes
  • Compressed c.jal: ±2KiB
  • jalr (I-type immediate): ±2KiB
  • Conditional branches(beq/bne/blt/bge/bltu/bgeu,B-type immediate): ±4KiB
  • jal (J-type immediate, PseudoBR): ±1MiB(notably smaller than other RISC architectures: AArch64 ±128MiB,PowerPC64 ±32MiB, LoongArch ±128MiB)
  • PseudoJump (using auipc +jalr): ±2GiB
  • beqi/bnei (Zibi extension, 5-bit compareimmediate (1 to 31 and -1)): ±4KiB

Qualcomm uC Branch Immediate extension (Xqcibi):

  • qc.beqi/qc.bnei/qc.blti/qc.bgei/qc.bltui/qc.bgeui(32-bit, 5-bit compare immediate): ±4KiB
  • qc.e.beqi/qc.e.bnei/qc.e.blti/qc.e.bgei/qc.e.bltui/qc.e.bgeui(48-bit, 16-bit compare immediate): ±4KiB

Qualcomm uC Long Branch extension (Xqcilb):

  • qc.e.j/qc.e.jal (48-bit,R_RISCV_VENDOR(QUALCOMM)+R_RISCV_QC_E_CALL_PLT): ±2GiB

For function calls:

  • The Gocompiler emits a single jal for calls and relies on itslinker to generate trampolines when the target is out of range.
  • In contrast, GCC and Clang emit auipc+jalrand rely on linker relaxation to shrink the sequence when possible.

The jal range (±1MiB) is notably smaller than other RISCarchitectures (AArch64 ±128MiB, PowerPC64 ±32MiB, LoongArch ±128MiB).This limits the effectiveness of linker relaxation ("start large andshrink"), and leads to frequent trampolines when the compileroptimistically emits jal ("start small and grow").

SPARC

  • Compare and branch (cxbe, R_SPARC_5): ±64bytes
  • Conditional branch (bcc, R_SPARC_WDISP19):±1MiB
  • Unconditional branch (b, R_SPARC_WDISP22):±8MiB
  • call(R_SPARC_WDISP30/R_SPARC_WPLT30): ±2GiB

With ±2GiB range for call, SPARC doesn't need rangeextension thunks in practice.

SuperH

SuperH uses fixed-width 16-bit instructions, which limits branchranges.

  • Conditional branch (bf/bt): ±256 bytes(8-bit displacement)
  • Unconditional branch (bra): ±4KiB (12-bitdisplacement)
  • Branch to subroutine (bsr): ±4KiB (12-bitdisplacement)

For longer distances, register-indirect branches(braf/bsrf) are used. The compiler invertsconditions and emits these when targets exceed the short ranges.

SuperH is supported by GCC and binutils, but not by LLVM.

Xtensa

Xtensa uses variable-length instructions: 16-bit (narrow,.n suffix) and 24-bit (standard).

  • Narrow conditional branch (beqz.n/bnez.n,16-bit): -28 to +35 bytes (6-bit signed + 4)
  • Conditional branch (compare two registers)(beq/bne/blt/bge/etc,24-bit): ±256 bytes
  • Conditional branch (compare with zero)(beqz/bnez/bltz/bgez,24-bit): ±2KiB
  • Unconditional jump (j, 24-bit): ±128KiB
  • Call(call0/call4/call8/call12,24-bit): ±512KiB

The assembler performs branch relaxation: when a conditional branchtarget is too far, it inverts the condition and inserts a jinstruction.

Per https://www.sourceware.org/binutils/docs/as/Xtensa-Call-Relaxation.html,for calls, GNU Assembler pessimistically generates indirect sequences(l32r+callx8) when the target distance isunknown. GNU ld then performs linker relaxation.

x86-64

  • Short conditional jump (Jcc rel8): -128 to +127bytes
  • Short unconditional jump (JMP rel8): -128 to +127bytes
  • Near conditional jump (Jcc rel32): ±2GiB
  • Near unconditional jump (JMP rel32): ±2GiB

With a ±2GiB range for near jumps, x86-64 rarely encountersout-of-range branches in practice. That said, Google and Meta Platformsdeploy mostly statically linked executables on x86-64 production serversand have run into the huge executable problem for certainconfigurations.

z/Architecture

  • Short conditional branch (BRC,R_390_PC16DBL): ±64KiB (16-bit halfword displacement)
  • Long conditional branch (BRCL,R_390_PC32DBL): ±4GiB (32-bit halfword displacement)
  • Short call (BRAS, R_390_PC16DBL):±64KiB
  • Long call (BRASL, R_390_PC32DBL):±4GiB

With ±4GiB range for long forms, z/Architecture doesn't need linkerrange extension thunks. LLVM's SystemZLongBranch passrelaxes short branches (BRC/BRAS) to longforms (BRCL/BRASL) when targets are out ofrange.

Compiler: branch rangehandling

Conditional branch instructions usually have shorter ranges thanunconditional ones, making them less suitable for linker thunks (as wewill explore later). Compilers typically keep conditional branch targetswithin the same section, allowing the compiler to handle out-of-rangecases via branch relaxation.

Within a function, conditional branches may still go out of range.The compiler measures branch distances and relaxes out-of-range branchesby inverting the condition and inserting an unconditional branch:

1
2
3
4
5
6
7
# Before relaxation (out of range)
beq .Lfar_target # ±4KiB range on RISC-V

# After relaxation
bne .Lskip # Inverted condition, short range
j .Lfar_target # Unconditional jump, ±1MiB range
.Lskip:

Some architectures have conditional branch instructions that comparewith an immediate, with even shorter ranges due to encoding additionalimmediates. For example, AArch64's cbz/cbnz(compare and branch if zero/non-zero) andtbz/tbnz (test bit and branch) have only±32KiB range. RISC-V Zibi beqi/bnei have ±4KiBrange. The compiler handles these in a similar way:

1
2
3
4
5
6
7
// Before relaxation (cbz has ±32KiB range)
cbz w0, far

// After relaxation
cbnz w0, .Lskip // Inverted condition
b far // Unconditional branch, ±128MiB range
.Lskip:

An Intel employee contributed https://reviews.llvm.org/D41634 (in 2017) when inversionof a branch condintion is impossible. This is for an out-of-treebackend. As of Jan 2026 there is no in-tree test for this code path.

In LLVM, this is handled by the BranchRelaxation pass,which runs just before AsmPrinter. Different backends havetheir own implementations:

  • BranchRelaxation: AArch64, AMDGPU, AVR, RISC-V
  • HexagonBranchRelaxation: Hexagon
  • PPCBranchSelector: PowerPC
  • SystemZLongBranch: SystemZ
  • MipsBranchExpansion: MIPS
  • MSP430BSel: MSP430

The generic BranchRelaxation pass computes block sizesand offsets, then iterates until all branches are in range. Forconditional branches, it tries to invert the condition and insert anunconditional branch. For unconditional branches that are still out ofrange, it calls TargetInstrInfo::insertIndirectBranch toemit an indirect jump sequence (e.g.,adrp+add+br on AArch64) or a longjump sequence (e.g., pseudo jump on RISC-V).

Note: The size estimates may be inaccurate due to inline assembly.LLVM uses heuristics to estimate inline assembly sizes, but for certainassembly constructs the size is not precisely known at compile time.

Unconditional branches and calls can target different sections sincethey have larger ranges. If the target is out of reach, the linker caninsert thunks to extend the range.

For x86-64, the large code model uses multiple instructions for callsand jumps to support text sections larger than 2GiB (see Relocationoverflow and code models: x86-64 large code model). This is apessimization if the callee ends up being within reach. Google and MetaPlatforms have interest in allowing range extension thunks as areplacement for the multiple instructions.

Assembler: instructionrelaxation

The assembler converts assembly to machine code. When the target of abranch is within the same section and the distance is known at assemblytime, the assembler can select the appropriate encoding. This isdistinct from linker thunks, which handle cross-section or cross-objectreferences where distances aren't known until link time.

Assembler instruction relaxation handles two cases (see Clang-O0 output: branch displacement and size increase for examples):

  • Span-dependent instructions: Select an appropriateencoding based on displacement.
    • On x86, a short jump (jmp rel8) can be relaxed to anear jump (jmp rel32) when the target is far.
    • On RISC-V, beqz may be assembled to the 2-bytec.beqz when the displacement fits within ±256 bytes.
  • Conditional branch transform: Invert the conditionand insert an unconditional branch. On RISC-V, a blt mightbe relaxed to bge plus an unconditional branch.

The assembler uses an iterative layout algorithm that alternatesbetween fragment offset assignment and relaxation until all fragmentsbecome legalized. See Integratedassembler improvements in LLVM 19 for implementation details.

Linker: range extensionthunks

When the linker resolves relocations, it may discover that a branchtarget is out of range. At this point, the instruction encoding isfixed, so the linker cannot simply change the instruction. Instead, itgenerates range extension thunks (also called veneers,branch stubs, or trampolines).

A thunk is a small piece of linker-generated code that can reach theactual target using a longer sequence of instructions. The originalbranch is redirected to the thunk, which then jumps to the realdestination.

Range extension thunks are one type of linker-generated thunk. Othertypes include:

  • ARM interworking veneers: Switch between ARM andThumb instruction sets (see Linker notes onAArch32)
  • MIPS LA25 thunks: Enable PIC and non-PIC codeinteroperability (see Toolchain notes onMIPS)
  • PowerPC64 TOC/NOTOC thunks: Handle calls betweenfunctions using different TOC pointer conventions (see Linker notes on PowerISA)

Short range vs long rangethunks

A short range thunk (see lld/ELF's AArch64implementation) contains just a single branch instruction. Since ituses a branch, its reach is also limited by the branch range—it can onlyextend coverage by one branch distance. For targets further away,multiple short range thunks can be chained, or a long range thunk withaddress computation must be used.

Long range thunks use indirection and can jump to (practically)arbitrary locations.

1
2
3
4
5
6
7
8
9
// Short range thunk: single branch, 4 bytes
__AArch64AbsLongThunk_dst:
b dst // ±128MiB range

// Long range thunk: address computation, 12 bytes
__AArch64ADRPThunk_dst:
adrp x16, dst // Load page address (±4GiB range)
add x16, x16, :lo12:dst // Add page offset
br x16 // Indirect branch

Thunk examples

AArch32 (PIC) (see Linker notes onAArch32):

1
2
3
4
5
__ARMV7PILongThunk_dst:
movw ip, :lower16:(dst - .) ; ip = intra-procedure-call scratch register
movt ip, :upper16:(dst - .)
add ip, ip, pc
bx ip

PowerPC64 ELFv2 (see Linker notes on PowerISA):

1
2
3
4
5
__long_branch_dst:
addis 12, 2, .branch_lt@ha # Load high bits from branch lookup table
ld 12, .branch_lt@l(12) # Load target address
mtctr 12 # Move to count register
bctr # Branch to count register

Thunk impact ondebugging and profiling

Thunks are transparent at the source level but visible in low-leveltools:

  • Stack traces: May show thunk symbols (e.g.,__AArch64ADRPThunk_foo) between caller and callee
  • Profilers: Samples may attribute time to thunkcode; some profilers aggregate thunk time with the target function
  • Disassembly: objdump orllvm-objdump will show thunk sections interspersed withregular code
  • Code size: Each thunk adds bytes; large binariesmay have thousands of thunks

lld/ELF's thunk creationalgorithm

lld/ELF uses a multi-pass algorithm infinalizeAddressDependentContent:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
assignAddresses();
for (pass = 0; pass < 30; ++pass) {
if (pass == 0)
createInitialThunkSections(); // pre-create empty ThunkSections
bool changed = false;
for (relocation : all_relocations) {
if (pass > 0 && normalizeExistingThunk(rel))
continue; // existing thunk still in range
if (!needsThunk(rel)) continue;
Thunk *t = getOrCreateThunk(rel);
ts = findOrCreateThunkSection(rel, src);
ts->addThunk(t);
rel.sym = t->getThunkTargetSym(); // redirect
changed = true;
}
mergeThunks(); // insert ThunkSections into output
if (!changed) break;
assignAddresses(); // recalculate with new thunks
}

Key details:

  • Multi-pass: Iterates until convergence (max 30passes). Adding thunks changes addresses, potentially puttingpreviously-in-range calls out of range.
  • Pre-allocated ThunkSections: On pass 0,createInitialThunkSections places emptyThunkSections at regular intervals(thunkSectionSpacing). For AArch64: 128 MiB - 0x30000 ≈127.8 MiB.
  • Thunk reuse: getThunk returns existingthunk if one exists for the same target;normalizeExistingThunk checks if a previously-created thunkis still in range.
  • ThunkSection placement: getISDThunkSecfinds a ThunkSection within branch range of the call site, or createsone adjacent to the calling InputSection.

lld/MachO's thunk creationalgorithm

lld/MachO uses a single-pass algorithm inTextOutputSection::finalize:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
for (callIdx = 0; callIdx < inputs.size(); ++callIdx) {
// Finalize sections within forward branch range (minus slop)
while (finalIdx < endIdx && fits_in_range(inputs[finalIdx]))
finalizeOne(inputs[finalIdx++]);

// Process branch relocations in this section
for (Relocation &r : reverse(isec->relocs)) {
if (!isBranchReloc(r)) continue;
if (targetInRange(r)) continue;
if (existingThunkInRange(r)) { reuse it; continue; }
// Create new thunk and finalize it
createThunk(r);
}
}

Key differences from lld/ELF:

  • Single pass: Addresses are assigned monotonicallyand never revisited
  • Slop reservation: ReservesslopScale * thunkSize bytes (default: 256 × 12 = 3072 byteson ARM64) to leave room for future thunks
  • Thunk naming:<function>.thunk.<sequence> where sequenceincrements per target

Thunkstarvation problem: If many consecutive branches need thunks, eachthunk (12 bytes) consumes slop faster than call sites (4 bytes apart)advance. The test lld/test/MachO/arm64-thunk-starvation.sdemonstrates this edge case. Mitigation is increasing--slop-scale, but pathological cases with hundreds ofconsecutive out-of-range callees can still fail.

mold's thunk creationalgorithm

mold uses a two-pass approach:

  • Pessimistically over-allocate thunks. Out-of-section relocations andrelocations referencing to a section not assigned address yetpessimistically need thunks.(requires_thunk(ctx, isec, rel, first_pass) whenfirst_pass=true)
  • Then remove unnecessary ones.

Linker pass ordering:

  • compute_section_sizes() callscreate_range_extension_thunks() — final section addressesare NOT yet known
  • set_osec_offsets() assigns section addresses
  • remove_redundant_thunks() is called AFTER addresses areknown — check unneeded thunks due to out-of-section relocations
  • Rerun set_osec_offsets()

Pass 1 (create_range_extension_thunks):Process sections in batches using a sliding window. The window tracksfour positions:

1
2
3
4
5
6
7
8
9
Sections:   [0] [1] [2] [3] [4] [5] [6] [7] [8] [9] ...
^ ^ ^ ^
A B C D
| |_______| |
| batch |
| |
earliest thunk
reachable placement
from C
  • [B, C) = current batch of sections to process (size≤ branch_distance/5)
  • A = earliest section still reachable from C (forthunk expiration)
  • D = where to place the thunk (furthest pointreachable from B)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// Simplified from OutputSection<E>::create_range_extension_thunks
while (b < sections.size()) {
// Advance D: find furthest point where thunk is reachable from B
while (d < size && thunk_at_d_reachable_from_b)
assign_address(sections[d++]);

// Compute batch [B, C)
c = b + 1;
while (c < d && sections[c] < sections[b] + batch_size) c++;

// Advance A: expire thunks no longer reachable
while (a < b && sections[a] + branch_distance < sections[c]) a++;
// Expire thunk groups before A: clear symbol flags.
for (; t < thunks.size() && thunks[t].offset < sections[a]; t++)
for (sym in thunks[t].symbols) sym->flags = 0;

// Scan [B,C) relocations. If a symbol is not assigned to a thunk group yet,
// assign it to the new thunk group at D.
auto &thunk = thunks.emplace_back(new Thunk(offset));
parallel_for(b, c, [&](i64 i) {
for (rel in sections[i].relocs) {
if (requires_thunk(rel)) {
Symbol &sym = rel.symbol;
if (!sym.flags.test_and_set()) { // atomic: skip if already set
lock_guard lock(mu);
thunk.symbols.push_back(&sym);
}
}
}
});
offset += thunk.size();
b = c; // Move to next batch
}

Pass 2 (remove_redundant_thunks): Afterfinal addresses are known, remove thunk entries for symbols actually inrange.

Key characteristics:

  • Pessimistic over-allocation: Assumes allout-of-section calls need thunks; safe to shrink later
  • Batch size: branch_distance/5 (25.6 MiB forAArch64, 3.2 MiB for AArch32)
  • Parallelism: Uses TBB for parallel relocationscanning within each batch
  • Single branch range: Uses one conservativebranch_distance per architecture. For AArch32, uses ±16 MiB(Thumb limit) for all branches, whereas lld/ELF uses ±32 MiB for A32branches.
  • Thunk size not accounted in D-advancement: Theactual thunk group size is unknown when advancing D, so the end of alarge thunk group may be unreachable from the beginning of thebatch.
  • No convergence loop: Single forward pass foraddress assignment, no risk of non-convergence

GNU ld's thunk creationalgorithm

Each port implements the algorithm on their own. There is no codesharing.

GNU ld's AArch64 port (bfd/elfnn-aarch64.c) uses aniterative algorithm but with a single stub type and no lookup table.

Main iteration loop(elfNN_aarch64_size_stubs()):

1
2
3
4
5
6
7
8
9
10
11
group_sections(htab, stub_group_size, ...);  // Default: 127 MiB
layout_sections_again();

for (;;) {
stub_changed = false;
_bfd_aarch64_add_call_stub_entries(&stub_changed, ...);
if (!stub_changed)
return true;
_bfd_aarch64_resize_stubs(htab);
layout_sections_again();
}

GNU ld's ppc64 port (bfd/elf64-ppc.c) uses an iterativemulti-pass algorithm with a branch lookup table(.branch_lt) for long-range stubs.

Section grouping: Sections are grouped bystub_group_size (~28-30 MiB default); each group gets onestub section. For 14-bit conditional branches(R_PPC64_REL14, ±32KiB range), group size is reduced by1024x.

Main iteration loop(ppc64_elf_size_stubs()):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
while (1) {
// Scan all relocations in all input sections
for (input_bfd; section; irela) {
// Only process branch relocations (R_PPC64_REL24, R_PPC64_REL14, etc.)
stub_type = ppc_type_of_stub(section, irela, ...);
if (stub_type == ppc_stub_none)
continue;
// Create or merge stub entry
stub_entry = ppc_add_stub(...);
}

// Size all stubs, potentially upgrading long_branch to plt_branch
bfd_hash_traverse(&stub_hash_table, ppc_size_one_stub, ...);

// Check for convergence
if (!stub_changed && all_sizes_stable)
break;

// Re-layout sections
layout_sections_again();
}

Convergence control:

  • STUB_SHRINK_ITER = 20 (PR28827): After 20 iterations,stub sections only grow (prevents oscillation)
  • Convergence when:!stub_changed && all section sizes stable

Stub type upgrade: ppc_type_of_stub()initially returns ppc_stub_long_branch for out-of-rangebranches. Later, ppc_size_one_stub() checks if the stub'sbranch can reach; if not, it upgrades toppc_stub_plt_branch and allocates an 8-byte entry in.branch_lt.

Comparing linker thunkalgorithms

Aspect lld/ELF lld/MachO mold GNU ld ppc64
Passes Multi (max 30) Single Two Multi (shrink after 20)
Strategy Iterative refinement Sliding window Sliding window Iterative refinement
Thunk placement Pre-allocated intervals Inline with slop Batch intervals Per stub-group

Linker relaxation

Some architectures take a different approach: instead of onlyexpanding branches, the linker can also shrinkinstruction sequences when the target is close enough. RISC-V andLoongArch both use this technique. See Thedark side of RISC-V linker relaxation for a deeper dive into thecomplexities and tradeoffs.

Consider a function call using the callpseudo-instruction, which expands to auipc +jalr:

1
2
3
4
5
# Before linking (8 bytes)
call ext
# Expands to:
# auipc ra, %pcrel_hi(ext)
# jalr ra, ra, %pcrel_lo(ext)

If ext is within ±1MiB, the linker can relax this to:

1
2
# After relaxation (4 bytes)
jal ext

This is enabled by R_RISCV_RELAX relocations thataccompany R_RISCV_CALL relocations. TheR_RISCV_RELAX relocation signals to the linker that thisinstruction sequence is a candidate for shrinking.

Example object code before linking:

1
2
3
4
5
6
7
8
9
0000000000000006 <foo>:
6: 97 00 00 00 auipc ra, 0
R_RISCV_CALL ext
R_RISCV_RELAX *ABS*
a: e7 80 00 00 jalr ra
e: 97 00 00 00 auipc ra, 0
R_RISCV_CALL ext
R_RISCV_RELAX *ABS*
12: e7 80 00 00 jalr ra

After linking with relaxation enabled, the 8-byteauipc+jalr pairs become 4-bytejal instructions:

1
2
3
4
5
6
0000000000000244 <foo>:
244: 41 11 addi sp, sp, -16
246: 06 e4 sd ra, 8(sp)
248: ef 00 80 01 jal ext
24c: ef 00 40 01 jal ext
250: ef 00 00 01 jal ext

When the linker deletes instructions, it must also adjust:

  • Subsequent instruction offsets within the section
  • Symbol addresses
  • Other relocations that reference affected locations
  • Alignment directives (R_RISCV_ALIGN)

This makes RISC-V linker relaxation more complex than thunkinsertion, but it provides code size benefits that other architecturescannot achieve at link time.

LoongArch uses a similar approach. Apcaddu12i+jirl sequence(R_LARCH_CALL36, ±128GiB range) can be relaxed to a singlebl instruction (R_LARCH_B26, ±128MiB range)when the target is close enough.

Diagnosing out-of-rangeerrors

When you encounter a "relocation out of range" error, check thelinker diagnostic and locate the relocatable file and function.Determine how the function call is lowered in assembly.

Summary

Handling long branches requires coordination across thetoolchain:

Stage Technique Example
Compiler Branch relaxation pass Invert condition + add unconditional jump
Assembler Instruction relaxation Invert condition + add unconditional jump
Linker Range extension thunks Generate trampolines
Linker Linker relaxation Shrink auipc+jalr to jal(RISC-V)

The linker's thunk generation is particularly important for largeprograms where function calls may exceed branch ranges. Differentlinkers use different algorithms with various tradeoffs betweencomplexity, optimality, and robustness.

Linker relaxation approaches adopted by RISC-V and LoongArch is analternative that avoids range extension thunks but introduces othercomplexities.

Related

Handling long branches

作者 MaskRay
2026年1月25日 16:00

Branch instructions on most architectures use PC-relative addressingwith a limited range. When the target is too far away, the branchbecomes "out of range" and requires special handling.

Consider a large binary where main() at address 0x10000calls foo() at address 0x8010000-over 128MiB away. OnAArch64, the bl instruction can only reach ±128MiB, so thiscall cannot be encoded directly. Without proper handling, the linkerwould fail with an error like "relocation out of range." The toolchainmust handle this transparently to produce correct executables.

This article explores how compilers, assemblers, and linkers worktogether to solve the long branch problem.

  • Compiler (IR to assembly): Handles branches within a function thatexceed the range of conditional branch instructions
  • Assembler (assembly to relocatable file): Handles branches within asection where the distance is known at assembly time
  • Linker: Handles cross-section and cross-object branches discoveredduring final layout

Branch range limitations

Different architectures have different branch range limitations.Here's a quick comparison of unconditional branch/call ranges:

Architecture Unconditional Branch Conditional Branch Notes
AArch64 ±128MiB ±1MiB Range extension thunks
AArch32 (A32) ±32MiB ±32MiB Range extension and interworking veneers
AArch32 (T32) ±16MiB ±1MiB Thumb has shorter ranges
PowerPC64 ±32MiB ±32KiB Range extension and TOC/NOTOC interworking thunks
RISC-V ±1MiB (jal) ±4KiB Linker relaxation
x86-64 ±2GiB ±2GiB Code models or thunk extension

The following subsections provide detailed per-architectureinformation, including relocation types relevant for linkerimplementation.

AArch32

In A32 state:

  • Branch (b/b<cond>), conditionalbranch and link (bl<cond>)(R_ARM_JUMP24): ±32MiB
  • Unconditional branch and link (bl/blx,R_ARM_CALL): ±32MiB

Note: R_ARM_CALL is for unconditionalbl/blx which can be relaxed to BLX inline;R_ARM_JUMP24 is for branches which require a veneer forinterworking.

In T32 state:

  • Conditional branch (b<cond>,R_ARM_THM_JUMP8): ±256 bytes
  • Short unconditional branch (b,R_ARM_THM_JUMP11): ±2KiB
  • ARMv5T branch and link (bl/blx,R_ARM_THM_CALL): ±4MiB
  • ARMv6T2 wide conditional branch (b<cond>.w,R_ARM_THM_JUMP19): ±1MiB
  • ARMv6T2 wide branch (b.w,R_ARM_THM_JUMP24): ±16MiB
  • ARMv6T2 wide branch and link (bl/blx,R_ARM_THM_CALL): ±16MiB. R_ARM_THM_CALL can berelaxed to BLX.

AArch64

  • Test and compare branches(tbnz/tbz/cbnz/cbz):±32KiB
  • Conditional branches (b.<cond>): ±1MiB
  • Unconditional branches (b/bl):±128MiB

PowerPC

  • Conditional branch (bc/bcl,R_PPC64_REL14): ±32KiB
  • Unconditional branch (b/bl,R_PPC64_REL24/R_PPC64_REL24_NOTOC):±32MiB

RISC-V

  • Compressed c.beqz: ±256 bytes
  • Compressed c.jal: ±2KiB
  • jalr (I-type immediate): ±2KiB
  • Conditional branches(beq/bne/blt/bge/bltu/bgeu,B-type immediate): ±4KiB
  • jal (J-type immediate, PseudoBR):±1MiB
  • PseudoJump (using auipc +jalr): ±2GiB

Qualcomm uC Branch Immediate extension (Xqcibi):

  • qc.beqi/qc.bnei/qc.blti/qc.bgei/qc.bltui/qc.bgeui(32-bit, 5-bit compare immediate): ±4KiB
  • qc.e.beqi/qc.e.bnei/qc.e.blti/qc.e.bgei/qc.e.bltui/qc.e.bgeui(48-bit, 16-bit compare immediate): ±4KiB

Qualcomm uC Long Branch extension (Xqcilb):

  • qc.e.j/qc.e.jal (48-bit,R_RISCV_VENDOR(QUALCOMM)+R_RISCV_QC_E_CALL_PLT): ±2GiB

SPARC

  • Compare and branch (cxbe, R_SPARC_5): ±64bytes
  • Conditional branches (bcc,R_SPARC_WDISP19): ±1MiB
  • call (R_SPARC_WDISP30): ±2GiB

Note: lld does not implement range extension thunks for SPARC.

x86-64

  • Short conditional jump (Jcc rel8): -128 to +127bytes
  • Short unconditional jump (JMP rel8): -128 to +127bytes
  • Near conditional jump (Jcc rel32): ±2GiB
  • Near unconditional jump (JMP rel32): ±2GiB

With a ±2GiB range for near jumps, x86-64 rarely encountersout-of-range branches in practice. A single text section would need toexceed 2GiB before thunks become necessary. For this reason, mostlinkers (including lld) do not implement range extension thunks forx86-64.

Compiler: branch relaxation

The compiler typically generates branches using a form with a largerange. However, certain conditional branches may still go out of rangewithin a function.

The compiler measures branch distances and relaxes out-of-rangebranches. In LLVM, this is handled by the BranchRelaxationpass, which runs just before AsmPrinter.

Different backends have their own implementations:

  • BranchRelaxation: AArch64, AMDGPU, AVR, RISC-V
  • HexagonBranchRelaxation: Hexagon
  • PPCBranchSelector: PowerPC
  • SystemZLongBranch: SystemZ
  • MipsBranchExpansion: MIPS
  • MSP430BSel: MSP430

For a conditional branch that is out of range, the pass typicallyinverts the condition and inserts an unconditional branch:

1
2
3
4
5
6
7
# Before relaxation (out of range)
beq .Lfar_target # ±4KiB range on RISC-V

# After relaxation
bne .Lskip # Inverted condition, short range
j .Lfar_target # Unconditional jump, ±1MiB range
.Lskip:

Assembler: instructionrelaxation

The assembler converts assembly to machine code. When the target of abranch is within the same section and the distance is known at assemblytime, the assembler can select the appropriate encoding. This isdistinct from linker thunks, which handle cross-section or cross-objectreferences where distances aren't known until link time.

Assembler instruction relaxation handles two cases (see Clang-O0 output: branch displacement and size increase for examples):

  • Span-dependent instructions: Select a largerencoding when the displacement exceeds the range of the smallerencoding. For x86, a short jump (jmp rel8) can be relaxedto a near jump (jmp rel32).
  • Conditional branch transform: Invert the conditionand insert an unconditional branch. On RISC-V, a blt mightbe relaxed to bge plus an unconditional branch.

The assembler uses an iterative layout algorithm that alternatesbetween fragment offset assignment and relaxation until all fragmentsbecome legalized. See Integratedassembler improvements in LLVM 19 for implementation details.

Linker: range extensionthunks

When the linker resolves relocations, it may discover that a branchtarget is out of range. At this point, the instruction encoding isfixed, so the linker cannot simply change the instruction. Instead, itgenerates range extension thunks (also called veneers,branch stubs, or trampolines).

A thunk is a small piece of linker-generated code that can reach theactual target using a longer sequence of instructions. The originalbranch is redirected to the thunk, which then jumps to the realdestination.

Range extension thunks are one type of linker-generated thunk. Othertypes include:

  • ARM interworking veneers: Switch between ARM andThumb instruction sets (see Linker notes onAArch32)
  • MIPS LA25 thunks: Enable PIC and non-PIC codeinteroperability (see Toolchain notes onMIPS)
  • PowerPC64 TOC/NOTOC thunks: Handle calls betweenfunctions using different TOC pointer conventions (see Linker notes on PowerISA)

Short range vs long rangethunks

A short range thunk (see lld/ELF's AArch64implementation) contains just a single branch instruction. Since ituses a branch, its reach is also limited by the branch range—it can onlyextend coverage by one branch distance. For targets further away,multiple short range thunks can be chained, or a long range thunk withaddress computation must be used.

Long range thunks use indirection and can jump to (practically)arbitrary locations.

1
2
3
4
5
6
7
8
9
// Short range thunk: single branch, 4 bytes
__AArch64AbsLongThunk_dst:
b dst // ±128MiB range

// Long range thunk: address computation, 12 bytes
__AArch64ADRPThunk_dst:
adrp x16, dst // Load page address (±4GiB range)
add x16, x16, :lo12:dst // Add page offset
br x16 // Indirect branch

Thunk examples

AArch32 (PIC) (see Linker notes onAArch32):

1
2
3
4
5
__ARMV7PILongThunk_dst:
movw ip, :lower16:(dst - .) ; ip = intra-procedure-call scratch register
movt ip, :upper16:(dst - .)
add ip, ip, pc
bx ip

PowerPC64 ELFv2 (see Linker notes on PowerISA):

1
2
3
4
5
__long_branch_dst:
addis 12, 2, .branch_lt@ha # Load high bits from branch lookup table
ld 12, .branch_lt@l(12) # Load target address
mtctr 12 # Move to count register
bctr # Branch to count register

Thunk impact ondebugging and profiling

Thunks are transparent at the source level but visible in low-leveltools:

  • Stack traces: May show thunk symbols (e.g.,__AArch64ADRPThunk_foo) between caller and callee
  • Profilers: Samples may attribute time to thunkcode; some profilers aggregate thunk time with the target function
  • Disassembly: objdump orllvm-objdump will show thunk sections interspersed withregular code
  • Code size: Each thunk adds bytes; large binariesmay have thousands of thunks

lld/ELF's thunk creationalgorithm

lld/ELF uses a multi-pass algorithm infinalizeAddressDependentContent:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
assignAddresses();
for (pass = 0; pass < 30; ++pass) {
if (pass == 0)
createInitialThunkSections(); // pre-create empty ThunkSections
bool changed = false;
for (relocation : all_relocations) {
if (pass > 0 && normalizeExistingThunk(rel))
continue; // existing thunk still in range
if (!needsThunk(rel)) continue;
Thunk *t = getOrCreateThunk(rel);
ts = findOrCreateThunkSection(rel, src);
ts->addThunk(t);
rel.sym = t->getThunkTargetSym(); // redirect
changed = true;
}
mergeThunks(); // insert ThunkSections into output
if (!changed) break;
assignAddresses(); // recalculate with new thunks
}

Key details:

  • Multi-pass: Iterates until convergence (max 30passes). Adding thunks changes addresses, potentially puttingpreviously-in-range calls out of range.
  • Pre-allocated ThunkSections: On pass 0,createInitialThunkSections places emptyThunkSections at regular intervals(thunkSectionSpacing). For AArch64: 128 MiB - 0x30000 ≈127.8 MiB.
  • Thunk reuse: getThunk returns existingthunk if one exists for the same target;normalizeExistingThunk checks if a previously-created thunkis still in range.
  • ThunkSection placement: getISDThunkSecfinds a ThunkSection within branch range of the call site, or createsone adjacent to the calling InputSection.

lld/MachO's thunk creationalgorithm

lld/MachO uses a single-pass algorithm inTextOutputSection::finalize:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
for (callIdx = 0; callIdx < inputs.size(); ++callIdx) {
// Finalize sections within forward branch range (minus slop)
while (finalIdx < endIdx && fits_in_range(inputs[finalIdx]))
finalizeOne(inputs[finalIdx++]);

// Process branch relocations in this section
for (Relocation &r : reverse(isec->relocs)) {
if (!isBranchReloc(r)) continue;
if (targetInRange(r)) continue;
if (existingThunkInRange(r)) { reuse it; continue; }
// Create new thunk and finalize it
createThunk(r);
}
}

Key differences from lld/ELF:

  • Single pass: Addresses are assigned monotonicallyand never revisited
  • Slop reservation: ReservesslopScale * thunkSize bytes (default: 256 × 12 = 3072 byteson ARM64) to leave room for future thunks
  • Thunk naming:<function>.thunk.<sequence> where sequenceincrements per target

Thunkstarvation problem: If many consecutive branches need thunks, eachthunk (12 bytes) consumes slop faster than call sites (4 bytes apart)advance. The test lld/test/MachO/arm64-thunk-starvation.sdemonstrates this edge case. Mitigation is increasing--slop-scale, but pathological cases with hundreds ofconsecutive out-of-range callees can still fail.

mold's thunk creationalgorithm

mold uses a two-pass approach: first pessimistically over-allocatethunks, then remove unnecessary ones.

Intuition: It's safe to allocate thunk space andlater shrink it, but unsafe to add thunks after addresses are assigned(would create gaps breaking existing references).

Pass 1 (create_range_extension_thunks):Process sections in batches using a sliding window. The window tracksfour positions:

1
2
3
4
5
6
7
8
9
Sections:   [0] [1] [2] [3] [4] [5] [6] [7] [8] [9] ...
^ ^ ^ ^
A B C D
| |_______| |
| batch |
| |
earliest thunk
reachable placement
from C
  • [B, C) = current batch of sections to process (size≤ branch_distance/5)
  • A = earliest section still reachable from C (forthunk expiration)
  • D = where to place the thunk (furthest pointreachable from B)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// Simplified from OutputSection<E>::create_range_extension_thunks
while (b < sections.size()) {
// Advance D: find furthest point where thunk is reachable from B
while (d < size && thunk_at_d_reachable_from_b)
assign_address(sections[d++]);

// Compute batch [B, C)
c = b + 1;
while (c < d && sections[c] < sections[b] + batch_size) c++;

// Advance A: expire thunks no longer reachable
while (a < b && sections[a] + branch_distance < sections[c]) a++;
// Expire thunk groups before A: clear symbol flags.
for (; t < thunks.size() && thunks[t].offset < sections[a]; t++)
for (sym in thunks[t].symbols) sym->flags = 0;

// Scan [B,C) relocations. If a symbol is not assigned to a thunk group yet,
// assign it to the new thunk group at D.
auto &thunk = thunks.emplace_back(new Thunk(offset));
parallel_for(b, c, [&](i64 i) {
for (rel in sections[i].relocs) {
if (requires_thunk(rel)) {
Symbol &sym = rel.symbol;
if (!sym.flags.test_and_set()) { // atomic: skip if already set
lock_guard lock(mu);
thunk.symbols.push_back(&sym);
}
}
}
});
offset += thunk.size();
b = c; // Move to next batch
}

Pass 2 (remove_redundant_thunks): Afterfinal addresses are known, remove thunk entries for symbols actually inrange.

Key characteristics:

  • Pessimistic over-allocation: Assumes allout-of-section calls need thunks; safe to shrink later
  • Batch size: branch_distance/5 (25.6 MiB forAArch64, 3.2 MiB for AArch32)
  • Parallelism: Uses TBB for parallel relocationscanning within each batch
  • Single branch range: Uses one conservativebranch_distance per architecture. For AArch32, uses ±16 MiB(Thumb limit) for all branches, whereas lld/ELF uses ±32 MiB for A32branches.
  • Thunk size not accounted in D-advancement: Theactual thunk group size is unknown when advancing D, so the end of alarge thunk group may be unreachable from the beginning of thebatch.
  • No convergence loop: Single forward pass foraddress assignment, no risk of non-convergence

Comparing thunk algorithms

Aspect lld/ELF lld/MachO mold
Passes Multi-pass (max 30) Single-pass Two-pass
Strategy Iterative refinement Greedy Greedy
Thunk placement Pre-allocated at intervals Inline with slop reservation Batch-based at intervals
Convergence Always (bounded iterations) Almost always Almost always
Range handling Per-relocation type Single conservative range Single conservative range
Parallelism Sequential Sequential Parallel (TBB)

Linker relaxation (RISC-V)

RISC-V takes a different approach: instead of only expandingbranches, it can also shrink instruction sequences whenthe target is close enough.

Consider a function call using the callpseudo-instruction, which expands to auipc +jalr:

1
2
3
4
5
# Before linking (8 bytes)
call ext
# Expands to:
# auipc ra, %pcrel_hi(ext)
# jalr ra, ra, %pcrel_lo(ext)

If ext is within ±1MiB, the linker can relax this to:

1
2
# After relaxation (4 bytes)
jal ext

This is enabled by R_RISCV_RELAX relocations thataccompany R_RISCV_CALL relocations. TheR_RISCV_RELAX relocation signals to the linker that thisinstruction sequence is a candidate for shrinking.

Example object code before linking:

1
2
3
4
5
6
7
8
9
0000000000000006 <foo>:
6: 97 00 00 00 auipc ra, 0
R_RISCV_CALL ext
R_RISCV_RELAX *ABS*
a: e7 80 00 00 jalr ra
e: 97 00 00 00 auipc ra, 0
R_RISCV_CALL ext
R_RISCV_RELAX *ABS*
12: e7 80 00 00 jalr ra

After linking with relaxation enabled, the 8-byteauipc+jalr pairs become 4-bytejal instructions:

1
2
3
4
5
6
0000000000000244 <foo>:
244: 41 11 addi sp, sp, -16
246: 06 e4 sd ra, 8(sp)
248: ef 00 80 01 jal ext
24c: ef 00 40 01 jal ext
250: ef 00 00 01 jal ext

When the linker deletes instructions, it must also adjust:

  • Subsequent instruction offsets within the section
  • Symbol addresses
  • Other relocations that reference affected locations
  • Alignment directives (R_RISCV_ALIGN)

This makes RISC-V linker relaxation more complex than thunkinsertion, but it provides code size benefits that other architecturescannot achieve at link time.

Diagnosing out-of-rangeerrors

When you encounter a "relocation out of range" error, here are somediagnostic steps:

  1. Check the error message: lld reports the sourcelocation, relocation type, and the distance. For example:

    1
    ld.lld: error: a.o:(.text+0x1000): relocation R_AARCH64_CALL26 out of range: 150000000 is not in [-134217728, 134217727]

  2. Use --verbose or-Map: Generate a link map to see sectionlayout and identify which sections are far apart.

  3. Consider -ffunction-sections:Splitting functions into separate sections gives the linker moreflexibility in placement, potentially reducing distances.

  4. Check for large data in .text:Embedded data (jump tables, constant pools) can push functions apart.Some compilers have options to place these elsewhere.

  5. LTO considerations: Link-time optimization candramatically change code layout. If thunk-related issues appear onlywith LTO, the optimizer may be creating larger functions or differentinlining decisions.

Summary

Handling long branches requires coordination across thetoolchain:

Stage Technique Example
Compiler Branch relaxation pass Invert condition + add unconditional jump
Assembler Instruction relaxation Short jump to near jump
Linker Range extension thunks Generate trampolines
Linker Linker relaxation Shrink auipc+jalr to jal(RISC-V)

The linker's thunk generation is particularly important for largeprograms where cross-compilation-unit calls may exceed branch ranges.Different linkers use different algorithms with various tradeoffsbetween complexity, optimality, and robustness.

RISC-V's linker relaxation is unique in that it can both expand andshrink code, optimizing for both correctness and code size.

老司机 iOS 周报 #363 | 2026-01-26

作者 ChengzhiHuang
2026年1月25日 20:23

ios-weekly
老司机 iOS 周报,只为你呈现有价值的信息。

你也可以为这个项目出一份力,如果发现有价值的信息、文章、工具等可以到 Issues 里提给我们,我们会尽快处理。记得写上推荐的理由哦。有建议和意见也欢迎到 Issues 提出。

文章

🐕 精通 UITableViewDiffableDataSource ——从入门到重构的现代 iOS 列表开发指南

@阿权:本文围绕 iOS 现代列表 UITableViewDiffableDataSource 展开,核心是替代传统数据源代理模式,解决列表开发中的崩溃、状态不一致等痛点,并在最后提供一个轻量工具集 DiffableDataSourceKit 来简化系统 API 的调用。文章核心内容如下:

  1. 使用 UITableViewDiffableDataSource API,通过声明式的“快照”来管理数据状态,系统自动计算并执行 UI 更新动画,从而一劳永逸地解决了传统模式中数据与 U 状态不同步导致的崩溃问题。
  2. 全文通过构建一个音乐播放列表 App 来贯穿始终,从移除 Storyboard、定义遵循 Hashable 协议的数据模型开始,一步步教你初始化数据源和填充数据。
  3. 文章还详细讲解了:
    • 自定义多样化的单元格:包括使用传统的 Auto Layout 布局,以及利用 iOS 14+ 的 UIContentConfiguration 进行现代化配置。
    • 实现核心交互:具体涉及了拖拽重排、滑动删除,以及如何通过 Cell 的代理事件处理等交互。
    • 处理复杂逻辑:特别讲解了如何利用模型的 Hashable 来实现“原地刷新”而非替换的刷新机制。

除了文章提到的 UITableViewDiffableDataSource,用好这些技术,不妨可以再看看以下几个 WWDC:

  1. WWDC19 - 215 - Advances in Collection View Layout
  2. WWDC19 - 220 - Advances in UI Data Sources
  3. WWDC20 - 10026 - Lists in UICollectionView
  4. WWDC20 - 10027 - Modern cell configuration

另外,与其用 UITableView 承载数据,其实 Apple 更推荐使用 UICollectionView 来实现列表,甚至还提供了增强版的 Cell。

恰逢 App Store 要求用 Xcode 26 带来的强制升级,不少 App 也终于抛弃了 iOS 12、iOS 13,也是用新技术(也不新了)升级项目架构的最好时机。

除了 API 本身,我们也应关注到一些架构或设计模式上的变化与趋势:

  1. 声明式。新的 API 更多使用构造时定义逻辑的声明式语法,在一开始就把相关的布局、样式等逻辑给定义好,后续不再通过各种存储属性配置逻辑。极大地减少了开发者对状态的维护。例如 UICollectionViewCompositionalLayout 通过 Item、可嵌套的 Group、Section 一层一层地在初始化配置布局。
  2. 数据驱动。声明式常常是为了数据驱动,因为声明式定义的不是最终对象,而是一些前置的配置数据结构。例如 Cell 提供了 Configuration 结构体,通过配置和重新赋值结构体来实现 UI 更新,而不是直接操作 View。类似的,UIButton 也提供了类型的 Configuration 结构体用于配置 UI。更深一层的意义,驱动 UI 的配置数据、视图甚至可以复用的和无痛迁移的。例如 UITableViewCellUICollectionViewCell 的配置类及其关联子视图是通用的,自定义 Cell 可以把重心放在自定义 Configuration 配置上,这样就可以把相同的视图样式套用在各种容器中。
  3. 数据绑定。将索引替换为 id 甚至是具体业务类型。以往 UITableViewUICollectionView 的 API 都是围绕索引(IndexPath)展开的,所有的数据(DataSource)、布局(CollectionViewLayout)和视图(Presentation: Cell、ReuseableView)即使有分离,但都需要通过索引来交换。虽然这样简化了不同模块的耦合和通信逻辑,但因为大多数业务场景数据是动态的,这让索引只是个临时态,一不小心就会用错,轻则展示错误,重则引入崩溃。DiffableDataSource 最具里程碑的一点是剔除了索引,直接让具体业务模型跟 Cell 直接绑定,不经过索引。
  4. 类型安全。不再是 Any/AnyObject,而是直接绑定一个具体的类型。常见做法是通过泛型机制将类型传入。
  5. 用轮子,而不是造轮子。系统 API 足够自由,直接使用实例,而不是子类化自定义逻辑。以往的开发经验,都是一言不合就重写实现,重新造控件、布局,UIButtonUICollectionViewLayout 就是两个典型的 case。近年来系统 API 都在丰富使用的自由度和易用程度,例如 UIButton 提供了许多拿来就能用的灵活样式,开发者只需要微调个 Configuration 就是能实现业务效果。UICollectionViewCompositionalLayout 则是用 Item、Group、Section 构造足够复杂的布局场景。另外一点验证了这个趋势的是,iOS 26 中,只有官方提供的控件、导航框架才有完整的液态玻璃交互。

架构的演进一般是为了提高研效、减少出错。一个合理、高效的代码架构,在当业务需求变得复杂的时候,业务调用代码不会随业务的复杂而线性增长,而是逐渐减少。

🐎 Dart 官方再解释为什么放弃了宏编程,并转向优化 build_runner ? 和 Kotlin 的区别又是什么?

@Crazy:本文主要介绍了 Dart 官方放弃宏编程改为优化 build_runner 的原因,在读本文之前,要先明白什么是宏编程。文章中介绍了 Dart 在实现宏编程的过程中试用的方案与思考,放弃的原因总结起来有三个 :

  1. 编译会卡在一个“先有鸡还是先有蛋”的死结
  2. 工具链双前端导致宏支持会引发“工作量爆炸 + 性能灾难”
  3. 即使做成了,也“高不成低不就”:替代不了 build_runner,不如直接扩展 build_runner 能力

文章最后还对比了 Kotlin 的 Compiler Plugins、KSP 与 Swift 的 Swift Macros 的差距,总的来说 build_runner 还有很长的一段路要走。

🐕 @_exported import VS public import

@AidenRao:Swift 6 带来了全新的 import 访问级别控制:@_exported import。它和我们熟悉的 public import 有什么不同?简单来说,public import 只是将一个模块声明为公开 API 的一部分,但使用者仍需手动导入它;而 @_exported import 则是将依赖的符号完全“吸收”,调用方无需关心底层依赖。文章深入对比了两者的意图和应用场景,并给出了明确建议:日常开发中应优先选择官方支持的 public import,仅在封装 SDK 或构建聚合模块(Umbrella Module)这类希望为用户简化导入操作的场景下,才考虑使用 @_exported

🐕 MVVM + Reducer Pattern

@含笑饮砒霜:这篇文章主要讲述如何将 MVVM 架构与 Reducer 模式结合来提升 iOS 应用中状态管理的可控性和可维护性。作者指出:传统的 MVVM 模式在复杂状态下易出现分散的状态变更和难以追踪的问题,这会导致难调试、隐式状态转换、竞态条件等不良后果;而 Reducer 模式(受 Redux/TCA 启发)通过 “单一状态源 + 明确 action + 纯函数 reduce ” 的方式,使状态变更更可预测、更易测试。文章建议在 ViewModel 内部局部引入 reducer,把所有状态通过单一 reduce(state, action) 处理,并把副作用(如异步任务)当作 effects 处理,从而达到更明确、可追踪且易单元测试的效果,同时保留 MVVM 和领域层的清晰分层,不强依赖某个框架。

🐢 用第一性原理拆解 Agentic Coding:从理论到实操

@Cooper Chen:文章从第一性原理出发,系统拆解了 Agentic Coding 背后的底层逻辑与工程现实,澄清了一个常见误区:效率瓶颈不在于上下文窗口不够大,而在于我们如何与 AI 协作。作者以 LLM 的自回归生成与 Attention 机制为起点,深入分析了 Coding Agent 在长任务中常见的“走偏”“失忆”“局部最优”等问题,并指出这些并非工具缺陷,而是模型工作方式的必然结果。

文章最有价值之处,在于将理论约束转化为可执行的工程实践:通过“短对话、单任务”的工作方式控制上下文质量;用结构化配置文件和工具设计引导 Agent 行为;通过 Prompt Caching、Agent Loop、上下文压缩等机制提升系统稳定性。更进一步,作者提出“复利工程(Compounding Engineering)”这一关键理念——不把 AI 当一次性工具,而是通过文档、规范、测试和审查,将每一次经验沉淀为系统的长期记忆。

最终,文章给出的启示非常清晰:AI 编程不是魔法,而是一门需要刻意练习的协作技能。当你真正理解模型的边界,并用工程化方法加以约束和放大,AI 才能从“能写代码”进化为“可靠的编程合作者”。

🐎 Universal Links At Scale: The Challenges Nobody Talks About

@Damien:文章揭示了 Universal Links 在大规模应用中的隐藏复杂性:AASA 文件缺乏 JSON 模式验证导致静默失效,Apple CDN 缓存延迟使问题修复滞后,苹果特有通配符语法和 substitutionVariables 变量无现成工具支持。作者提出通过 CI 集成模式验证、CDN 同步检查、自定义正则解析和 staging 环境测试的完整方案,并开源了 Swift CLI 工具实现全链路自动化验证。

🐕 How I use Codex GPT 5.2 with Xcode (My Complete Workflow)

@JonyFang: 本视频深入介绍了如何让 AI 代理(如 Codex GPT 5.2)真正提升 iOS/macOS 开发效率的三个核心策略:

  1. 构建脚本自动化(Build Scripts):通过标准化的构建流程,让 AI 能够理解和复现你的构建环境
  2. 让构建失败显而易见(Make Build Failures Obvious):优化错误信息的呈现方式,使 AI 能够快速定位问题根源
  3. 给你的代理装上"眼睛"(Give Your Agent Eyes):这是最核心的部分 - 让 AI 能够"看到"应用运行时的状态,而不仅仅是读取代码

最有价值之处:作者强调了一个常被忽视的问题 - AI 代码助手不仅需要理解代码逻辑,更需要理解应用的运行时状态。通过工具如 Peekaboo 等,让 AI 能够获取视觉反馈(截图、UI 层级等),从而提供更精准的问题诊断和代码建议。这种"可观测性优先"的思路,与传统的代码审查工作流形成了有趣的对比,值得所有尝试将 AI 工具深度集成到开发流程中的团队参考。

视频时长约 49 分钟,适合希望系统性提升 AI 辅助开发效率的 iOS/macOS 开发者观看。

工具

🐎 Skip Is Now Free and Open Source

@Crazy:Skip 框架正式免费并且开源,该库从 2023 年开始开发,已有三年的开发历程。该库的目的是让开发者能够仅用一套 Swift 与 SwiftUI 代码库,同时打造 iOS 与 Android 上的高品质移动应用——而且不必接受那些自“跨平台工具诞生以来就一直存在”的妥协。因为 Skip 是采用编译为 Kotlin 与 Compose 的方式,所以相应的执行效率是非常高的。相较于其他的跨平台开发,效率高,并且使用的是 Swift 语言。既然已经免费并开源,移动端开发的时候又多了一个可供选择的跨端技术。

内推

重新开始更新「iOS 靠谱内推专题」,整理了最近明确在招人的岗位,供大家参考

具体信息请移步:https://www.yuque.com/iosalliance/article/bhutav 进行查看(如有招聘需求请联系 iTDriverr)

关注我们

我们是「老司机技术周报」,一个持续追求精品 iOS 内容的技术公众号,欢迎关注。

关注有礼,关注【老司机技术周报】,回复「2024」,领取 2024 及往年内参

同时也支持了 RSS 订阅:https://github.com/SwiftOldDriver/iOS-Weekly/releases.atom

说明

🚧 表示需某工具,🌟 表示编辑推荐

预计阅读时间:🐎 很快就能读完(1 - 10 mins);🐕 中等 (10 - 20 mins);🐢 慢(20+ mins)

ARC 原理与 weak 底层实现(Side Table 深度解析)

作者 汉秋
2026年1月25日 17:22

ARC 原理与 weak 底层实现(Side Table 深度解析)

面向:有一定 iOS / Runtime 基础的开发者

目标:真正搞清楚 weak 到底 weak 了谁、SideTable 里存的是什么、为什么能自动置 nil


一、先给结论(非常重要)

weak 不是修饰对象,而是修饰“指针变量”

SideTable 记录的是:某个对象,被哪些 weak 指针地址指向

换句话说:

  • ❌ 不是「A weak 引用 B」
  • ❌ 不是「对象记住了谁 weak 它」
  • ✅ 是「Runtime 记住:哪些内存地址(weak 指针)指向了这个对象」

二、从一行代码开始

@property (nonatomic, weak) Person *person;

编译后本质是:

Person *__weak _person;

说明三点:

  1. _person 是一个普通指针变量
  2. weak 修饰的是这个指针变量的行为
  3. 并不是 Person 对象“变成了 weak”

三、明确三个核心角色

Person *p = [[Person alloc] init];
self.person = p; // weak

此时内存中存在三样东西:

角色 含义
Person 对象 真正的 OC 实例
strong 指针 拥有对象(如 p)
weak 指针 不拥有对象(如 self->_person)

weak 的对象不是 Person,而是 _person 这个指针变量。


四、objc_storeWeak 到底做了什么?

self.person = p;

编译后:

objc_storeWeak(&self->_person, p);

注意这里传入的两个参数:

  • &self->_person 👉 weak 指针的地址

  • p 👉 对象地址

Runtime 的真实意图:

登记:对象 p,被这个 weak 指针地址弱引用了


五、SideTable / weak_table 的真实逻辑结构

1️⃣ SideTable(简化)

struct SideTable {
    spinlock_t lock;
    RefcountMap refcnts;     // 强引用计数
    weak_table_t weak_table; // 弱引用表
};

2️⃣ weak_table_t

struct weak_table_t {
    weak_entry_t *weak_entries;
};

3️⃣ weak_entry_t(重点)

struct weak_entry_t {
    DisguisedPtr<objc_object> referent; // 被 weak 的对象
    weak_referrer_t *referrers;         // weak 指针地址数组
};

六、SideTable 中真正存的是什么?

用一张逻辑图表示:

SideTable
└── weak_table
    └── weak_entry
        ├── referent = Person 对象
        └── referrers = [
              &self->_person,
              &vc->_delegate,
              &cell->_model
          ]

关键点(一定要记住):

  • weak_table 的 key 是对象
  • value 是 所有指向它的 weak 指针地址

七、谁被 weak?谁被记录?

以:

self.person = p;

为例:

问题 答案
谁被 weak? _person 这个指针变量
谁被引用? Person 对象
SideTable 记录什么? Person → weak 指针地址列表

八、对象释放时为什么能自动置 nil?

当 Person 的引用计数降为 0:

objc_destroyWeak(obj);

Runtime 的逻辑流程:

1. 找到 obj 对应的 weak_entry
2. 遍历 referrers(weak 指针地址)
3. 对每个地址执行:
      *(Person **)referrer = nil
4. 移除 weak_entry

⚠️ Runtime 完全不知道变量名,只操作内存地址。


九、用内存视角完整走一遍

1️⃣ 内存布局

0x1000  Person 对象

0x2000  p (strong)        → 0x1000
0x3000  self->_person     → 0x1000

2️⃣ weak_table

key: 0x1000
value: [0x3000]

3️⃣ Person 释放

free(0x1000)
*(0x3000) = nil

最终:

self.person == nil

十、为什么 weak 不会产生野指针?

修饰符 行为
assign 不 retain、不置 nil → 野指针
weak Runtime 扫表并置 nil

weak 的安全性来自 Runtime 的集中清理机制


十一、为什么 weak_table 以“对象”为中心?

因为:

对象释放是一个确定事件

以对象为 key:

  • 对象销毁 → 一次性清理所有 weak 指针
  • 性能可控
  • 逻辑集中

十二、常见误解澄清

❌ A weak 引用 B

✅ A 的某个指针 weak 指向 B

❌ 对象知道谁 weak 它

✅ Runtime 知道,对象本身不知道

❌ weak 是对象属性

✅ weak 是指针语义

❌ weak 只是“不 retain”

✅ weak = 不 retain + 注册 weak_table + 自动置 nil


十三、一句话总结

weak 的本质不是弱引用对象,

而是 Runtime 记录“哪些指针弱指向了这个对象”,

并在对象销毁时统一把这些指针置为 nil。


十四、下一步可继续深入

  • block 捕获下 weak_table 的变化过程

  • __unsafe_unretained 与 weak 的实现对比

  • objc-runtime 源码中 weak_entry 的真实实现

❌
❌