RunLoop深度理解-面试必备
🔍 RunLoop的本质:一个"智能等待"的消息循环
想象一下,你是一个餐厅服务员:
- 有客人来:你忙着服务(处理事件)
- 没客人来:你站在那里发呆等待(浪费时间)
- RunLoop就是:一个聪明的服务员,他知道"没客人时去休息,但随时准备被叫醒"
🏗️ 核心架构深度剖析
1. RunLoop的三大核心组件
RunLoop = {
Input Sources(输入源): 触发RunLoop的事件源
Timers(定时器): 定时触发的事件
Observers(观察者): 监听RunLoop状态变化
}
Input Sources分为两种:
-
Source0:用户主动触发的事件(触摸、点击)
- 需要手动"叫醒"RunLoop
- 比如:按钮点击事件
-
Source1:系统自动触发的事件(网络、硬件)
- 可以自动"叫醒"RunLoop
- 比如:网络数据到达
2. RunLoop的"五种状态"生命周期
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = 1 << 0, // 1. 进入RunLoop
kCFRunLoopBeforeTimers = 1 << 1, // 2. 准备处理定时器
kCFRunLoopBeforeSources = 1 << 2, // 3. 准备处理输入源
kCFRunLoopBeforeWaiting = 1 << 3, // 4. 准备进入休眠
kCFRunLoopAfterWaiting = 1 << 4, // 5. 从休眠中唤醒
kCFRunLoopExit = 1 << 5, // 6. 退出RunLoop
};
🔄 RunLoop的完整运行流程(用代码说话)
// RunLoop的核心就是一个do-while循环
void CFRunLoopRun(void) {
do {
// 1️⃣ 通知:我要开始工作了
__CFRunLoopDoObservers(runLoop, kCFRunLoopEntry);
// 2️⃣ 检查定时器:有没有到点的闹钟?
__CFRunLoopDoTimers(runLoop);
// 3️⃣ 处理Source0:用户主动触发的事件(触摸、点击)
__CFRunLoopDoSources0(runLoop);
// 4️⃣ 通知:我要休息了,有事叫我
__CFRunLoopDoObservers(runLoop, kCFRunLoopBeforeWaiting);
// 5️⃣ 进入休眠:等待被唤醒(核心!节省CPU)
mach_msg(&msg, MACH_RCV_MSG, 0, sizeof(msg_buffer));
// 6️⃣ 被唤醒:有人叫我干活了!
__CFRunLoopDoObservers(runLoop, kCFRunLoopAfterWaiting);
// 7️⃣ 处理唤醒事件:看看是谁叫醒的我
if (是定时器唤醒的) {
__CFRunLoopDoTimers(runLoop);
} else if (是Source0事件) {
__CFRunLoopDoSources0(runLoop);
} else if (是Source1事件) {
__CFRunLoopDoSource1(runLoop);
}
} while (!停止条件);
}
🎯 Mode机制:RunLoop的"工作模式"
Mode就像手机的飞行模式:
- 不同场景:开会用静音,娱乐用正常
- RunLoop Mode:不同场景执行不同任务
系统预定义的Mode:
// 1. 默认模式:日常工作
NSString * const NSDefaultRunLoopMode = @"kCFRunLoopDefaultMode";
// 2. 跟踪模式:用户滑动时专用
NSString * const UITrackingRunLoopMode = @"UITrackingRunLoopMode";
// 3. 通用模式:Default + Tracking的组合
NSString * const NSRunLoopCommonModes = @"kCFRunLoopCommonModes";
Mode解决的核心问题
场景:滑动TableView时,定时器暂停的问题
// ❌ 问题代码:定时器在滑动时暂停
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:3.0
target:self
selector:@selector(timerAction)
userInfo:nil
repeats:YES];
// ✅ 解决方案:使用CommonModes
NSTimer *timer = [NSTimer timerWithTimeInterval:3.0
target:self
selector:@selector(timerAction)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
为什么这样就能解决问题?
- DefaultMode:日常工作模式
- TrackingMode:滑动时的专用模式
- CommonModes:包含Default和Tracking,定时器在这两种模式下都能运行
🚀 RunLoop在项目中的实际应用
1. 常驻线程(网络监听)
- (void)createNetworkThread {
NSThread *networkThread = [[NSThread alloc] initWithTarget:self
selector:@selector(networkLoop)
object:nil];
[networkThread start];
}
- (void)networkLoop {
@autoreleasepool {
// 创建RunLoop
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
// 添加端口,保持RunLoop运行(没有输入源RunLoop会立即退出)
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
// 启动RunLoop(这是一个死循环,直到线程结束)
[runLoop run];
}
}
2. 延迟执行的本质
// 这个方法底层就是用Timer实现的
[self performSelector:@selector(doSomething) withObject:nil afterDelay:2.0];
// 等价于:
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0
target:self
selector:@selector(doSomething)
userInfo:nil
repeats:NO];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
3. 屏幕刷新(CADisplayLink)
// CADisplayLink也是基于RunLoop的
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self
selector:@selector(refresh)];
[displayLink addToRunLoop:[NSRunLoop currentRunLoop]
forMode:NSDefaultRunLoopMode];
🤔 深入理解的关键问题
Q1: 为什么主线程不会死掉?
A: 因为主线程的RunLoop一直在运行,处理各种UI事件和用户交互。
Q2: 子线程默认没有RunLoop吗?
A: 是的,子线程需要手动创建RunLoop,否则执行完任务就销毁了。
Q3: RunLoop的休眠机制是什么?
A: 使用mach_msg()系统调用进入内核休眠,等待消息唤醒。这就是为什么RunLoop能节省CPU的原因。
Q4: Source0和Source1的本质区别?
A:
- Source0:需要"手动叫醒"(用户事件)
- Source1:能"自动叫醒"(系统事件)
Q5: RunLoop Observer有什么用?
A: 可以监听RunLoop的状态变化,用于性能监控、调试、自定义事件处理等。
💡 面试时的回答技巧
不要只是背概念,要能解释原理:
// 好的回答示例:
"RunLoop本质上是一个事件循环,它让线程在有任务时工作,无任务时休眠。
比如NSTimer为什么需要RunLoop?因为Timer的触发依赖RunLoop的循环检查。
又比如为什么滑动时Timer暂停?因为RunLoop切换到TrackingMode,而Timer默认在DefaultMode。"
关键是要理解:RunLoop不是什么高深的技术,它就是操作系统为了节省CPU资源而设计的一个智能等待机制。
📚 RunLoop面试高频问题详解
1. iOS 项目中有用到RunLoop的吗?举例
答案:是的,RunLoop在iOS开发中被广泛使用,虽然很多时候我们是间接使用的:
举例场景:
// 1. NSTimer的使用(最常见)
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:@selector(timerAction)
userInfo:nil
repeats:YES];
// 这其实是在主线程RunLoop中添加了Timer
// 2. performSelector:afterDelay:(延迟执行)
[self performSelector:@selector(doSomething) withObject:nil afterDelay:2.0];
// 底层通过Timer实现
// 3. 常驻线程(网络监听)
- (void)createNetworkThread {
NSThread *thread = [[NSThread alloc] initWithTarget:self
selector:@selector(networkTask)
object:nil];
[thread start];
}
- (void)networkTask {
@autoreleasepool {
// 创建RunLoop保持线程存活
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
// 添加端口保持RunLoop运行
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
// 4. 图片异步加载完成后更新UI
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 下载图片
UIImage *image = [self downloadImage];
// 回到主线程更新UI(主线程RunLoop处理)
dispatch_async(dispatch_get_main_queue(), ^{
self.imageView.image = image;
});
});
// 5. CADisplayLink(屏幕刷新)
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self
selector:@selector(displayLinkAction)];
[displayLink addToRunLoop:[NSRunLoop currentRunLoop]
forMode:NSDefaultRunLoopMode];
2. RunLoop内部实现逻辑?
RunLoop的核心是一个do-while循环,其内部实现逻辑如下:
// 伪代码表示RunLoop的核心循环
int32_t __CFRunLoopRun() {
do {
// 1. 通知观察者:即将进入RunLoop
__CFRunLoopDoObservers(runLoop, kCFRunLoopEntry);
// 2. 处理Timer(到期的定时器)
__CFRunLoopDoTimers(runLoop);
// 3. 处理Source0(非端口事件,如触摸事件)
__CFRunLoopDoSources0(runLoop);
// 4. 通知观察者:即将进入休眠
__CFRunLoopDoObservers(runLoop, kCFRunLoopBeforeWaiting);
// 5. 休眠等待(核心!节省CPU)
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer));
// 6. 被唤醒后通知观察者
__CFRunLoopDoObservers(runLoop, kCFRunLoopAfterWaiting);
// 7. 处理唤醒事件
if (msg_is_timer) {
__CFRunLoopDoTimers(runLoop);
} else if (msg_is_source) {
__CFRunLoopDoSources0(runLoop);
} else if (msg_is_source1) {
__CFRunLoopDoSource1(runLoop);
}
} while (!stop);
}
核心流程:
- 进入循环 → 通知Observers
- 处理Timers → 检查到期的定时器
- 处理Sources → 处理输入源事件
- 休眠等待 → 没有事件时进入休眠
- 唤醒处理 → 有事件时被唤醒处理
- 循环重复 → 回到步骤2
3. RunLoop和线程的关系?
答案:RunLoop与线程是一一对应的关系。
- 主线程:系统自动创建并启动RunLoop,无需手动管理
- 子线程:默认没有RunLoop,需要手动创建和启动
- 生命周期:RunLoop的生命周期与线程一致
- 管理方式:每个线程最多只能有一个RunLoop
// 主线程RunLoop(自动存在)
NSRunLoop *mainRunLoop = [NSRunLoop mainRunLoop];
// 子线程RunLoop(需要手动创建)
NSThread *thread = [[NSThread alloc] initWithTarget:self
selector:@selector(threadTask)
object:nil];
[thread start];
- (void)threadTask {
// 获取当前线程的RunLoop(自动创建)
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
// 添加输入源保持RunLoop运行
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
// 启动RunLoop
[runLoop run];
}
4. Timer与RunLoop的关系?
答案:Timer必须依赖RunLoop才能工作。
- 依赖关系:NSTimer需要添加到RunLoop中才能被调度执行
- 运行机制:RunLoop在每次循环中检查Timer是否到期
- 默认行为:scheduledTimerWithTimeInterval自动添加到当前RunLoop
// 方式1:自动添加到当前RunLoop
NSTimer *timer1 = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:@selector(timerAction)
userInfo:nil
repeats:YES];
// 方式2:手动添加到指定RunLoop
NSTimer *timer2 = [NSTimer timerWithTimeInterval:1.0
target:self
selector:@selector(timerAction)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer2 forMode:NSDefaultRunLoopMode];
5. 程序中添加每3秒响应一次的NSTimer,当拖动tableView时timer可能无法响应要怎么解决?
问题分析:拖动TableView时,RunLoop切换到UITrackingRunLoopMode,而Timer默认在NSDefaultRunLoopMode运行,导致暂停。
解决方案:将Timer添加到NSRunLoopCommonModes
// 问题代码(拖动时会暂停)
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:3.0
target:self
selector:@selector(timerAction)
userInfo:nil
repeats:YES];
// 解决方案
NSTimer *timer = [NSTimer timerWithTimeInterval:3.0
target:self
selector:@selector(timerAction)
userInfo:nil
repeats:YES];
// 添加到CommonModes,这样在DefaultMode和TrackingMode下都会执行
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
CommonModes包含:
- NSDefaultRunLoopMode(默认模式)
- UITrackingRunLoopMode(跟踪模式)
6. RunLoop是怎么响应用户操作的,具体流程是什么样的?
响应流程:
- 用户操作发生 → 触摸屏幕、点击按钮等
- 系统生成事件 → 转换为UIEvent对象
- 事件入队 → 加入到UIApplication的事件队列
- RunLoop唤醒 → Source1(端口事件)唤醒主线程RunLoop
- 事件分发 → UIApplication将事件分发给UIWindow
- 视图查找 → UIWindow找到最合适的UIView处理事件
- 事件传递 → 通过响应链(Responder Chain)传递
- 事件处理 → 目标对象处理事件(touchesBegan/Moved/Ended等)
- RunLoop继续 → 处理完后继续下一个循环
// 触摸事件的处理流程(简化)
触摸发生 → UIEvent创建 → Source1唤醒RunLoop →
UIApplication分发 → UIWindow查找 → UIView响应 →
touchesBegan/Moved/Ended调用 → 完成处理
关键点:
- Source1事件:系统事件可以自动唤醒RunLoop
- 主线程RunLoop:UI事件都在主线程处理
- 事件循环:每个事件处理完后,RunLoop继续等待下一个事件
7. 说说RunLoop的几种状态?
RunLoop有以下几种状态(通过CFRunLoopActivity枚举定义):
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入RunLoop
kCFRunLoopBeforeTimers = (1UL << 1), // 处理Timer前
kCFRunLoopBeforeSources = (1UL << 2), // 处理Source前
kCFRunLoopBeforeWaiting = (1UL << 3), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 4), // 从休眠中唤醒
kCFRunLoopExit = (1UL << 5), // 退出RunLoop
kCFRunLoopAllActivities = 0x0FFFFFFFU // 所有状态
};
状态详解:
- kCFRunLoopEntry:RunLoop开始运行
- kCFRunLoopBeforeTimers:处理Timer之前
- kCFRunLoopBeforeSources:处理Source之前
- kCFRunLoopBeforeWaiting:即将进入休眠,等待事件
- kCFRunLoopAfterWaiting:从休眠中被唤醒
- kCFRunLoopExit:RunLoop退出
8. RunLoop的Mode作用是什么?
Mode是RunLoop的核心机制,用于控制在不同场景下执行不同的任务。
系统预定义Mode:
- NSDefaultRunLoopMode(kCFRunLoopDefaultMode):默认模式
- UITrackingRunLoopMode:界面跟踪模式(滑动ScrollView时)
- NSRunLoopCommonModes:通用模式(Default+Tracking的组合)
Mode的作用:
- 任务隔离:不同Mode下任务互不干扰
- 优先级控制:特定场景优先执行特定任务
- 性能优化:滑动时暂停耗时操作,避免卡顿
- 事件过滤:只处理当前Mode相关的输入源
// Mode使用示例
// Timer默认在DefaultMode,滑动时暂停
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:@selector(updateUI)
userInfo:nil
repeats:YES];
// 滑动时切换到TrackingMode,只处理滑动相关事件
// Timer暂停执行,避免UI卡顿
// 解决方法:添加到CommonModes
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
面试时重点强调:
- Mode隔离机制保证滑动流畅性
- CommonModes解决跨场景任务执行
- 理解Mode是掌握RunLoop的关键
🎯 总结
RunLoop不是什么高深的技术,它就是操作系统为了节省CPU资源而设计的一个智能等待机制。通过让线程在有任务时忙碌,没有任务时休眠,RunLoop保证了应用的响应性和性能。
核心理解:
- RunLoop = 事件循环 + 智能休眠
- Mode = 场景隔离机制
- Source0/Source1 = 用户事件/系统事件
- Timer = 定时任务
- Observer = 状态监听
掌握RunLoop,就是掌握了iOS事件处理的核心机制!