RunLoop 是 iOS 系统中非常重要的一个概念,它是事件处理的核心机制之一。在 iOS 应用程序中,每个线程都有一个与之关联的 RunLoop 对象,它主要负责管理这个线程所需要执行的任务、消息和事件,并且在必要时切换线程状态,使得线程可以进入休眠或者唤醒。
什么是 Run Loop
Run Loop 是一个事件处理循环,用于接收和处理来自输入源(如触摸、定时器、网络连接等)的事件。简单来说,它是一个不断循环的过程,直到所有事件都被处理完成或者强制停止。
我们可以将 Run Loop 理解为一个邮递员,不断地检查信箱里是否有新的信件,如果有就派送给相应的收件人,并且在空闲时间里休息一下。
Run Loop 的实现原理
在iOS系统中,每个线程都有一个与之关联的 Run Loop 对象。这个对象维护了该线程需要处理的事件和任务,并且提供了一个接口,用于启动和停止循环。
当我们调用 UIApplicationMain
函数来启动一个应用程序时,系统会自动创建一个主线程(也就是 UI 线程),并在其中创建一个 Run Loop 对象。这个 Run Loop 进入一个无限循环,等待事件的发生。
当事件发生时(比如用户点击了屏幕),系统会将事件放入当前线程的 Run Loop 中,并唤醒 Run Loop,从而开始处理事件。处理完事件后,Run Loop 又会进入休眠状态,等待下一个事件的发生。
RunLoop可以注册多种输入源,如:
- Timer Source: 定时器
- Dispatch Source: GCD任务
- Port Based Source: 端口,用于进程间通信
- Custom Input Sources: 自定义输入源
当某个输入源有事件产生时,RunLoop 会将其放入一个指定的 Mode 下的 Events Queue 中。RunLoop 会按照顺序依次处理此 queue 中的事件,直到 queue 中没有事件为止。
RunLoop 的运行模式
在处理事件时,RunLoop 需要知道哪些事件需要处理,哪些事件不需要处理。为了达到这个目的,RunLoop 引入了 Mode 的概念。Mode 是一个字符串,用于标识一组事件。在一个特定的 Mode 下,RunLoop 只会处理该 Mode 中注册的事件,而忽略其他 Mode 中的事件。iOS 系统中默认提供了五种运行模式:
- NSDefaultRunLoopMode:默认模式,处理大部分事件源。
- UITrackingRunLoopMode:界面跟踪模式,用于拖动 ScrollView 时接收触摸事件,其他输入源将被暂停处理。
- UIInitializationRunLoopMode:启动模式,应用启动时使用,只处理特定事件。
- NSRunLoopCommonModes:公共模式,同时处理多个模式下的事件源。
- NSConnectionReplyMode:用于处理分布式对象连接的模式。
RunLoop 支持多个不同的 Mode 同时存在,当一个 Mode 中没有任何事件需要处理时,RunLoop 会自动切换到另一个 Mode 中,从而避免阻塞线程。
Run Loop 的内部结构
Run Loop 内部包含以下几个重要的数据结构:
- Mode:表示 Run Loop 当前正在处理的事件模式。
- Source:表示 Run Loop 注册的输入源集合。
- Observer:表示 Run Loop 注册的观察者集合。
- Timer:表示 Run Loop 注册的定时器集合。
- Activity:表示 Run Loop 的状态信息,包括是否处于活动状态、是否被暂停、是否正在等待睡眠等。
RunLoop 的内部结构可以类比为一个状态机。在不同的状态下,RunLoop 执行不同的操作,从而实现事件的处理和线程的管理。
Run Loop 的状态转换
RunLoop 可以处于以下几种状态:
- Inactive:表示 Run Loop 未启动或已停止。
- Waiting:表示 Run Loop 正在等待输入源的唤醒。
- Running:表示 Run Loop 正在运行,处理事件队列中的事件。
- Stopped:表示 Run Loop 已经停止,不再接收新的事件。
RunLoop 的生命周期
RunLoop 的生命周期主要包含以下几个阶段:
创建阶段
RunLoop 对象的创建是在线程第一次获取 RunLoop 时完成的,如果没有获取过就会创建一个默认的 RunLoop。RunLoop 的创建是由 CFRunLoopGetMain() 或 CFRunLoopGetCurrent() 函数实现的,它们返回的对象都可以被称为 RunLoop,但实际上主线程的 RunLoop 和其他线程的 RunLoop 是不同的。
RunLoop 的创建过程会生成一个 CFRunLoopRef 实例。CFRunLoopRef 包含了 RunLoop 的所有信息,例如输入源和定时源等。RunLoop 的初始化方法是 CFRunLoopRun(),它会一直运行,直到 RunLoop 被停止。
运行阶段
RunLoop 进入运行状态后,会不停地从消息队列中获取消息,并且传递给相应的处理器进行处理。当 RunLoop 处理完所有任务后,会进入休眠状态。
RunLoop 的运行方法有两种:CFRunLoopRun() 和 CFRunLoopRunInMode()。CFRunLoopRun() 是一个无限循环的函数,直到 RunLoop 被停止才会返回。CFRunLoopRunInMode() 则是一个带有模式参数的函数,只有在指定的模式下才会处理消息,其他消息会被暂时忽略。
下面的代码中,手动创建了一个后台线程,并且在其中创建了一个 RunLoop 对象。我们通过添加一个定时器事件源来模拟后台任务的执行,并且在主线程中设置 shouldStop 标志位来控制 RunLoop 的结束。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)startBackgroundTask {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//创建 RunLoop 对象
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
//添加一个定时器事件源
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(backgroundTask:) userInfo:nil repeats:YES];
[runLoop addTimer:timer forMode:NSDefaultRunLoopMode];
//启动 RunLoop,直到被停止
while (!self.shouldStop) {
[runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
});
}
- (void)backgroundTask:(NSTimer *)timer {
//执行后台任务,并根据需要更新 UI 界面
}
休眠阶段
如果当前线程没有任务需要执行,那么 RunLoop 就会进入休眠状态,直到有新的任务被加入到消息队列中才会重新唤醒。RunLoop 的休眠方法是 CFRunLoopRunInMode(),我们可以设置超时时间来避免 RunLoop 永久休眠。
停止阶段
RunLoop 可以手动停止,也可以由于某些异常情况导致自动停止。RunLoop 停止的方法是 CFRunLoopStop(),它可以立即停止 RunLoop 的运行,并且退出当前的 CFRunLoopRun() 或 CFRunLoopRunInMode() 函数调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
- (void)startObservingRunLoop {
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
//创建一个 Observer 对象,监听 RunLoop 的 Entry、BeforeWaiting 和 Exit 事件
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault,
kCFRunLoopEntry | kCFRunLoopBeforeWaiting | kCFRunLoopExit,
true,
0,
^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
NSLog(@"RunLoop is %s", [self stringFromRunLoopActivity:activity]);
});
//将 Observer 添加到当前 RunLoop 中
CFRunLoopAddObserver(runLoop, observer, kCFRunLoopCommonModes);
//释放 Observer 对象
CFRelease(observer);
}
- (NSString *)stringFromRunLoopActivity:(CFRunLoopActivity)activity {
switch (activity) {
case kCFRunLoopEntry:
return @"Entry";
case kCFRunLoopBeforeTimers:
return @"Before Timers";
case kCFRunLoopBeforeSources:
return @"Before Sources";
case kCFRunLoopBeforeWaiting:
return @"Before Waiting";
case kCFRunLoopAfterWaiting:
return @"After Waiting";
case kCFRunLoopExit:
return @"Exit";
default:
return @"Unknown";
}
}
RunLoop 的使用场景
RunLoop 可以在许多情况下提高程序的性能和响应速度,以下是常见的 RunLoop 使用场景:
- 处理异步任务
当需要在后台执行一些比较耗时的任务时,我们可以在其他线程上手动创建一个 RunLoop 对象,并且在任务执行结束后关闭它,以保证线程不会因为长时间阻塞而导致无法响应其他事件。这种方式可以避免使用 GCD 等异步执行方法时可能出现的问题,例如任务执行顺序不可控、不能中途取消任务等。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
- (void)startBackgroundTask {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//创建 RunLoop 对象
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
//添加一个端口输入源
NSMachPort *port = [NSMachPort port];
[port setDelegate:self];
[runLoop addPort:port forMode:NSDefaultRunLoopMode];
//执行后台任务
[self doBackgroundTask];
//停止 RunLoop
CFRunLoopStop([runLoop getCFRunLoop]);
});
}
- (void)doBackgroundTask {
//执行后台任务,并根据需要更新 UI 界面
}
- (void)handleMachMessage:(void *)msg {
//处理从主线程发送的消息
}
- 控制下载速度
当需要控制下载速度时,可以通过定时器来限制每次下载的数据量,并且在下载完成后休眠一段时间再继续下载。这样可以避免过多的网络请求和数据传输占用系统资源,提高下载效率和稳定性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (void)startDownloadTask {
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
config.timeoutIntervalForRequest = 10.0;
NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://example.com"]];
NSURLSessionDataTask *task = [session dataTaskWithRequest:request];
//启动下载任务,并且在 RunLoop 中控制下载速度
[task resume];
[self delayWithTimeInterval:0.5 block:^{
[task cancel];
}];
}
- (void)delayWithTimeInterval:(NSTimeInterval)interval block:(dispatch_block_t)block {
//创建定时器事件源,并且添加到当前线程的 RunLoop 中
NSRunLoopTimer *timer = [[NSRunLoop currentRunLoop] timerWithTimeInterval:interval repeats:NO block:^(NSTimer * _Nonnull timer) {
if (block) {
block();
}
}];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
}
- UI 渲染
UIKit 中大量的 UI 操作都是通过 RunLoop 来实现的。当界面需要更新时,RunLoop 会将相关的刷新操作加入到消息队列中,并且在下一次循环中执行,从而保证界面的流畅性。如果没有使用 RunLoop,那么界面更新可能会出现卡顿甚至崩溃的情况。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)startRenderingTask {
dispatch_async(dispatch_get_main_queue(), ^{
//获取当前 RunLoop 对象
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
//添加一个定时器事件源,每隔 1/60 秒触发一次
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(renderingTask:)];
[displayLink addToRunLoop:runLoop forMode:NSRunLoopCommonModes];
//启动 RunLoop
[runLoop runUntilDate:[NSDate distantFuture]];
});
}
- (void)renderingTask:(CADisplayLink *)displayLink {
//执行 UI 渲染操作
}