清单 解释 +load 中使用 dispatch_once 的原因与示例(Objective‑C)。 说明 Swift 运行时是什么、与 ObjC runtime 的区别与联系。 描述 Swift 的 class layout、ABI、引用计数管理的差异。 详细解释 RunLoop 的 Source0 / Source1 区别与实现细节(含实例)。 说明子线程默认 RunLoop 行为,如何在子线程使用 performSelector/Timer,为什么优先用 GCD/Operation。 子线程保活的常见实现方式与推荐做法(run loop、port、GCD)。 CFRunLoop 底层如何依赖内核事件(mach port、kqueue 等)来唤醒线程——原理与唤醒条件。 对第 7 的补充(重复项)做整合说明与调试建议。 开始逐条说明。 在 +load 里务必使用 dispatch_once。是什么?为什么要这样做?(Objective‑C) +load 是类和 category 被加载进 Objective‑C runtime 时自动调用的方法。系统保证每个类/分类的 +load 会被调用一次(加载时机由 loader 决定),并且 +load 在程序启动早期且可能在并发环境中调用(不同类的 +load 可能并发执行)。 dispatch_once 的用途:在多次调用/多次尝试做同一件事时保证只执行一次(线程安全、幂等)。虽然 +load 本身“每个类只调用一次”,但我们通常在 +load 内做 method swizzling 或全局只需一次的注册/替换逻辑,且会涉及父类/子类/category 等多处代码,使用 dispatch_once 能确保: 在并发 +load 执行环境中只有一份替换逻辑会执行(避免重复交换导致双重交换恢复原实现或其它竞态)。 在多处可能会被重复调用(比如通过不同路径初始化或 library 重复加载)时仍然安全。 典型用法(Objective‑C): 推荐放在 +load 里执行 swizzling,但内部使用 dispatch_once 来保证幂等性。 示例:
- (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Class cls = [self class]; SEL orig = @selector(viewWillAppear:); SEL swiz = @selector(my_viewWillAppear:); Method m1 = class_getInstanceMethod(cls, orig); Method m2 = class_getInstanceMethod(cls, swiz); BOOL didAdd = class_addMethod(cls, orig, method_getImplementation(m2), method_getTypeEncoding(m2)); if (didAdd) { class_replaceMethod(cls, swiz, method_getImplementation(m1), method_getTypeEncoding(m1)); } else { method_exchangeImplementations(m1, m2); } }); } 注意事项: +load 的调用顺序复杂(class > category 等),不要依赖调用顺序做重要业务逻辑。 如果只在 runtime 初始化时需要一次操作,也可以考虑使用 dispatch_once 放到 +initialize 或程序启动时的其他位置;但 +initialize 有它自己的延迟/线程语义(当类第一次被消息发送时调用)。 Swift 有自己的运行时,是怎么样的?与 Objective‑C runtime 的区别与联系在哪里? Swift runtime(swift runtime)简介 Swift 有自己的运行时库(libswiftCore 等),负责类型元数据(metadata)、内存分配、引用计数实现(swift_retain/swift_release)、动态镜像(reflection)、协议遵循(witness tables)、异常处理等。 自 Swift 5 起,Swift ABI 已稳定(在苹果平台上),运行时时常发生变化已小幅减少,但仍存在与语言演进相关的改动。 与 Objective‑C runtime 的区别(高层) 语言特性: Objective‑C 是以消息传递为核心(objc_msgSend):方法调用动态分派;类型信息以 runtime 数据结构维护(Class、SEL、IMP)。 Swift 默认使用静态与表驱动的 dispatch(对于 final、static、free functions),以及 vtable(类方法非 final)或 witness table(协议)来进行 dispatch,只有标记为 @objc 或 dynamic 的 Swift 方法才会暴露为 ObjC 的消息机制(objc_msgSend)。 类型系统与元数据: Swift 提供值类型(struct/enum)和引用类型(class),并且对 struct/enum 做了更复杂的内存/ABI 布局(inline 存储、可变大小类型等)。 Swift 的元数据结构(metadata)与 ObjC 的 Class/objc_object 不同,但两者可以互操作(当 Swift 类继承 NSObject 或标注 @objc)。 动态特性: Objective‑C runtime 在运行时能自由添加方法/替换实现并广泛依赖于消息转发机制。 Swift 运行时并不鼓励也不直接提供像 ObjC 那样广泛的方法替换 API(虽然可以通过暴露为 ObjC 来间接使用 runtime)。对纯 Swift 方法做 method swizzling 非常危险且一般不可行(除非使用 dynamic/@objc)。 联系点(互操作) 当 Swift 类继承自 NSObject 或标注 @objc / dynamic 时,Swift 的方法(或部分)会被暴露到 ObjC runtime,能被 objc_msgSend 调用和 runtime API 操作(比如 swizzling、associated objects)。 桥接(Bridging):Swift 与 ObjC 的对象和集合类型(NSString/Array 等)可桥接,ARC 也在交互时协调 Autorelease 池等。 调度(dispatch)比较总结 ObjC:全部通过消息发送(动态查找 IMP)。 Swift:编译器尽可能静态或通过 vtable/witness table 调度,只有 @objc/dynamic 使用 ObjC 消息分发(当需要 runtime 动态性时才这样做)。 何时使用 ObjC runtime 对 Swift 做操作 只对被暴露到 ObjC 的成员(@objc/dynamic 或继承 NSObject 的方法)安全可行;不要对纯 Swift 方法做 swizzle 或期望 runtime 行为与 ObjC 一致。 Swift 的 class layout、ABI、引用计数管理的差异在哪里? class layout(类内存布局) Objective‑C:对象开头通常是 isa 指针(指向 class/元类),类结构里有方法列表、ivar 列表等;实例变量通常以 ObjC 指定的顺序排布,能够通过 runtime 访问 ivar 偏移。 Swift: Swift 类对象也包含 isa(在 Apple 平台上仍使用 Objective‑C 兼容的 isa 表示法,尤其对于继承自 NSObject 的类),但 Swift 本身有自己的 metadata 结构,表示类大小、字段偏移、构造器等。 Swift 的 struct/enum 是值类型,布局可被内联到其他对象/栈上,且编译器可能做内存布局优化(比如所需对齐、内联存储、容器化等)。 Swift 支持 resilient(可演化)ABI 特性(模块间兼容时字段偏移可能不可预测),所以编译器使用 metadata 来访问字段。 ABI(应用二进制接口) ObjC:Obj‑C runtime ABI 稳定多年,消息发送约定固定(objc_msgSend)。 Swift:自 Swift 5 起,官方稳定了 ABI(对于 apple 平台)。但早期 Swift 的 ABI 不稳定导致不能随意混合不同版本二进制。 Swift 的 ABI 包括函数调用约定、元数据格式、对象布局方式、协议 witness 表的格式等。 引用计数(ARC)与内存管理 Objective‑C 使用 ObjC ARC(编译器插入 objc_retain/objc_release/objc_autorelease 等)。 Swift 使用 Swift ARC,实现为 swift_retain / swift_release 系列 runtime 函数(在编译器层插入)。在目标平台上与 ObjC 的内存管理需要协调: 对继承自 NSObject 的 Swift 对象或桥接到 ObjC 的值,编译器可能会插入 objc_retain/objc_release/objc_autorelease,或 runtime 会桥接到 Swift 的 retain/release。 Autorelease 池:Objective‑C 使用 autorelease 池(NSAutoreleasePool / @autoreleasepool),Swift 在与 ObjC 交互时会使用 autorelease,并在 Swift 异常处理/桥接中产生 autoreleased 对象。但 Swift 的纯内部对象不经常使用 autorelease。 优化与差异: Swift ARC 的函数名与实现与 ObjC 不同(swift_retain vs objc_retain),并有更多优化(比如栈上对象优化、尾递归优化、去除不必要的 retain/release)。 当 Swift 与 ObjC 混合时,编译器负责插入正确的跨运行时 retain/release 调用并维护对象生命周期。 动态分派和 vtable Swift 对 class 方法通常使用 vtable(虚函数表),而 ObjC 使用消息查找;这使得 Swift 在不需要动态性时能够获得更高性能(直接调用或索引 vtable)。 protocol 的方法在 Swift 使用 witness table(运行时结构,映射到具体实现),不同于 ObjC 的 protocol 方法通过 objc_msgSend + selector 调度(如果协议为 @objc)。 Source0(非端口事件)与 Source1(基于端口/内核)的具体解释(以及如何触发/实现) 概念回顾(RunLoop 的 input sources) Source0(也称为非端口源 / kCFRunLoopSourceVersion0): 通常是用户级别的事件源,不依赖内核事件。它由应用代码主动 signal(唤醒)或通过 performSelector:onThread: / performSelector:withObject:afterDelay: 等机制产生。 Source0 的回调在 RunLoop 的 “beforeSources” 阶段被处理。因为它不和内核打交道,RunLoop 不能被内核自动唤醒来处理它(通常需要某个内核事件或手动唤醒)。 一个常见 Source0 实例:performSelector:onThread:withObject:waitUntilDone: 在目标线程的 RunLoop 上放入了一个端口操作,或者在实现上通过 CFRunLoopSourceSchedule/Signal 调度回调。 Source1(也称为端口源 / kCFRunLoopSourceVersion1): 绑定内核级别的事件(mach port、socket、kqueue 等),当内核事件发生时,系统会唤醒对应线程的 RunLoop,并将事件放入 Source1 处理。 常见用途:低级网络事件(CFSocket、CFNetwork)、分布式通知、mach port 通信等。 触发与实现的差别(实战角度) Source0: 触发方式:程序显式 signal(如 CFRunLoopSourceSignal),或通过 UIKit/Cocoa 的高层 API(performSelector、Timer scheduling 在某些实现上也使用 Source0)。 典型实现:你可以创建一个 CFRunLoopSource(版本 0),手动在你的线程 runloop 上 schedule,该 source 的 perform 回调在被 signal 后会被处理。需要注意:因为没有内核事件关联,通常需要手动调用 CFRunLoopWakeUp 来确保运行中的 RunLoop 立即检查并处理 Source0(如果 RunLoop 正处于休眠)。 Source1: 触发方式:内核事件发生(socket 可读/可写、mach message 到达等),内核会自动唤醒等待该事件的线程 RunLoop。 典型实现:使用 CFSocket 创建 socket,然后用 CFSocketCreateRunLoopSource 将其包装成 CFRunLoopSource 并添加到 runloop。此后当 socket 有事件时,系统内核会通知,RunLoop 被唤醒并调用回调。 简单代码示例 Source0(用 CFRunLoopSourceCreate 创建一个 version 0 source): // Objective-C 样例(简化) CFRunLoopSourceContext ctx = {0}; ctx.perform = ^(void *info) { // 这里是 source 被处理时的回调 NSLog(@"Source0 callback"); }; CFRunLoopSourceRef source = CFRunLoopSourceCreate(NULL, 0, &ctx); CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
// 在别处 signal 并唤醒 CFRunLoopSourceSignal(source); CFRunLoopWakeUp(CFRunLoopGetCurrent()); Source1(使用 CFSocket): // Objective-C 简化示例 CFSocketContext sockCtx = {0, (__bridge void*)self, NULL, NULL, NULL}; CFSocketRef sock = CFSocketCreate(NULL, PF_INET, SOCK_STREAM, IPPROTO_TCP, kCFSocketReadCallBack, socketCallback, &sockCtx); CFRunLoopSourceRef sockSource = CFSocketCreateRunLoopSource(NULL, sock, 0); CFRunLoopAddSource(CFRunLoopGetCurrent(), sockSource, kCFRunLoopDefaultMode); // 当 socket 可读时,内核会触发该回调并唤醒 runloop 实用提示 如果你的事件是由外部 I/O(socket、mach port)驱动,优先使用 Source1(内核支持,低功耗、更正确)。 如果事件由纯应用逻辑产生(例如某线程想以消息的形式通知另一个线程),可以使用 Source0 或直接调用 performSelector:onThread:(但要确保目标线程的 runloop 正在运行并监听该 source)。 子线程:默认没有运行 RunLoop(懒创建但不自动 run)。使用 performSelector:afterDelay:、NSTimer 的注意事项与常见做法(为何优先用 GCD/NSOperation) 默认行为 每个线程都可以有一个 RunLoop(通过 CFRunLoopGetCurrent / RunLoop.current 获取),但子线程的 RunLoop 是“懒惰创建”,并且系统不会自动启动/运行它(除非你显式调用 run/runUntilDate/CFRunLoopRun 等)。 意味着:在子线程中如果不运行 runloop,scheduledTimer、performSelector:afterDelay:、performSelector:onThread: 等与 runloop 相关的机制不会被触发。 使用 performSelector:afterDelay: / NSTimer 的先决条件 这些 API 依赖于 RunLoop 中已注册的 source/ timer,如果你在子线程上 schedule 了一个 Timer,但该线程没有进入 runloop,Timer 永远不会触发。 performSelector:onThread: 也需要目标线程 runloop 处于运行状态且监听 source0。 常见替代方案(推荐) GCD(Grand Central Dispatch)或 OperationQueue 是更现代、更简单且可靠的方式来做异步工作/定时任务: GCD 示例:DispatchQueue.global().async { ... } 或 DispatchSourceTimer(替代 NSTimer,支持更精确和更低开销的调度)。 OperationQueue:支持依赖、取消及更丰富的控制。 优点: 不需要管理 runloop,避免子线程保活与清理问题。 系统更擅长管理线程池(而非你长期保活很多线程)。 何时仍需要 runloop: 你需要在子线程上实现长期存在的 event loop(比如某些需要保持 socket/port 持久连接的旧式设计),或者你必须与一些 API 互通(需要 runloop 的回调),这时才需要手动运行 runloop。 小示例:子线程上 schedule Timer 并运行 runloop let thread = Thread { // 把一个 port 加到 runloop,避免 runloop 立即退出(保持至少一个 source) let port = Port() RunLoop.current.add(port, forMode: .default) let timer = Timer(timeInterval: 1.0, repeats: true) { _ in print("timer fired on background thread") } RunLoop.current.add(timer, forMode: .default) // 运行 runloop RunLoop.current.run() // 直到被显式停止 } thread.start() 结论:默认不运行 runloop;更推荐用 GCD/Operation 做大多数并发任务,只有在必须时才管理子线程的 runloop。 子线程保活:while/run(mode:before:) vs CFRunLoopRun;为什么更建议用 GCD/OperationQueue? 子线程保活的方式 常用 1:RunLoop.current.run() 或 CFRunLoopRun():直接运行 runloop,直到 CFRunLoopStop 被调用。 常用 2:循环调用 RunLoop.current.run(mode: .default, before: .distantFuture) / while (!cancelled) { RunLoop.current.run(mode: .default, before: Date().addingTimeInterval(0.5)) }:更可控(可以在循环中检查退出条件)。 常用 3:给 runloop 加入一个 Port(如 Port())或 Schedule 一个 Timer,避免 runloop 在没有 source 时立即退出。 为什么更推荐 GCD/OperationQueue? 资源管理:GCD 使用系统管理的线程池,避免长时间保活线程的内存/上下文开销。 简洁:你不需要写复杂的循环或处理线程退出逻辑。 性能与伸缩性更好:系统可根据负载动态创建或回收线程。 可组合性:DispatchSource(比如 Timer、Signal、File Descriptor)可以替代不少 RunLoop 场景,且更现代。 performSelector:onThread: 的注意点 只会工作的前提:目标线程必须正在运行 RunLoop 且监听 source0。 因此,如果你用 GCD/Operation 并且没有显式运行 runloop,那么 performSelector:onThread: 将无法触发(不可靠)。 CFRunLoop 的底层主要依赖内核事件/通知(如 mach port、kqueue 等)来唤醒线程 —— 具体原理与唤醒条件 底层原理概述 CFRunLoop 在用户层面管理 sources/timers/observers,但真正让线程从休眠状态唤醒通常依赖内核通知机制。常见机制(具体实现视平台): Mach ports / mach messages:macOS/iOS 使用 Mach 内核,线程可以阻塞在一个或多个 mach port 上,内核在消息到达时唤醒线程。 kqueue / kevent:BSD 内核机制,用于文件描述符、事件通知等(macOS 支持 kqueue)。 select/poll/epoll(在某些实现或古老实现中)。 对 CFRunLoop 来说,Source1/ports 会被映射到底层这些内核等待机制上;当内核检测到相关事件发生(数据可读、消息到达),它会唤醒对应线程,内核会把事件传递回用户态的 runloop,runloop 调用相应的 callback。 RunLoop 的唤醒条件(具体): 已注册的 Source1 收到内核事件(socket 可读、mach port 被发送消息等); Timer 到期(NSTimer / CADisplayLink)—— runloop 被安排唤醒并处理计时器; 手动唤醒:CFRunLoopWakeUp(runloop) 强制唤醒,常配合 Source0 使用; 外部 I/O(内核事件)或系统事件(触摸/屏幕/网络)到来; RunLoop 内部被 signal(CFRunLoopSourceSignal)并且随后 CFRunLoopWakeUp 被调用(如果需要立刻处理)。 Source0 的特殊点(为何要 CFRunLoopWakeUp) 因为 Source0 不一定与内核事件关联(没有内核文件描述符或 port),如果 RunLoop 正处于等待内核通知的休眠状态,内核并不会因为一个只存在于用户空间的 Source0 被 signal 而唤醒线程。因此: 当你 signal 一个 Source0 后,常需要调用 CFRunLoopWakeUp 来确保 runloop 退出休眠并检查该 source。 CFRunLoopWakeUp / CFRunLoopStop CFRunLoopWakeUp(runloop):如果 runloop 在等待内核事件,会被唤醒并继续循环,处理 pending sources/timers。 CFRunLoopStop(runloop):让 CFRunLoopRun() 或 RunLoop.current.run() 退出。 调试与验证 使用 CFRunLoopObserver 在不同阶段打印 activity,可以观察到 runloop 是如何被唤醒、处理事件与再次休眠的。 使用 Instruments(Time Profiler / System Trace / Network)观察线程休眠/唤醒行为与内核调用。 (第 7 的重复项)整合说明与常见实践、调试建议 整体要点回顾 如果你的 source 与内核事件(socket、mach port、文件描述符)有关,使用 Source1(CFSocket / mach port)是正确选择——系统会由内核在事件到来时唤醒 runloop。 如果你的 source 是用户级别(逻辑触发),使用 Source0,并在 signal 后调用 CFRunLoopWakeUp(或确保有其它内核事件会唤醒 runloop),否则事件可能被延迟到下一次内核唤醒。 常见问题(与解决办法) Timer 在滚动/触摸时暂停:原因通常是 timer 被加入 default mode,而当前 runloop 在 UITracking mode。解决:把 timer 加到 .common 或同时加到需要的 mode。 performSelector:onThread: 无效:检查目标线程是否运行 runloop 并监听 Source0;否则改用 GCD。 子线程长时间占用导致资源浪费:避免用大量保活线程,用 GCD/Operation 做任务拆分。 调试技巧 在代码中添加 CFRunLoopObserver 打印 runloop activity,逐步定位是哪个阶段阻塞或未被唤醒。 使用 po CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent()) 等查看当前 mode。 Instruments 的 Main Thread Checker / Time Profiler / System Trace 分析主线程卡顿和线程唤醒。 在调试 Source0 时,可以在 signal 之后临时调用 CFRunLoopWakeUp 来确认回调是否被立刻执行(用于排查是否是唤醒问题)。 补充示例:如何使用 CFRunLoopObserver 查看 RunLoop 活动(Swift) import Foundation
let observer = CFRunLoopObserverCreateWithHandler(nil, CFRunLoopActivity.allActivities.rawValue, true, 0) { (observer, activity) in print("RunLoop 活动:", activity) } CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, CFRunLoopMode.commonModes) // 现在 RunLoop 中会打印所有活动阶段 总结(快速回顾) 在 +load 做全局替换时使用 dispatch_once 能保证线程安全与幂等性。 Swift 有自己的运行时与 ARC 实现,默认更静态、性能更好;与 ObjC runtime 的联系体现在 @objc/dynamic、NSObject 继承和桥接上。 Swift 的 class layout、ABI、引用计数在实现细节上与 ObjC 不同(metadata、vtable/witness table、swift_retain 等),但在平台上可互操作。 RunLoop 的 Source0/Source1 本质差异在于是否与内核事件绑定(Source1 与内核绑定,Source0 为用户级),这决定了是否需要手动唤醒。 子线程默认不运行 runloop,优先推荐使用 GCD/Operation 来处理并发任务;只有在必须长期监听事件或兼容老 API(需要 runloop)时才手动运行 runloop。 CFRunLoop 的唤醒依赖内核事件(mach port、kqueue 等)或主动 CFRunLoopWakeUp,理解这些能帮助定位定时器、performSelector、socket 等相关问题。 如果你想,我可以为下面任一项准备可运行的小 Demo(Xcode / Playground): 一个演示 method swizzling(+load + dispatch_once)与其线程安全性的小例子(Objective‑C)。 一个演示 Swift 与 ObjC runtime 互操作(@objc/dynamic swizzle)的示例。 一个 RunLoop demo:演示 Source0/Source1 差别(例如创建 Source0、CFSocket -> Source1、并观察唤醒行为)并用 CFRunLoopObserver 打印活动。 告诉我你最想要哪个 Demo,我会直接给出完整可运行代码与运行步骤