iOS开发基础39-RunLoop
RunLoop 深度解析:运行循环、Mode、Source/Timer/Observer 与应用场景
本文系统梳理 iOS RunLoop 机制:RunLoop 的作用与本质、与线程的一一对应关系、Core Foundation 五大相关类(CFRunLoopRef/Mode/Source/Timer/Observer)、RunLoop 处理流程(休眠与唤醒)、以及 NSTimer 模式问题、常驻线程、自动释放池等典型应用场景。
一、什么是 RunLoop
1. 定义
RunLoop(运行循环)是线程中的一个 do-while 循环,核心作用:
- 保持程序持续运行:App 启动后不退出,持续等待用户事件。
- 处理各种事件:触摸事件、定时器事件、Selector 事件、端口消息等。
- 节省 CPU 资源:有事处理事,没事休眠,不占用 CPU 轮询。
2. 没有 RunLoop 的程序
int main(int argc, char *argv[]) {
NSLog(@"execute main function");
return 0; // 执行完立即退出
}
没有 RunLoop,main 函数执行完后程序立即退出。
3. 有 RunLoop 的程序
iOS App 的 UIApplicationMain 内部启动了主线程的 RunLoop,程序保持运行状态,等待事件。
int main(int argc, char *argv[]) {
@autoreleasepool {
// UIApplicationMain 内部启动主线程 RunLoop,不会返回(直到 App 退出)
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
UIApplicationMain在启动 RunLoop 后不会返回,直到 App 终止。可以通过在 main 函数中UIApplicationMain后加打印验证——该打印永远不会执行。
4. RunLoop 本质(模拟实现)
// RunLoop 本质就是一个 do-while 循环
int main(int argc, const char *argv[]) {
do {
// 1. 处理事件(Source/Timer/Observer)
// 2. 没有事件时休眠(mach_msg 等待消息)
// 3. 被消息唤醒后继续处理
} while (1);
return 0;
}
RunLoop 不是简单的空循环(空循环会占用 100% CPU),而是通过 mach_msg 进入内核态休眠,等待消息唤醒,无事时几乎不消耗 CPU。
二、RunLoop 对象与线程
1. 两套 API
| 框架 | 类型 | 说明 |
|---|---|---|
| Foundation | NSRunLoop |
Objective-C 封装,面向对象 |
| Core Foundation | CFRunLoopRef |
C 语言 API,开源,可研究底层 |
NSRunLoop 基于 CFRunLoopRef 封装,研究底层原理需看 CFRunLoopRef。
2. RunLoop 与线程的关系
- 一一对应:每条线程有且仅有一个 RunLoop 对象,存储在全局字典中(key 为线程指针,value 为 RunLoop)。
- 主线程:RunLoop 已自动创建并启动(
UIApplicationMain中)。 - 子线程:RunLoop 默认未创建,第一次获取时创建,线程结束时销毁。
- 子线程 RunLoop 必须手动启动:创建后需调用
run或runMode:beforeDate:才会运行,且必须至少添加一个 Source 或 Timer,否则 RunLoop 会立即退出。
3. 获取 RunLoop 对象
// Foundation
NSRunLoop *current = [NSRunLoop currentRunLoop]; // 当前线程的 RunLoop
NSRunLoop *main = [NSRunLoop mainRunLoop]; // 主线程的 RunLoop
// Core Foundation
CFRunLoopRef cfCurrent = CFRunLoopGetCurrent();
CFRunLoopRef cfMain = CFRunLoopGetMain();
在子线程中调用
[NSRunLoop currentRunLoop]会触发 RunLoop 的创建。
三、RunLoop 相关类(Core Foundation)
RunLoop 涉及 5 个核心类:
| 类 | 说明 |
|---|---|
CFRunLoopRef |
RunLoop 对象本身 |
CFRunLoopModeRef |
运行模式 |
CFRunLoopSourceRef |
事件源(输入源) |
CFRunLoopTimerRef |
定时源 |
CFRunLoopObserverRef |
观察者 |
1. CFRunLoopModeRef(运行模式)
一个 RunLoop 包含多个 Mode,每个 Mode 包含若干 Source、Timer、Observer。每次 RunLoop 运行只能指定一个 Mode(CurrentMode),切换 Mode 需退出当前 Loop 再重新指定。
| Mode | 说明 |
|---|---|
kCFRunLoopDefaultMode(NSDefaultRunLoopMode) |
App 默认模式,处理大部分事件 |
UITrackingRunLoopMode |
界面追踪模式,UIScrollView 滚动时切换到此模式,保证滑动流畅 |
UIInitializationRunLoopMode |
App 启动初始化时的模式,启动完成后不再使用 |
GSEventReceiveRunLoopMode |
系统事件接收模式,内部使用 |
kCFRunLoopCommonModes(NSRunLoopCommonModes) |
模式集合,不是真正的模式,包含 Default 和 Tracking 等;添加到 CommonModes 的事件在 Default 和 Tracking 下都能执行 |
NSRunLoopCommonModes是一个模式集合(结构体),将 Timer/Source 添加到 CommonModes 时,系统会同步添加到集合中的所有模式。
2. CFRunLoopSourceRef(事件源)
| 类型 | 说明 |
|---|---|
| Source0 | 非基于 Port,需要手动触发(CFRunLoopSourceSignal + CFRunLoopWakeUp 唤醒 RunLoop)。如 UIEvent 触摸事件、performSelector 等 |
| Source1 | 基于 Port(mach_port),由内核或其他线程主动唤醒。如系统事件分发、CFMachPort 消息等 |
Source0 处理完后,RunLoop 会检查是否有 Source1,如果有则跳转到 Source1 处理流程。
3. CFRunLoopTimerRef(定时源)
基于时间的触发器,NSTimer 本质就是对 CFRunLoopTimerRef 的封装。Timer 只能在添加时指定的 Mode 下运行,Mode 不匹配时不会触发。
4. CFRunLoopObserverRef(观察者)
监听 RunLoop 的状态变化,可监听以下活动(CFRunLoopActivity):
| 活动 | 说明 |
|---|---|
kCFRunLoopEntry |
即将进入 RunLoop |
kCFRunLoopBeforeTimers |
即将处理 Timer |
kCFRunLoopBeforeSources |
即将处理 Source |
kCFRunLoopBeforeWaiting |
即将进入休眠 |
kCFRunLoopAfterWaiting |
刚从休眠唤醒 |
kCFRunLoopExit |
即将退出 RunLoop |
kCFRunLoopAllActivities |
所有活动 |
// 创建 Observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(
kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES, // repeats: 是否重复
0, // order: 优先级,0 为默认
^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry: NSLog(@"进入 Loop"); break;
case kCFRunLoopBeforeTimers: NSLog(@"即将处理 Timer"); break;
case kCFRunLoopBeforeSources: NSLog(@"即将处理 Source"); break;
case kCFRunLoopBeforeWaiting: NSLog(@"即将休眠"); break;
case kCFRunLoopAfterWaiting: NSLog(@"刚唤醒"); break;
case kCFRunLoopExit: NSLog(@"退出 Loop"); break;
default: break;
}
}
);
// 添加 Observer 到主线程 RunLoop 的默认模式
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopDefaultMode);
CFRelease(observer); // Core Foundation 对象需手动释放
CFRunLoopObserverCreateWithHandler创建的对象需用CFRelease释放(Create 规则)。
四、RunLoop 处理逻辑
RunLoop 每次循环的精确流程:
1. 通知 Observer:kCFRunLoopEntry(进入 Loop)
↓
2. 通知 Observer:kCFRunLoopBeforeTimers(即将处理 Timer)
↓
3. 通知 Observer:kCFRunLoopBeforeSources(即将处理 Source)
↓
4. 处理 Source0(非 Port 事件,如触摸、performSelector)
↓
5. 如果有 Source1(Port 事件),跳转到第 9 步处理
↓
6. 通知 Observer:kCFRunLoopBeforeWaiting(即将休眠)
↓
7. 休眠:调用 mach_msg 进入内核态,等待消息唤醒
(Timer 到期 / Source1 事件 / 手动唤醒 / 超时)
↓
8. 通知 Observer:kCFRunLoopAfterWaiting(刚唤醒)
↓
9. 处理唤醒事件:
- Timer 到期 → 处理 Timer
- Source1 → 处理 Source1
- 手动唤醒 → 无操作
↓
10. 回到第 2 步(继续循环)
↓(RunLoop 退出时)
11. 通知 Observer:kCFRunLoopExit(退出 Loop)
休眠通过
mach_msg系统调用实现,线程进入内核态等待消息,几乎不消耗 CPU。被唤醒后回到用户态继续处理事件。
五、RunLoop 应用场景
1. NSTimer 的 Mode 问题(UIScrollView 滚动时不执行)
NSTimer 必须添加到 RunLoop 才能执行,且只在指定 Mode 下触发。
问题:用 scheduledTimerWithTimeInterval: 创建的 Timer 默认添加到 NSDefaultRunLoopMode。当 UIScrollView 滚动时,RunLoop 切换到 UITrackingRunLoopMode,Default 模式下的 Timer 不会触发,表现为滚动时定时器暂停。
解决方案:将 Timer 添加到 NSRunLoopCommonModes(模式集合,滚动时也能执行)。
// 方式一:手动创建并添加到 CommonModes
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(tick) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
// 方式二:scheduled 方法(默认添加到 DefaultMode,滚动时不执行)
NSTimer *scheduledTimer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(tick) userInfo:nil repeats:YES];
如果 Timer 任务不要求滚动时执行,用 DefaultMode 即可(滚动时暂停,保证滑动流畅)。如果要求滚动时也执行,用 CommonModes。
2. GCD 定时器(不依赖 RunLoop)
GCD 定时器(dispatch_source_t)基于 GCD 队列执行,不依赖 RunLoop,比 NSTimer 更精准,在子线程也可运行:
dispatch_queue_t queue = dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0);
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 设置定时器:起始时间、间隔、误差(0 为最精准)
dispatch_source_set_timer(timer,
dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC),
2 * NSEC_PER_SEC,
0);
dispatch_source_set_event_handler(timer, ^{
NSLog(@"GCD 定时器触发");
});
dispatch_resume(timer); // GCD 源默认挂起,必须 resume 才开始
GCD 定时器创建后默认挂起,必须调用
dispatch_resume才开始执行。取消用dispatch_cancel。
3. 常驻线程(子线程保持存活)
子线程默认执行完任务后即销毁。如需子线程长期存活(常驻线程),需启动 RunLoop 并添加至少一个 Source 或 Timer:
@interface PermanentThread : NSObject
@property (nonatomic, strong) NSThread *thread;
@end
@implementation PermanentThread
- (instancetype)init {
if (self = [super init]) {
self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadMain) object:nil];
[self.thread start];
}
return self;
}
- (void)threadMain {
@autoreleasepool {
// 获取 RunLoop(第一次获取时创建)
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
// 必须添加至少一个 Source 或 Timer,否则 RunLoop 立即退出
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
// 启动 RunLoop(阻塞当前线程,保持存活)
[runLoop run];
}
}
// 在常驻线程执行任务
- (void)performTaskInPermanentThread {
[self performSelector:@selector(doWork) onThread:self.thread withObject:nil waitUntilDone:NO];
}
- (void)doWork {
NSLog(@"常驻线程执行任务: %@", [NSThread currentThread]);
}
@end
常驻线程的 RunLoop 必须添加 Source/Timer,否则
run会立即返回,线程结束。添加NSMachPort是常见的保持方式(不实际使用,仅为了让 RunLoop 不退出)。
4. 自动释放池(AutoreleasePool)与 RunLoop
主线程的自动释放池由 RunLoop 的 Observer 管理:
- 进入 RunLoop(kCFRunLoopEntry):Observer 创建第一个 autoreleasepool。
- 即将休眠(kCFRunLoopBeforeWaiting):Observer 释放旧的 autoreleasepool,创建新的。
- 刚唤醒(kCFRunLoopAfterWaiting):不操作 pool(新 pool 已在休眠前创建)。
- 退出 RunLoop(kCFRunLoopExit):Observer 释放最后一个 autoreleasepool。
这就是为什么主线程不需要手动管理 autoreleasepool——RunLoop 在每次循环休眠前自动释放旧 pool 并创建新 pool,防止内存峰值过高。子线程没有自动创建的 RunLoop,需手动管理 autoreleasepool(如
@autoreleasepool {})。
5. 性能优化:监听 RunLoop 卡顿
通过 Observer 监听主线程 RunLoop 的 kCFRunLoopBeforeSources 到 kCFRunLoopBeforeWaiting 的耗时,如果超过阈值(如 16ms),说明主线程卡顿,可调用栈信息定位卡顿原因。这是第三方卡顿检测库(如 SMCallStack)的底层原理。
六、Swift 版本对照
获取 RunLoop
let current = RunLoop.current // 当前线程 RunLoop
let main = RunLoop.main // 主线程 RunLoop
let cfCurrent = CFRunLoopGetCurrent()
let cfMain = CFRunLoopGetMain()
NSTimer 添加到 CommonModes
let timer = Timer(timeInterval: 2.0, target: self, selector: #selector(tick), userInfo: nil, repeats: true)
RunLoop.current.add(timer, forMode: .common) // .common 即 NSRunLoopCommonModes
GCD 定时器
let queue = DispatchQueue.global()
let timer = DispatchSource.makeTimerSource(queue: queue)
timer.schedule(deadline: .now() + 2, repeating: 2.0)
timer.setEventHandler {
print("GCD 定时器触发")
}
timer.resume() // 必须 resume
常驻线程
class PermanentThread {
private var thread: Thread?
init() {
thread = Thread(target: self, selector: #selector(threadMain), object: nil)
thread?.start()
}
@objc private func threadMain() {
autoreleasepool {
let runLoop = RunLoop.current
runLoop.add(NSMachPort(), forMode: .default)
runLoop.run()
}
}
func performTask() {
perform(#selector(doWork), on: thread!, with: nil, waitUntilDone: false)
}
@objc private func doWork() {
print("常驻线程: \(Thread.current)")
}
}
CFRunLoopObserver
let observer = CFRunLoopObserverCreateWithHandler(
kCFAllocatorDefault,
CFRunLoopActivity.allActivities.rawValue,
true,
0
) { _, activity in
switch activity {
case .entry: print("进入 Loop")
case .beforeTimers: print("即将处理 Timer")
case .beforeSources: print("即将处理 Source")
case .beforeWaiting: print("即将休眠")
case .afterWaiting: print("刚唤醒")
case .exit: print("退出 Loop")
default: break
}
}
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, .defaultMode)
七、总结
- RunLoop 本质:线程中的 do-while 循环,通过
mach_msg休眠等待消息,有事处理、无事休眠,保持线程存活并节省 CPU。 - 与线程关系:一一对应,存储在全局字典;主线程自动创建启动,子线程第一次获取时创建,需手动启动且必须添加 Source/Timer。
- 五大类:
CFRunLoopRef(RunLoop 本身)、CFRunLoopModeRef(模式,每次只能运行一个)、CFRunLoopSourceRef(Source0 手动触发 / Source1 内核唤醒)、CFRunLoopTimerRef(定时源)、CFRunLoopObserverRef(观察者,监听 6 种活动)。 - Mode 机制:Default(默认)、Tracking(滚动)、CommonModes(模式集合);Timer/Source 只在指定 Mode 执行,UIScrollView 滚动时切换到 Tracking 导致 Default 下 Timer 暂停,解决方案是添加到 CommonModes。
- 处理流程:Entry → BeforeTimers → BeforeSources → 处理 Source0 → BeforeWaiting → 休眠(mach_msg)→ AfterWaiting → 处理唤醒事件 → 循环;退出时通知 Exit。
- 应用场景:NSTimer Mode 问题与解决方案、GCD 定时器(不依赖 RunLoop,更精准)、常驻线程(启动 RunLoop + 添加 Source/Timer)、自动释放池(RunLoop Observer 管理,休眠前释放旧 pool 创建新 pool)、卡顿检测(Observer 监听耗时)。
- 两套 API:Foundation
NSRunLoop(面向对象)和 Core FoundationCFRunLoopRef(C 语言,开源),NSRunLoop 基于 CFRunLoopRef 封装。

浙公网安备 33010602011771号