一、Runtime(Objective‑C 运行时)
-
核心概念与机制
-
什么是 Runtime
-
Runtime 是 Objective‑C 的动态运行时库(libobjc),负责类型信息、对象内存布局、方法分发(消息传递)、动态创建类/方法/变量、关联对象等功能。
-
它使 Objective‑C 成为动态语言:许多决策(方法绑定、实现替换)都在运行时完成,而非编译时。
-
重要数据结构(高层)
-
对象(object)通常是一个指向类的指针(以前称为 isa 指针)。对象内存开始处通常存放 isa。
-
类(Class):包含方法列表(method list)、属性列表(ivar list / property list)、协议(protocols)、父类指针、元类指针等。
-
元类(meta‑class):每个类有对应的元类,元类存放类方法的实现(类方法在元类中),类本身也是一个对象(其 isa 指向元类)。
-
Selector(SEL):方法名的抽象表示(C 字符串映射到整数/指针)。
-
IMP:函数指针,指向具体的函数实现(方法体)。
-
消息发送(objc_msgSend)
-
Objective‑C 的方法调用本质是消息发送:objc_msgSend(receiver, selector, ...).
-
objc_msgSend 在运行时查找 receiver 中 selector 对应的 IMP:向上搜索类的 method list -> categories -> 父类 -> 根类。
-
若找不到实现,则走动态方法解析 / 转发流程(见下)。
-
方法查找与流程(包含动态解析与转发)
-
查找不到 selector 时的处理顺序(简化):
-
a) +resolveInstanceMethod: / +resolveClassMethod: —— 允许动态添加实现(class_addMethod)。
-
b) -forwardingTargetForSelector: —— 简单转发:返回另一个对象去执行 selector(避免构造 NSInvocation)。
-
c) -methodSignatureForSelector: 与 -forwardInvocation: —— 完整转发:创建 NSMethodSignature/NSInvocation,可以修改调用或转发给其他对象。
-
d) 若都没有处理,则抛出 unrecognized selector sent to instance 异常。
-
动态添加方法(class_addMethod)常用于在 runtime 阶段补救未实现方法。
-
常用 Runtime API(核心)
-
查找/操作类/方法/ivar/property: objc_getClass, objc_getMetaClass, object_getClass class_addMethod, class_replaceMethod, class_getInstanceMethod, class_getClassMethod method_exchangeImplementations(方法交换,Swizzling) method_getImplementation / method_setImplementation sel_registerName / NSSelectorFromString, sel_getName class_copyMethodList, class_copyIvarList, class_copyPropertyList object_setClass / object_getClass
-
关联对象:
-
objc_setAssociatedObject, objc_getAssociatedObject, objc_removeAssociatedObjects
-
关联策略:OBJC_ASSOCIATION_ASSIGN, _RETAIN_NONATOMIC, _COPY_NONATOMIC, 等。
-
方法交换(Method Swizzling)
-
用途:在不修改原始类源代码的情况下替换或扩展方法行为(AOP、统计、日志、fix 等)。
-
实现要点:
-
通常在 +load 或 +initialize 中安全执行(+load 比较早且线程安全)。
-
交换两个方法的 IMP(method_exchangeImplementations)。
-
小心父类/子类、category 的加载顺序、保证幂等(防重复交换)。
-
风险:破坏类的这一契约可导致不可预期副作用、兼容性问题、难以调试。
-
示例(Objective‑C):
#import <objc/runtime.h>
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class cls = [self class];
SEL originalSEL = @selector(viewWillAppear:);
SEL swizzledSEL = @selector(xxx_viewWillAppear:);
Method originalMethod = class_getInstanceMethod(cls, originalSEL);
Method swizzledMethod = class_getInstanceMethod(cls, swizzledSEL);
BOOL didAdd = class_addMethod(cls,
originalSEL,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAdd) {
class_replaceMethod(cls,
swizzledSEL,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
关联对象(Associated Objects) 用于在 Category 中添加属性的“替代”实现,不需要修改原始类的 ivar。 示例: objc_setAssociatedObject(self, @selector(customProperty), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC); id value = objc_getAssociatedObject(self, @selector(customProperty)); Swift 与 Objective‑C Runtime 的交互 Swift 有自己的运行时,但与 ObjC 运行时交互通过 @objc、dynamic、@objcMembers 等关键字。 只有被暴露到 ObjC 的 Swift 方法/属性(标注 @objc 或继承自 NSObject 且满足自动推断)才能被 objc_msgSend 或 runtime API 操作/swizzle。 对纯 Swift(非 @objc)方法不能直接用 method_exchangeImplementations;若需支持,必须声明为 dynamic 或 @objc dynamic(dynamic 会使用 ObjC 运行时分发)。 Swift 的 class layout、ABI、引用计数管理有差异,直接用 runtime 改写 Swift 内部实现很危险。 常见应用场景 KVO(runtime 在底层创建子类并重写 setter) Category:增加方法(但不能增加实例变量) AOP(日志、统计、性能埋点) JSON ↔ Model(字典转模型通过 runtime 枚举属性) 自动归档 NSCoding、自动复制(实现 property 列表导出) 运行时修复(method swizzling 修补第三方 bug) 常见错误/坑 在 +load 里务必使用 dispatch_once。 不当的 method swizzling 会影响所有实例,需小心父类/子类影响。 通过关联对象持有大量对象可能导致内存泄漏(需要管理策略)。 使用 runtime 修改私有 API 或未公开行为可能导致上架风险。 对 Swift 对象做 Runtime 操作时,需确保方法确实暴露到 ObjC。 二、RunLoop(事件循环) — 概念、组成与实战 什么是 RunLoop RunLoop 是对线程事件循环的抽象。每个线程都有一个 RunLoop(懒创建),主线程的 RunLoop 由 UIApplicationMain 启动并管理 UI 事件。 RunLoop 的职责:在没有任务时让线程休眠,等待事件(input sources、timers、ports 等)唤醒并处理,然后继续休眠或退出。 主要组成要素 Input Sources(事件源) Source0(非端口事件,基于 performSelector:onThread: 或手动唤醒的任务):不基于内核,适用于代码驱动任务调度。 Source1(基于端口/内核,如 mach port、CFRunLoopSource 做系统事件与 IPC 通信):通常配合内核事件(socket、port)。 Timers(定时器) NSTimer(RunLoop-layer),CADisplayLink(屏幕刷新同步)。 Observers(观察者) 可以监听 RunLoop 的 lifecycle events:entry、beforeTimers、beforeSources、beforeWaiting、afterWaiting、exit 等(CFRunLoopObserver)。 Modes(模式) RunLoop 在某一时刻只有一个 mode 在运行,不同的 mode 决定哪些 sources/timers/observers 会被处理。 常见 mode: NSDefaultRunLoopMode(kCFRunLoopDefaultMode) UITrackingRunLoopMode(用于滚动/触摸追踪) NSRunLoopCommonModes(kCFRunLoopCommonModes,代表“通用”集合) 如果 timer 被加入到了 default mode 而当前 runloop 在 UITracking 模式,那个 timer 将不会触发 — 这是 UI scroll 时计时器暂停的常见原因。 主线程 vs 子线程 主线程:UIApplicationMain 启动并运行主 RunLoop,负责事件分发、UI 刷新、触摸事件。 子线程:默认没有运行 RunLoop(懒创建但不自动 run),如果需要持续监听事件或使用 performSelector:afterDelay:、NSTimer,需要自己 run RunLoop(例如在线程函数最后调用 CFRunLoopRun)。 常见做法是使用 GCD/NSOperation 来替代自己管理 RunLoop,因为 GCD 更现代且更容易管理。 RunLoop 生命周期事件(CFRunLoopActivity 枚举) kCFRunLoopEntry:进入 runloop。 kCFRunLoopBeforeTimers:处理 timers 之前。 kCFRunLoopBeforeSources:处理 sources 之前。 kCFRunLoopBeforeWaiting:进入休眠之前。 kCFRunLoopAfterWaiting:从休眠中唤醒之后。 kCFRunLoopExit:退出 runloop。 kCFRunLoopAllActivities:所有状态。 常见 API(Swift / CF) Swift: RunLoop.current / RunLoop.main RunLoop.main.add(timer, forMode: .common) RunLoop.current.run(until: Date()) Thread.sleep(forTimeInterval:) perform(_:on:with:waitUntilDone:) CF(低层): CFRunLoopGetCurrent(), CFRunLoopGetMain() CFRunLoopAddSource/Timer/Observer CFRunLoopRun(), CFRunLoopRunInMode(), CFRunLoopStop() 常见用例与注意事项 NSTimer scheduledTimer 会被加入默认 mode;如果需要在滚动(UITracking)期间也触发,加入 RunLoop.Mode.common。 NSTimer 与目标对象的循环引用:使用 block timer 或在 target 中手动 invalidate 来避免内存泄漏。 非主线程:使用 scheduledTimer 之前需确保 thread runloop 正在 run。 CADisplayLink 与屏幕刷新同步,通常加入 RunLoop.Mode.common。 子线程保活 如果你想让子线程持续运行以便接收事件,可以在 runLoop 内做 while循环并调用 run(mode:before:) 或 CFRunLoopRun。 更建议的做法:用 GCD 或 OperationQueue 进行异步工作,避免自己管理 runloops。 performSelector:onThread: 需要目标线程的 runloop 正在 run 并监听 source0 才能收到。 UI 线程卡顿问题 RunLoop 在执行任务时阻塞会导致 UI 无响应,建议将耗时任务放到后台队列,不在主线程中长时间运行。 交互模式与 Timer 若你需要 timer 在滑动时仍然触发,使用 RunLoop.Mode.common;否则 timer 可能在滑动期间暂停。 进阶细节(实现与内核交互) CFRunLoop 的底层主要依赖内核事件/通知(如 mach port、kqueue 等)来唤醒线程。 Source0 是用户级别事件(不通过内核),Source1/port 绑定内核资源。 RunLoop 的唤醒条件:一个已注册的 source 有事件、timer 到期、手动唤醒(CFRunLoopWakeUp)或者外部 I/O 等。 调试与分析技巧 查看当前 RunLoop 模式: RunLoop.current.currentMode 或 CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent())。 使用 CFRunLoopObserverCreateWithHandler 在开发时打印 RunLoop 状态来分析卡顿点。 Instruments 的 Time Profiler / Main Thread Checker / System Trace:找出主线程长时间占用。 在崩溃日志中判断是否为 unrecognized selector 来调试 runtime 转发问题。 使用 symbolicated crash log 来查看被交换或转发的函数栈。 常见错误/坑与建议 NSTimer 在默认 mode 下在滚动时被暂停:解决方案为加入 common modes: RunLoop.main.add(timer, forMode: .common) 或 RunLoop.commonModes 在 ObjC。 在子线程使用 performSelector:afterDelay: 而没有 runloop:不会回调。 不要在主线程长时间阻塞 runloop:尽量使用异步、分片处理(拆分任务到多帧执行)。 在 use CADisplayLink 时避免重逻辑放到回调中导致 drop frame。 当使用 CFRunLoopObserver 做监控时,注意不要在回调中做耗时操作(会干扰测量)。 三、示例代码(便于实操) Objective‑C:简单的 method swizzling(前面已给出) — 见上。 Swift:在主 RunLoop 添加一个 observer(使用 CFRunLoop) import Foundation
let observer = CFRunLoopObserverCreateWithHandler(nil, CFRunLoopActivity.beforeSources.rawValue | CFRunLoopActivity.afterWaiting.rawValue, true, 0) { (observer, activity) in print("RunLoop activity: (activity)") } CFRunLoopAddObserver(CFRunLoopGetMain(), observer, CFRunLoopMode.commonModes) Swift:使用 Timer 并加入 common mode let timer = Timer(timeInterval: 1.0, repeats: true) { t in print("tick") } RunLoop.main.add(timer, forMode: .common) Swift:在后台线程开启 RunLoop(示例,仅用于理解) let thread = Thread { // 需要保活的任务:添加 Port 或 Timer let port = Port() RunLoop.current.add(port, forMode: .default) // 运行 runloop,直到显式停止 RunLoop.current.run() } thread.start()
// 在另一个线程上执行 perform perform(#selector(doWork), on: thread, with: nil, waitUntilDone: false) (注:通常 prefer GCD/OperationQueue) 四、将 Runtime 与 RunLoop 联系起来的场景 在运行时替换方法时,如果在某些 RunLoop 模式下替换行为不同,可能导致 UI 行为差异(例如 swizzle 导致 UITracking 模式影响)。 使用 performSelector:onThread: 触发 selector 依赖 runloop 的 source0,被转发的 selector 又可能牵涉到 runtime 转发机制(forwardInvocation)。 五、调试/学习资源(推荐) Apple 文档:Objective‑C Runtime Reference,RunLoop Programming Guide Mike Ash 的博客(关于 runtime internals,method dispatch) WWDC 关于 Runtime、Main RunLoop 和线程管理的视频 《深入理解 Objective‑C 运行时》(书籍/博客文章) 六、快速要点总结 Runtime 是 Objective‑C 的动态引擎:消息发送、方法解析/替换/转发、关联对象、动态创建类型等都基于它。慎用 method swizzling,理解 resolve/forwarding 流程。 RunLoop 是线程的事件循环:用它管理 timers、sources、observers 和模式。主线程的 RunLoop 由系统启动,子线程需手动 run。避免在主线程做耗时任务,正确选择 RunLoop 模式以控制事件触发行为。 Swift 与 ObjC runtime 的交互需要显式 @objc/dynamic,纯 Swift 方法无法直接被 ObjC runtime 操作。