RunLoop初步了解
文章目录
- RunLoop初步了解
-
-
- UIApplication
- Mode与Item
- [Loop 机制](#Loop 机制)
- mach_msg
- AutoreleasePool
-
RunLoop是线程的管理者,让线程在有任务时高效工作,没有任务的时候进入内核休息,需要时能被硬件中断或端口消息立即唤醒
UIApplication
UIApplication 是 iOS 应用程序的"根对象",它本质上是一个单例类,继承自 UIResponder,负责管理和控制整个 App 的生命周期、事件循环和核心状态
objc
int UIApplicationMain(int argc, char *argv[], NSString *principalClassName, NSString *delegateClassName);
这个方法是UIKit提供的一个C函数,他做了五件事
- 创建UIApplication单例:整个 App 的根对象,全局唯一,负责管理 App 生命周期,分发系统级事件,管理所有 UIWindow
- 创建AppDelegate为代理
- 加载主界面
- 启动主线程RunLoop,在这之后主线程才变成了永不退出的事件循环
- 处理事件循环,RunLoop 启动后,所有的事情都由 RunLoop 驱动,触摸事件,Timer 回调,GCD 主队列任务
子线程的RunLoop是调用 [NSRunLoop currentRunLoop] 或 CFRunLoopGetCurrent() 时才会创建,如果从未调用过 CFRunLoopGetCurrent(),它执行完任务后立刻销毁,不会保活
RunLoop退出的条件:App退出,线程关闭,设置最大时间到期,ModeItem为空
Mode与Item
RunLoop 在 Core Foundation 层的结构如下
objc
struct __CFRunLoop {
pthread_t _pthread; // 绑定的线程
CFMutableSetRef _commonModes; // 通用模式名单
CFMutableSetRef _commonModeItems; // 通用事件仓库
CFRunLoopModeRef _currentMode; // 当前运行的 Mode
CFMutableSetRef _modes; // 所有 Mode
};
struct __CFRunLoopMode {
CFStringRef _name; // Mode 名字
CFMutableSetRef _sources0; // 手动事件源
CFMutableSetRef _sources1; // 内核事件源
CFMutableArrayRef _observers; // 观察者
CFMutableArrayRef _timers; // 定时器
pthread_mutex_t _lock; // 线程锁
};
Mode
RunLoop在同一时刻只能运行一种Mode,切换Mode只能退出RunLoop,Mode的意义就是隔离不同优先级的事件,只能添加不能删除,因为RunLoop同时指出里一个任务,所以遍历元素少效率高
类型
-
kCFRunLoopDefaultMode: 默认 mode,通常主线程在这个 Mode 下运行
-
UITrackingRunLoopMode: 追踪mode,保证Scrollview滑动顺畅不受其他 mode 影响
-
UIInitializationRunLoopMode: 启动程序后的过渡mode,启动完成后就不再使用
4: GSEventReceiveRunLoopMode: Graphic相关事件的mode,通常用不到
5: kCFRunLoopCommonModes: 占位mode,作为标记DefaultMode和CommonMode用
RunLoop 内部有一个非常重要的函数,叫 CFRunLoopRunSpecific(),它接受一个 Mode 参数。每一次 RunLoop 进入循环时,都会明确指定本次循环要跑哪个 Mode
当你滑动 UITableView 时,UIKit 会在底层调用 CFRunLoopRunInMode( UITrackingRunLoopMode, ...),强制 RunLoop 切换到 Tracking模式
开发者指定:你手动调用 [[NSRunLoop currentRunLoop] runMode:beforeDate:]时,你传什么 Mode,它就只跑什么 Mode
当然自定义Mode的情况极为少见,一般只用系统给的Mode
Item
作为Mode里的具体任务,每个Mode里都有四个集合存放具体的Item
| Item 类型 | 作用 | 典型例子 |
|---|---|---|
| Source0 | 手动事件源,不唤醒 RunLoop | performSelector:、UI 事件分发 |
| Source1 | 内核事件源,能主动唤醒 RunLoop | 触摸事件、GCD 主队列回调 |
| Timer | 定时器 | NSTimer、performSelector:afterDelay: |
| Observer | 观察者,监听 RunLoop 状态 | 自动释放池释放、卡顿监控 |
objc
// Source0:没有 port
CFRunLoopSource {order = ..., {callout = ...}}
// Source1:拥有 port
CFRunLoopSource {order = ..., {port = ..., callout = ...}}
- Source
Source0:没有 port,无法监听内核信号,只能由 App 内部手动触发(CFRunLoopSourceSignal + CFRunLoopWakeUp)
Source1:拥有 mach_port_t 端口,可以监听内核消息,具备主动唤醒 RunLoop 的能力
一般来说就是Source1唤醒RunLoop,然后触发后面多个Source0,让它去完成任务
- Timer
objc
struct __CFRunLoopTimer {
uint64_t _fireTSR; // 绝对触发时间(基于 mach_absolute_time 的纳秒数)
CFTimeInterval _interval; // 重复间隔,0 表示只触发一次
CFIndex _order; // 优先级
CFRunLoopTimerCallBack _callout; // 回调函数指针
};
首先调用 addTimer:forMode:将你想定的时间点塞进该 Mode 的 _timers 数组,数组按 _fireTSR(触发时间)升序排列,最近的排最前面,RunLoop开始循环时,首先会告诉Observer要开始检查Timer了,但是不处理Timer,先处理的是Source0,知道任务完成,再看Timer,看自己能睡多久(mach_msg),差值就是现在的时间距离Timer的差,然后该Timer被移除
有Timer内核睡到 Timer 触发时间,超时返回,RunLoop 醒来执行 Timer 回调。
没有Timer就是RunLoop 无限期休眠,只能靠硬件中断(如触摸屏幕)或其他线程调用 CFRunLoopWakeUp*来唤醒
如果下一次Timer到了,上一个任务还没被完成,那么这个任务完成后就去完成被耽搁的任务,卡顿不可避免,RunLoop是单线程串行的,当前任务不执行完,RunLoop 根本没有机会回到循环顶部去检查 _timers 数组
- Observer
objc
// Observer:order(优先级),activity(监听状态),callout(回调函数)
CFRunLoopObserver {order = ..., activities = ..., callout = ...}
Observer 监听 RunLoop 的 6 种状态变化:
| 状态常量 | 含义 |
|---|---|
kCFRunLoopEntry |
即将进入 RunLoop |
kCFRunLoopBeforeTimers |
即将处理 Timer |
kCFRunLoopBeforeSources |
即将处理 Source |
kCFRunLoopBeforeWaiting |
即将休眠(自动释放池在这里释放) |
kCFRunLoopAfterWaiting |
刚被唤醒 |
kCFRunLoopExit |
即将退出 |
Loop 机制
RunLoop 的 Loop 机制不是简单的 while(1),而是一个精密的任务处理 + 内核休眠的完整周期,每次循环都经历以下5个步骤:
- 准备(通知观察者)
c
// 步骤 1:通知 Observer,即将进入 Loop
__CFRunLoopDoObservers(rl, kCFRunLoopEntry);
- 干活(处理所有待办任务)
c
// 步骤 2:通知 Observer,即将处理 Timer
__CFRunLoopDoObservers(rl, kCFRunLoopBeforeTimers);
// 步骤 3:通知 Observer,即将处理 Source0
__CFRunLoopDoObservers(rl, kCFRunLoopBeforeSources);
// 步骤 4:执行被添加到 RunLoop 的 Blocks
__CFRunLoopDoBlocks(rl);
// 步骤 5:处理所有 Source0(手动触发的事件)
__CFRunLoopDoSources0(rl);
// 步骤 6:再次执行 Blocks
__CFRunLoopDoBlocks(rl);
// 步骤 7:检查是否有 Source1 就绪(有的话跳转到步骤 9)
- 睡觉(进入内核休眠)
c
// 步骤 8:★ 核心休眠 ★
// 8.1 通知 Observer,即将休眠
__CFRunLoopDoObservers(rl, kCFRunLoopBeforeWaiting);
// 8.2 计算休眠时间:找出最近要触发的 Timer
CFTimeInterval timeout = 最近Timer的触发时间 - 当前时间;
// 8.3 调用 mach_msg,线程挂起
mach_msg(..., timeout);
线程从这里开始彻底休眠,CPU 占用率为 0
内核接管计时任务,靠硬件时钟中断或硬件输入中断来唤醒线程
如果没有 Timer,timeout 为无穷大,线程永远等待触摸事件
- 醒来(被唤醒)
c
// 步骤 9:mach_msg 返回,线程被唤醒
// 9.1 通知 Observer,刚被唤醒
__CFRunLoopDoObservers(rl, kCFRunLoopAfterWaiting);
// 9.2 判断唤醒原因
if (超时了) {
// 执行到点的 Timer 回调
__CFRunLoopDoTimers(rl);
} else if (收到 Source1 消息) {
// 处理端口消息(触摸事件、GCD 主队列回调)
__CFRunLoopDoSource1(rl);
}
- 决策(是否继续循环)
c
// 步骤 10:检查退出条件
if (超时 || 被停止 || Mode为空) {
__CFRunLoopDoObservers(rl, kCFRunLoopExit);
return; // 退出循环
} else {
// 回到步骤 1,继续下一轮循环
}
mach_msg
RunLoop 的休眠不是 sleep(),而是通过 XNU 内核的 mach_msg() 系统调用实现的:
c
mach_msg_return_t mach_msg(
mach_msg_header_t *msg,
mach_msg_option_t option, // MACH_RCV_MSG(接收消息)
mach_msg_size_t send_size, // 0
mach_msg_size_t rcv_size,
mach_port_t rcv_name, // 当前线程的端口
mach_msg_timeout_t timeout, // 最大等待时间
mach_port_t notify
);
当 RunLoop 调用 mach_msg(..., timeout = 2.0 秒) 时:
- 触发
mach_msg_trap系统调用,从用户态陷入内核态 - 内核将线程从"运行队列"移到"等待队列",保存上下文
- CPU 释放,该线程不再占用任何 CPU 时间片,CPU 占用率为 0%
- 等待唤醒:硬件中断(触摸)、超时(Timer 到点)或其他线程调用
CFRunLoopWakeUp mach_msg返回,线程回到用户态,RunLoop 继续执行
AutoreleasePool
自动释放池也和RunLoop有关,在 ARC 时代,大部分内存管理由编译器自动完成,但对象的释放时机不是即时的
objc
- (void)viewDidLoad {
[super viewDidLoad];
for (int i = 0; i < 100000; i++) {
NSString *str = [NSString stringWithFormat:@"第 %d 条", i];
// str 什么时候释放?
}
}
如果每创建一个临时对象就立即释放,会带来两个问题:频繁释放导致性能开销大,且有些对象需要在当前作用域结束后才释放。AutoreleasePool 的作用就是将对象的释放延迟到池子销毁时统一进行
@autoreleasepool {
NSString *str = [[NSString alloc] initWithFormat:@"Hello"];
// str 被加入当前的 AutoreleasePool
}
// 池子销毁,str 的引用计数 -1,如果为 0 则释放
当调用 [obj autorelease] 时,会把对象加入当前线程栈顶的 Pool,Pool 销毁时,遍历列表,对每个对象发送 release
与RunLoop的关系
AutoreleasePool 的释放完全依赖 RunLoop 的 Observer 机制,主线程 RunLoop 中注册了两个 Observer,第一个 Observer 监听的是 kCFRunLoopEntry(RunLoop 刚启动),第二个监听 kCFRunLoopBeforeWaiting(即将休眠)和kCFRunLoopExit(退出 RunLoop)
objc
Observer 回调 (kCFRunLoopBeforeWaiting) {
// 1. 释放当前的 AutoreleasePool
_objc_autoreleasePoolPop(当前池);
// 2. 创建新的 AutoreleasePool
_objc_autoreleasePoolPush();
}
主线程中不需要手动写 @autoreleasepool,因为 RunLoop 通过 Observer 帮你自动管理了,每次 RunLoop 循环结束(即将休眠)时,系统会自动释放当前池并创建新池,确保临时对象不会无限堆积