文章目录
- RunLoop
- RunLoop 和 线程的关系
- 为什么 RunLoop 能让线程休眠
- RunLoop Mode (运行模式)
- Timer 在滚动的时候会失效
- NSRunLoopCommonModes
- RunLoop 的 Source
- Timer
- Observer
- RunLoop 一次完整运行过程
- RunLoop 和 线程保活
- 为什么必须要有 `addPort`?
- 没有 `addPort` 的情况:
- 有 `addPort` 的情况:
- RunLoop 与 performSelector
- 常见的几种形式:
- RunLoop 与 GCD 区别
RunLoop
RunLoop 本质上是一个事件循环机制, 让线程在没有任务是休眠, 有事件时被唤醒并处理事件
普通线程:
-(void)threadMain{NSLog(@"开始执行");// 执行完// 线程结束}如果希望线程一直存活:
while(YES){// 等待任务}这种方式会导致 CPU 空转。
RunLoop 解决的就是:
没有事件 ↓ 线程进入休眠 ↓ 有事件到来 ↓ 唤醒线程 ↓ 处理事件 ↓ 继续等待所以可以把它理解成:
RunLoop = 线程的“事件调度中心” + 休眠/唤醒机制
RunLooP作用
- 保持线程 (尤其是主线程) 持续运行, 不退出
- 处理 App 中的各种事件 (触摸, Timer, performSelector, GCD主队列任务, 界面刷新)
- 节省CPU 资源: 有事做事, 无事休眠
RunLoop 和 线程的关系
一个线程对应一个RunLoop, 但不是说每个线程启动后都有一个已经运行的RunLoop,
主线程:
NSRunLoop*runLoop=[NSRunLoop mainRunLoop];主线程的RunLoop会被系统自动启动
子线程则不同:
NSThread*thread=[[NSThread alloc]initWithBlock:^{NSLog(@"子线程");[[NSRunLoop currentRunLoop]run];}];[thread start];如果不调用:[[NSRunLoop currentRunLoop] run];,那么子线程执行完任务之后就结束了
- 主线程的 RunLoop 在App启动时自动创建并执行
- 子线程默认没有RunLoop, 需要手动获取并启动 (否则线程执行完成任务就退出)
为什么 RunLoop 能让线程休眠
底层并不是简单的:
while(1)而是:
RunLoop ↓ 检查有没有事件 ↓ 有事件 → 处理 ↓ 没事件 → 让线程进入休眠 ↓ Mach Port / Source / Timer 等事件到来 ↓ 系统唤醒线程所以 RunLoop 的核心价值之一就是:
避免线程在没有任务的时候持续占用 CPU。
RunLoop的核心组成: Mode, Source, Timer, Observer
RunLoop Mode (运行模式)
RunLoop 并不是把所有的事件全部一起处理, 它通过Mode对事件进行分类
Mode:
| Mode 名称 | 说明 |
|---|---|
| kCFRunLoopDefaultMode / NSDefaultRunLoopMode | 默认模式,App 平时运行在这里 |
| UITrackingRunLoopMode | 界面追踪模式(UIScrollView 滑动时切换到这个模式) |
| kCFRunLoopCommonModes / NSRunLoopCommonModes | 伪模式 / 标记,不是真实 Mode。默认包含 Default + Tracking |
| UIInitializationRunLoopMode | App 启动时的临时 Mode,启动完成后不再使用 |
| GSEventReceiveRunLoopMode | 系统内部接收事件用(一般碰不到) |
常见的 Mode 有:
NSDefaultRunLoopMode NSRunLoopCommonModes UITrackingRunLoopMode主线程的 RunLoop 是在 UIApplicationMain 内部自动创建并启动的 (最终调用CFRunLoop), 所以App可以一直运行
Timer 在滚动的时候会失效
例如:
[NSTimer scheduledTimerWithTimeInterval:1target:selfselector:@selector(test)userInfo:nil repeats:YES];默认Timer被加入NSDefaultRunLoopMode
那么这时候有一个TableView, 在滑动TableView的时候, Timer就会失效
这是因为在UITableView上滑动的时候:
RunLoop ↓ 进入 UITrackingRunLoopMode ↓ Default Mode 暂时不处理 ↓ Timer 不触发那么如何解决这个问题呢?
只需要将 Timer 加入到NSRunLoopCommonModes就可以解决这个问题
NSTimer*timer=[NSTimer timerWithTimeInterval:1target:selfselector:@selector(test)userInfo:nil repeats:YES];[[NSRunLoop mainRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];NSRunLoopCommonModes
NSRunLoopCommonModes不是一个独立运行的 Mode
它可以理解为 Mode集合, 包含多个Common Mode
当把 Timer 加入到NSRunLoopCommonModes, 就是相当于就把 Timer 添加到当前RunLoop标记为 Common 的那些 Mode 中
RunLoop 的 Source
Source 是 RunLoop 的事件来源
常见可以分为:
Source 0
需要手动唤醒/处理的事件源
例如:
performSelector这类机制可以和 Source 0 联系起来
Source 1
基于 Mach Port 等底层事件机制
例如:
触摸事件 IPC 系统事件可以粗略理解为:
Source 1 ↓ 底层事件到达 ↓ 唤醒 RunLoop ↓ 处理事件Timer
NSTimer也是RunLoop 的一个输入源, 这里需要注意的是: NSTimer 本身不是一个独立线程
例如:
NSTimer*timer=[NSTimer scheduledTimerWithTimeInterval:1target:selfselector:@selector(sayHello)userInfo:nil repeats:YES];Timer 是注册到 RunLoop 中
NSTimer ↓ RunLoop ↓ 时间到了 ↓ RunLoop 检查 ↓ 执行 selector如果 RunLoop 不运行, NSTimer 就不会正常触发
-(void)viewDidLoad{[superviewDidLoad];// 1. 创建一个 Timer,每隔 1 秒执行一次 tick 方法NSTimer*timer=[NSTimer scheduledTimerWithTimeInterval:1.0target:selfselector:@selector(tick)userInfo:nil repeats:YES];for(inti=0;i<5;i++){sleep(1);// 休眠 1 秒,模拟重度计算NSLog(@"正在计算... 第 %d 秒",i+1);}}-(void)tick{NSLog(@"定时器触发!");}打印:
正在计算... 第 1 秒 正在计算... 第 2 秒 正在计算... 第 3 秒 正在计算... 第 4 秒 正在计算... 第 5 秒 定时器触发从上面的代码运行结果可以观察到, 定时器事件在for循环结束之前没有触发
让我们看看 CPU 实际执行指令的顺序:
// ========== RunLoop 的伪代码 ========== function CFRunLoopRun() { while (true) { // ← RunLoop 的 while 循环 // 1. 检查 Timer // 2. 检查 Source // 3. 如果没有事件,休眠 // 4. 如果有事件,执行它 if (有事件需要处理) { // 这里会调用你的代码 [target performSelector:action]; // ← 在这里调用了 viewDidLoad // 你的代码执行完后,才继续下一轮循环 } } }Observer
Observer 用来监听 RunLoop 的状态变化
例如:
RunLoop 即将进入 ↓ 处理 Timer ↓ 处理 Source ↓ 即将休眠 ↓ 被唤醒 ↓ 退出 RunLoop可以通过CFRunLoopObserver监听
常见状态:
kCFRunLoopEntry kCFRunLoopBeforeTimers kCFRunLoopBeforeSources kCFRunLoopBeforeWaiting kCFRunLoopAfterWaiting kCFRunLoopExitRunLoop 一次完整运行过程
RunLoop 开始 ↓ BeforeTimers ↓ BeforeSources ↓ 处理 Timer / Source ↓ 是否还有事件? ↙ ↘ 有 没有 ↓ ↓ 继续 BeforeWaiting ↓ 休眠 ↓ 事件到达 / Timer 到期 ↓ AfterWaiting ↓ 处理事件 ↓ 循环可以把它浓缩成:
进入 ↓ 处理事件 ↓ 没有事件 ↓ 休眠 ↓ 被唤醒 ↓ 处理事件 ↓ 循环RunLoop 和 线程保活
比如创建一个常驻线程:
NSThread*thread=[[NSThread alloc]initWithBlock:^{[[NSRunLoop currentRunLoop]addPort:[NSPort port]forMode:NSDefaultRunLoopMode];[[NSRunLoop currentRunLoop]run];}];NSThread* thread = [[NSThread alloc] initWithBlock:^{ ... }];
- 作用: 创建一个新的子线程对象
initWithBlock: 告诉这个线程; “当启动时候,执行 Block 里面的代码”- 此时,线程还没有启动, 知识准别好了
[[NSRunLoop currentRunLoop]addPort:[NSPort port]forMode:NSDefaultRunLoopMode];[[NSRunLoop currentRunLoop]run];[NSRunLoop currentRunLoop]
- 获取当前线程的 RunLoop
- 因为这段代码写在 Block 里, 而 Block 会在子线程中执行, 所以这里拿到的是子线程的 RunLoop
[NSPort port]
- 创建一个 Port (端口)
- 可以把它理解成一个虚拟的 “信箱“, 有了这个信箱, RunLoop 就会认为: “我有东西要监听,不能退出”
add:forMode:
- 把上面的信箱加入到 RunLoop 中
- 作用: 给RunLoop添加一个永久有效的事件源, 让它有事可做, 不会空转退出
[thread start]
- 启动子线程
- 这时, 子线程开始执行 Block 里的代码
为什么必须要有addPort?
这是一个极其关键的问题。
没有addPort的情况:
[[NSRunLoop currentRunLoop] run];- RunLoop 启动后,会检查自己有没有事件源(比如 Timer、Port、Observer)。
- 如果什么都没有,RunLoop 会认为:“我没事可做,没必要继续跑。”
- 于是,RunLoop 立即退出,子线程执行完所有代码,线程被销毁。
有addPort的情况:
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode]; [[NSRunLoop currentRunLoop] run];- 添加 Port 后,RunLoop 发现:“我有一个 Port 要监听,我不能退出。”
- 于是,RunLoop 进入永久等待状态,线程永不销毁。
RunLoop 与 performSelector
performSelector是NSObject提供的一系列方法,允许你延迟执行或在指定线程上执行某个方法。
[selfperformSelector:@selector(test)onThread:thread withObject:nil waitUntilDone:NO];常见的几种形式:
| 方法 | 作用 |
|---|---|
[obj performSelector:@selector(method)] | 在当前线程立即执行 |
[obj performSelector:withObject:afterDelay:] | 延迟执行(依赖 RunLoop) |
[obj performSelector:onThread:withObject:waitUntilDone:] | 在指定线程执行 |
[obj performSelectorOnMainThread:withObject:waitUntilDone:] | 在主线程执行 |
这个机制和目标线程的RunLoop 有密切关系
主线程 ↓ performSelector ↓ 目标线程 RunLoop ↓ 唤醒 ↓ 执行 test如果创建了一个子线程:
NSThread*thread=...但是如果没有正确启动它的 RunLoop:
[[NSRunloop currentRunLoop]run];那么一些基于RunLoop的任务就有可能无法进行.
RunLoop 与 GCD 区别
GCD更偏向 任务调度
例如:
dispatch_async(dispatch_get_global_queue(0,0),^{// 后台任务});关注任务 -> Queue -> Thread
RunLoop 更偏向 事件循环
关注:事件 -> RunLoop -> 处理 -> 休眠 -> 唤醒
两者不是互相替代的关系 ,GCD 和 RunLoop 在iOS 系统中是协同工作的