RunLoop
面试回答版
一句话概括
RunLoop 是 iOS 基于 mach_msg 实现的线程事件循环机制,它可以管理输入源、定时器、观察者,并在没有事件时让线程进入休眠以节省 CPU。
RunLoop 与线程的关系
- 每个线程有且仅有一个 RunLoop(通过 TLS 线程局部存储绑定)
- 主线程的 RunLoop 在 UIApplicationMain 中自动创建并运行
- 子线程的 RunLoop 默认不创建,调用
[NSRunLoop currentRunLoop]时懒加载创建 - 子线程 RunLoop 需要手动
run才能启动,且需要 Source/Timer 维持 - RunLoop 不能直接创建,只能通过
CFRunLoopGetMain()/CFRunLoopGetCurrent()获取
五个核心类
+---------------------------+------------------------------------------+
| CFRunLoop | 事件循环本身,包含多个 Mode |
| CFRunLoopMode | 事件运行模式,一个 RunLoop 一次只能在一个 |
| | Mode 下运行 |
| CFRunLoopSource (source0 | 输入源:source0(手动触发) / source1 |
| / source1) | (基于 mach_port 内核消息唤醒) |
| CFRunLoopTimer | 定时器(NSTimer 的底层) |
| CFRunLoopObserver | 观察者,监听 RunLoop 各种状态变化 |
+---------------------------+------------------------------------------+
CFRunLoopMode
RunLoop 一次运行只能在一个 Mode 下。切换 Mode 时会退出当前循环,再进入新 Mode。
| Mode | 说明 |
|---|---|
kCFRunLoopDefaultMode | 默认 mode,平时在此 mode 下运行 |
UITrackingRunLoopMode | 滑动追踪 mode(UIScrollView 滚动时) |
kCFRunLoopCommonModes | 不是真正的 mode,是一个 Mode 集合标记 |
UIInitializationRunLoopMode | App 启动时进入,启动后不再使用 |
GSEventReceiveRunLoopMode | 图形事件接收 |
CommonModes:
kCFRunLoopCommonModes是一组 Mode 的标签- 默认包含
DefaultMode和TrackingMode - 添加到 CommonModes 的 source/timer 会在集合内的所有 mode 下执行
- 常用于解决 NSTimer 在滑动时不触发的问题
RunLoop 内部逻辑
CFRunLoopRun()
│
├─ 1. 通知 Observer:kCFRunLoopEntry(即将进入 RunLoop)
│
├─ 2. 通知 Observer:kCFRunLoopBeforeTimers(即将处理 Timer)
│
├─ 3. 通知 Observer:kCFRunLoopBeforeSources(即将处理 Source)
│
├─ 4. 执行 blocks(dispatch到当前 RunLoop 的 block)
│
├─ 5. 处理 Source0(非 mach_port 事件,需手动唤醒)
│ └─ 可能被 dispatch_async 到 main queue 的 block
│
├─ 6. 如果有 Source1 → 跳到步骤 9
│
├─ 7. 通知 Observer:kCFRunLoopBeforeWaiting(即将休眠)
│
├─ 8. 休眠等待 mach_msg(内核态等待)
│ ├─ Source1(mach_port 消息)唤醒
│ ├─ Timer 到时唤醒
│ ├─ 外部手动唤醒(CFRunLoopWakeUp)
│ └─ 超时
│
├─ 9. 通知 Observer:kCFRunLoopAfterWaiting(被唤醒)
│
├─ 10. 处理事件
│ ├─ Timer 到点 → 处理 Timer
│ ├─ Source1 到来 → dispatch 到 Source0 处理
│ └─ 手动唤醒 → 处理 block
│
├─ 11. 通知 Observer:kCFRunLoopExit(即将退出)
│
└─ 12. 循环回到步骤 2(除非被要求退出)
休眠机制
有事件 → 处理事件(用户态)
↓
无事件 → mach_msg 系统调用(用户态 → 内核态)
↓
内核态等待事件到来(线程休眠,不消耗 CPU)
↓
事件到达 → mach_msg 返回(内核态 → 用户态)
↓
处理事件
这是 RunLoop 省电的核心机制:没有事件时线程在内核态休眠,不消耗 CPU 时间片。
卡顿监控原理
// 使用 CFRunLoopObserver 监听 RunLoop 状态切换
// 当 BeforeSources 和 AfterWaiting 之间间隔超过阈值 → 卡顿
static void runLoopObserverCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
if (activity == kCFRunLoopBeforeSources || activity == kCFRunLoopAfterWaiting) {
// 记录时间戳
CFAbsoluteTime currentTime = CFAbsoluteTimeGetCurrent();
// 对比上次记录的 BeforeSources 时间
// 如果差 > 100ms → 卡顿,采集堆栈
}
}
// 在主线程 RunLoop 添加 observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(
kCFAllocatorDefault,
kCFRunLoopBeforeSources | kCFRunLoopAfterWaiting | kCFRunLoopBeforeWaiting,
YES, 0,
&runLoopObserverCallback,
&context
);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
RunLoop 与 autoreleasepool 的关系
RunLoop 循环周期:
1. objc_autoreleasePoolPush() ← 打一个哨兵
2. 处理事件:Timer、Source0、Source1
3. objc_autoreleasePoolPop(哨兵) ← 释放本周期产生的临时对象
4. 回到步骤 1
所以主线程 RunLoop 每次循环结束都会释放临时对象。
手势识别、dispatch block 等也会由各自的 autoreleasepool 包裹。
高频追问清单
| 问题 | 关键要点 |
|---|---|
| 主线程 RunLoop 为什么是开启的? | UIApplicationMain 内部调用了 CFRunLoopRun,默认在 DefaultMode 下运行 |
| 子线程如何保活? | 添加一个 port source / timer,然后 run。不添加 source 会在第一次 run 后立刻退出 |
| Timer 在滑动时不触发怎么办? | 加入 CommonModes([[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]) |
| Source0 和 Source1 的区别? | Source0 需手动唤醒(CFRunLoopSourceSignal + CFRunLoopWakeUp);Source1 基于 mach_port 内核自动唤醒 |
| RunLoop 如何节省能耗? | 通过 mach_msg 让线程进入内核态休眠,不消耗 CPU |
| performSelector:afterDelay: 的机制? | 创建 Timer 加到当前 RunLoop 的当前 Mode。当前 Mode 不是 DefaultMode 时不会触发 |
| 卡顿监控的原理? | Observer 监听状态切换,计算 BeforeSources 到 AfterWaiting 的间隔 |
项目落地版
场景 1:子线程保活(常驻线程)
@interface PermanentThread : NSObject
@property (nonatomic, strong) NSThread *thread;
- (void)executeTask:(void(^)(void))task;
@end
@implementation PermanentThread {
NSRunLoop *_runLoop;
}
- (instancetype)init {
if (self = [super init]) {
self.thread = [[NSThread alloc] initWithBlock:^{
// 添加 port 让 RunLoop 有事件源可处理
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
_runLoop = [NSRunLoop currentRunLoop]; // 持有引用用于后续操作
// 启动 RunLoop(永不超时)
while (YES) {
[_runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
}];
[self.thread start];
}
return self;
}
- (void)executeTask:(void(^)(void))task {
[self performSelector:@selector(performTask:)
onThread:self.thread
withObject:[task copy]
waitUntilDone:NO];
}
- (void)performTask:(void(^)(void))task {
task();
}
- (void)stop {
// 退出 RunLoop
CFRunLoopStop(CFRunLoopGetCurrent());
self.thread = nil;
}
@end
⚠️ 子线程保活需注意线程生命周期管理,避免创建过多常驻线程。
场景 2:卡顿监控器
final class LagMonitor {
static let shared = LagMonitor()
private var observing = false
private var lastTimestamp: CFAbsoluteTime = 0
func start() {
guard !observing else { return }
observing = true
let observer = CFRunLoopObserverCreateWithHandler(
kCFAllocatorDefault,
CFOptionFlags([.beforeSources, .afterWaiting, .beforeWaiting]),
true, 0
) { [weak self] _, activity in
guard let self else { return }
switch activity {
case .beforeSources, .afterWaiting:
self.lastTimestamp = CFAbsoluteTimeGetCurrent()
case .beforeWaiting:
// 即将休眠,检查卡顿
let now = CFAbsoluteTimeGetCurrent()
let diff = (now - self.lastTimestamp) * 1000
if diff > 100 {
// 卡顿超过 100ms,采集堆栈
print("[LagMonitor] 卡顿: \(diff)ms")
// BSBacktraceLogger 或 PLCrashReporter 采集堆栈
}
default:
break
}
}
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, .commonModes)
}
}
场景 3:NSTimer 在滑动时保持触发
// 问题:NSTimer 加到 DefaultMode,滑动时 Timer 不触发
// 修复方案 1:加入 CommonModes
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"timer fired");
}];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
// 修复方案 2:使用 dispatch_source 定时器(不受 RunLoop Mode 影响)
dispatch_source_t dispatchTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_set_timer(dispatchTimer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0);
dispatch_source_set_event_handler(dispatchTimer, ^{
NSLog(@"dispatch timer fired");
});
dispatch_resume(dispatchTimer);
场景 4:子线程 RunLoop 延迟执行
// 子线程正常执行 performSelector:afterDelay:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// ⚠️ 子线程 RunLoop 默认不开启,直接调 afterDelay 不会触发
[self performSelector:@selector(task) withObject:nil afterDelay:2.0];
// 需要手动启动 RunLoop
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3.0]];
});
场景 5:空闲任务调度(IDLE 时执行低优任务)
// 利用 RunLoop 空闲时执行非关键任务
static void addIdleTask(void (^task)(void)) {
CFRunLoopRef runLoop = CFRunLoopGetMain();
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(
kCFAllocatorDefault,
kCFRunLoopBeforeWaiting | kCFRunLoopExit,
YES, 0,
^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
task();
CFRunLoopObserverInvalidate(observer); // 执行一次后移除
CFRelease(observer);
}
);
CFRunLoopAddObserver(runLoop, observer, kCFRunLoopCommonModes);
}
学习路径与优先级
初级(P1)— 理解 RunLoop 基本概念
- 理解 RunLoop 是什么、主线程 RunLoop 如何启动
- 理解 Timer 添加到 RunLoop 的基本方式
- 知道滑动时 Timer 不触发的问题
- 知道
performSelector:afterDelay:依赖 RunLoop
自检:
- 解释为什么 NSTimer 在滑动时不准确
- 写出 Timer 添加到 CommonModes 的代码
中级(P0)— 掌握实践
- 理解 Mode 的概念和 CommonModes 的机制
- 能实现子线程保活
- 理解卡顿监控的底层原理和实现
- 理解 RunLoop 休眠机制(mach_msg)
- 能区分 Source0/Source1/Timer/Observer 的行为差异
动手实践:
- 写一个子线程保活的 PermanentThread
- 实现一个简单的卡顿监控器
- 验证 Timer 在不同 Mode 下的行为差异
高级(P1)— 深度应用
- 能设计基于 RunLoop 的空闲任务调度框架
- 理解 GCD 和 RunLoop 的关系(dispatch 到 main queue 的 block 如何被 RunLoop 处理)
- 能结合 RunLoop 设计性能监控体系
- 理解 CFRunLoop 源码的关键路径
实战项目:
- 实现一个卡顿堆栈采集 + 上报系统
- 设计 RunLoop 驱动的空闲任务执行器
- 分析 App 启动过程中 RunLoop 的状态变化