RunLoop 和线程 (Thread) 的关系?
RunLoop 和线程(Thread)是 iOS/macOS 开发中密不可分的两个概念。用一句话总结它们的关系:
RunLoop 是为了管理线程而存在的,它们之间是「一一对应」的关系。
以下是详细的深度解析:
1. 核心关系:一一对应
- 映射关系: 每个线程(Thread)都有且仅有一个 RunLoop 对象与之对应。
- 存储方式: 在底层(Core Foundation 源码中),RunLoop 是存储在一个全局的
CFDictionary中的。- Key: 线程对象(
pthread_t)。 - Value: 对应的 RunLoop 对象(
CFRunLoopRef)。
- Key: 线程对象(
2. 创建与获取机制(Lazy Loading)
RunLoop 并不是线程创建时自动生成的(主线程除外),而是懒加载(Lazy Loading)的。
- 主线程(Main Thread): 系统在 App 启动时,会自动为主线程创建并启动一个 RunLoop。这是因为主线程需要一直处理 UI 事件、手势、屏幕刷新等,不能退出。
- 子线程(Background Thread): 默认情况下,子线程没有 RunLoop。
- 如果你不主动获取,它就没有。
- 当你第一次调用
[NSRunLoop currentRunLoop]或CFRunLoopGetCurrent()时,系统会检测该线程是否有 RunLoop,如果没有,就会创建一个并保存到全局字典中。
3. 生命周期绑定
RunLoop 的生命周期与线程是绑定的:
- 出生: 在线程内部第一次获取 RunLoop 时创建。
- 消亡: 当线程结束(Exit)时,其对应的 RunLoop 也会被销毁。
- 注意: 你无法自己创建 RunLoop 对象(不能
alloc init),只能通过系统提供的 API 获取当前线程的 RunLoop。
4. 为什么线程需要 RunLoop?
默认情况下,线程执行完入口任务(block 或 selector)后就会立即退出(销毁)。
- 没有 RunLoop 的线程:plaintext
// 任务执行完,线程立马死掉 [NSThread detachNewThreadSelector:@selector(doSomething) toTarget:self withObject:nil]; - 有 RunLoop 的线程:
RunLoop 的本质是一个do...while循环。它让线程在有工作时忙碌,没工作时休眠(释放 CPU 资源),而不是直接退出。- 应用场景: 如果你需要一个子线程“常驻”在后台,随时等待处理新的任务(例如:后台网络监听、定时器
NSTimer),你就需要在这个子线程中开启 RunLoop。
- 应用场景: 如果你需要一个子线程“常驻”在后台,随时等待处理新的任务(例如:后台网络监听、定时器
5. 代码层面的体现
在 Core Foundation 源码中,获取 RunLoop 的逻辑大致如下(伪代码):
c
// 获取当前线程的 RunLoop
CFRunLoopRef _CFRunLoopGet0(pthread_t thread) {
// 1. 加锁访问全局字典
// 2. 尝试从字典中根据 thread 获取 RunLoop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, thread);
if (!loop) {
// 3. 如果没有,创建一个新的 RunLoop
loop = __CFRunLoopCreate(thread);
// 4. 存入字典
CFDictionarySetValue(__CFRunLoops, thread, loop);
}
// 5. 返回 RunLoop
return loop;
}
6. 总结对比
| 特性 | 主线程 (Main Thread) | 子线程 (Child Thread) |
|---|---|---|
| RunLoop 状态 | 默认已创建并启动 | 默认无 RunLoop |
| 创建时机 | App 启动时系统自动创建 | 第一次调用 currentRunLoop 时创建 |
| 启动方式 | 系统自动 [runloop run] |
开发者需手动调用 [runloop run] |
| 主要作用 | 处理 UI、点击、屏幕刷新 | 处理耗时操作、后台常驻任务 |
| 线程存活 | App 运行期间一直存活 | 任务结束即销毁(除非开启 RunLoop 保活) |
形象的比喻
- 线程 是一个 工人。
- RunLoop 是一个 流水线/任务管理系统。
- 主线程 是 工厂里的核心工人,他一上班,老板(系统)就给他配了一条流水线(RunLoop),让他一直盯着,有活干活,没活休息,不能回家。
- 子线程 是 临时工。默认情况下,临时工干完手里的活就直接下班回家了(线程销毁)。如果你想让临时工一直待在工厂里听候差遣,你就必须专门给他配一条流水线(手动创建并开启 RunLoop),告诉他:“没活的时候在旁边睡觉,别回家,有活我会叫醒你”。