RunLoop的概念
一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线程能随时处理事件但并不退出,这就是runloop做的事。在iOS应用中,程序就是运行在一个主线程的runloop中,所以应用才会一直不断的运行。
一、RunLoop的基础知识
RunLoop的结构
一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。 每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。
CFRunLoopMode 和 CFRunLoop 的结构大致如下:
struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
};
struct __CFRunLoop {
CFMutableSetRef _commonModes; // Set
CFMutableSetRef _commonModeItems; // Set
这里有个概念叫 “CommonModes”:一个 Mode 可以将自己标记为”Common”属性(通过将其 ModeName 添加到 RunLoop 的 “commonModes” 中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 “Common” 标记的所有Mode里。
如:
主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultMode 和UITrackingRunLoopMode。这两个 Mode 都已经被标记为”Common”属性。DefaultMode 是 App 平时所处的状态,TrackingRunLoopMode 是追踪 ScrollView 滑动时的状态。当你创建一个 Timer 并加到 DefaultMode 时,Timer 会得到重复回调,但此时滑动一个TableView时,RunLoop 会将 mode 切换为 TrackingRunLoopMode,这时 Timer 就不会被回调,并且也不会影响到滑动操作。因为当你滑动时TrackingRunLoopMode追踪的事件加入到_commonModeItems,而RunLoop又会将该事件同步到具有 “Common” 标记的所有Mode(kCFRunLoopDefaultMode)里,所以此时响应了追踪事件而放弃了Timer的调用。
RunLoop与线程的关系
苹果不允许直接创建 RunLoop,它只提供了两个自动获取的函数:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。 这两个函数内部的逻辑大概是下面这样:(不想看代码的话可以跳过,记住下面的结论就好)
/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 访问 loopsDic 时的锁
static CFSpinLock_t loopsLock;
/// 获取一个 pthread 对应的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
OSSpinLockLock(&loopsLock);
if (!loopsDic) {
// 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。
loopsDic = CFDictionaryCreateMutable();
CFRunLoopRef mainLoop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
}
/// 直接从 Dictionary 里获取。
CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
if (!loop) {
/// 取不到时,创建一个
loop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, thread, loop);
/// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
_CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
}
OSSpinLockUnLock(&loopsLock);
return loop;
}
CFRunLoopRef CFRunLoopGetMain() {
return _CFRunLoopGet(pthread_main_thread_np());
}
CFRunLoopRef CFRunLoopGetCurrent() {
return _CFRunLoopGet(pthread_self());
}
从上面的代码可以看出,线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。
有了上面对runloop的基本了解,我们再来具体看看runloop相关的函数
1、Runloop相关函数
RunLoop运行接口:
- NSRunLoop的运行接口:
//运行 NSRunLoop,运行模式为默认的NSDefaultRunLoopMode模式,没有超时限制
- (void)run;
//运行 NSRunLoop: 参数为运时间期限,运行模式为默认的NSDefaultRunLoopMode模式
- (void)runUntilDate:(NSDate *)limitDate;
//运行 NSRunLoop: 参数为运行模式、时间期限,返回值为YES表示是处理事件后返回的,NO表示是超时或者停止运行导致返回的
- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;
- CFRunLoopRef的运行接口:
//运行 CFRunLoopRef
void CFRunLoopRun();
//运行 CFRunLoopRef: 参数为运行模式、时间和是否在处理Input Source后退出标志,返回值是exit原因
SInt32 CFRunLoopRunInMode (mode, seconds, returnAfterSourceHandled);
//停止运行 CFRunLoopRef
void CFRunLoopStop( CFRunLoopRef rl );
//唤醒CFRunLoopRef
void CFRunLoopWakeUp ( CFRunLoopRef rl );
先来讲讲NSRunLoop的这三个函数:
1、
- (void)run; 无条件运行
这个函数一般是不建议使用的,因为它会导致RunLoop永久的运行在NSDefaultRunLoopMode模式,即使CFRunLoopStop(runloopRef)
也无法停止runloop的运行,除非能移除这个runloop上的所有事件源,包括定时器和source事件,不然这个子线程就无法停止,只能永久运行下去。
2、
- (void)runUntilDate:(NSDate *)limitDate; 有一个超时时间限制
有个超时时间,可以控制每次runloop的运行时间,也是运行在NSDefaultRunLoopMode模式。但是CFRunLoopStop(runloopRef)
也无法停止runloop的运行。
3、
//有一个超时时间限制,而且设置运行模式
- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;
这里不仅可以设置超时时间,还有可以设置运行模式。这种运行方式可以被CFRunLoopStop(runloopRef)
所停止。
查看 run 方法的文档还可以知道,它的本质就是无限调用 runMode:beforeDate: 方法,同样地,runUntilDate: 也会重复调用 runMode:beforeDate:,区别在于它超时后就不会再调用。
总结来说,runMode:beforeDate: 表示的是 runloop 的单次调用,另外两者则是循环调用。
这里有一点东西需要讲解(下面分割线里的内容),涉及到runloop的一些源码知识,如果没有精力的可以跳过不看,想深入探讨的可以看一看
这里第三种运行方式除了limitDate超时和CFRunLoopStop(runloopRef)能够使runloop退出外,其它事件也可以导致runloop退出,举个例子:
- (void)test
{
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"runloop线程开始");
//获取到当前线程
self.thread = [NSThread currentThread];
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
//添加一个Port,为了防止runloop没事干直接退出
[runloop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
//运行一个runloop
[runloop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
NSLog(@"runloop线程结束");
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//在我们开启的异步线程调用方法,该方法在runloop的线程中执行
[self performSelector:@selector(run) onThread:self.thread withObject:nil waitUntilDone:NO];
});
}
- (void)run
{
NSLog(@"run in runloop thread");
}
输出如下:
2017-09-15 20:21:23.386 test[5829:32394503] runloop线程开始
2017-09-15 20:21:25.387 test[5829:32394503] run in runloop thread
2017-09-15 20:21:25.388 test[5829:32394503] runloop线程结束
可以看到,runloop收到一个“run”的消息,然后runloop处理完这个消息后就退出了,为什么会这样呢?看看runloop的源代码就知道了
/// RunLoop的实现
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
/// 首先根据modeName找到对应mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
/// 如果mode里没有source/timer/observer, 直接返回。
if (__CFRunLoopModeIsEmpty(currentMode)) return;
/// 1. 通知 Observers: RunLoop 即将进入 loop。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
/// 内部函数,进入loop
__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
Boolean sourceHandledThisLoop = NO;
int retVal = 0;
do {
/// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
/// 执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
/// 4. RunLoop 触发 Source0 (非port) 回调。
sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
/// 执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
/// 5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
if (__Source0DidDispatchPortLastTime) {
Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
if (hasMsg) goto handle_msg;
}
/// 6.通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
if (!sourceHandledThisLoop) {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}
/// 7. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
/// ? 一个基于 port 的Source 的事件。
/// ? 一个 Timer 到时间了
/// ? RunLoop 自身的超时时间到了
/// ? 被其他什么调用者手动唤醒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
}
/// 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
/// 9.收到消息,处理消息。
handle_msg:
/// 10.1 如果一个 Timer 到时间了,触发这个Timer的回调。
if (msg_is_timer) {
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
}
/// 10.2 如果有dispatch到main_queue的block,执行block。
else if (msg_is_dispatch) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}
/// 10.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件
else {
CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
if (sourceHandledThisLoop) {
mach_msg(reply, MACH_SEND_MSG, reply);
}
}
/// 执行加入到Loop的block
__CFRunLoopDoBlocks(runloop, currentMode);
if (sourceHandledThisLoop && stopAfterHandle) {
/// 进入loop时参数说处理完事件就返回。
retVal = kCFRunLoopRunHandledSource;
} else if (timeout) {
/// 超出传入参数标记的超时时间了
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(runloop)) {
/// 被外部调用者强制停止了
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
/// source/timer/observer一个都没有了
retVal = kCFRunLoopRunFinished;
}
/// 如果没超时,mode里没空,loop也没被停止,那继续loop。
} while (retVal == 0);
}
/// 11. 通知 Observers: RunLoop 即将退出。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}
RunLoop的运行逻辑:
上面的代码逻辑注释中已经解释得很清楚了,我就不再多说。可以知道我上面的例子中runloop就是收到了一个source1的消息,所以直接从5跳转到了9。然后10.3处理这个Source1处理完后会吧sourceHandledThisLoop这个变量赋值为true,注意后面判断是否退出runloopif (sourceHandledThisLoop && stopAfterHandle) { /// 进入loop时参数说处理完事件就返回。 retVal = kCFRunLoopRunHandledSource; }
满足了sourceHandledThisLoop为true,还有一个变量stopAfterHandle就是在- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;
运行runloop的时候已经将它设置为true了,所以到这里runloop就退出了。
OK,分析到这我们就明确知道runloop退出的条件了。
再看看CFRunLoopRef这里的几个函数:
1、
void CFRunLoopRun();
运行在默认的kCFRunLoopDefaultMode模式下,直到使用CFRunLoopStop接口停止这个Run Loop,或者Run Loop的所有事件源都被删除。
2、
SInt32 CFRunLoopRunInMode (mode, seconds, returnAfterSourceHandled);
根据前面的经验看参数名就知道这些参数的作用,这里说说最后一个参数和返回值。前面分割线的内容我们已经分析了runloop的源码,其实- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;
这个函数就是根据这个函数实现的,这就是为什么它可以通过CFRunLoopStop来停止runloop的原因,第三个参数就是设置源码中returnAfterSourceHandled参数的,- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;
这个函数默认就是把它设置成了YES。这个返回值也就是前面源码中retVal变量返回的值。有返回值就说明runloop已经结束运行。
OK,以上就是使runloop运行的全部方法。
二、RunLoop的应用
通过Runloop使线程保活:
以AF2.x的源代码为例:
+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
这是最runloop最基础的一个应用。这里创建了一个线程,取名为AFNetworking,然后添加了一个runloop,所以这个线程不会被销毁,直到runloop停止。
基于runloop的线程通信:
- 1、Cocoa 执行 Selector 的源:
[self performSelectorOnMainThread:<#(nonnull SEL)#> withObject:<#(nullable id)#> waitUntilDone:<#(BOOL)#>]
[self performSelectorOnMainThread:<#(nonnull SEL)#> withObject:<#(nullable id)#> waitUntilDone:<#(BOOL)#> modes:<#(nullable NSArray *)#>]
[self performSelector:<#(nonnull SEL)#> onThread:<#(nonnull NSThread *)#> withObject:<#(nullable id)#> waitUntilDone:<#(BOOL)#>]
[self performSelector:<#(nonnull SEL)#> onThread:<#(nonnull NSThread *)#> withObject:<#(nullable id)#> waitUntilDone:<#(BOOL)#> modes:<#(nullable NSArray *)#>]
大概讲一下 waitUntilDone 这个参数,顾名思义,就是是否等到结束。
1)如果这个值设为YES,那么就需要等到这个方法执行完,线程才能继续往下去执行。它会阻塞提交的线程。
2)如果为NO的话,这个调用的方法会异步的实行,不会阻塞提交线程。
- 2、定时源:
[NSTimer scheduledTimerWithTimeInterval:<#(NSTimeInterval)#> target:<#(nonnull id)#> selector:<#(nonnull SEL)#> userInfo:<#(nullable id)#> repeats:<#(BOOL)#>
[NSTimer timerWithTimeInterval:<#(NSTimeInterval)#> target:<#(nonnull id)#> selector:<#(nonnull SEL)#> userInfo:<#(nullable id)#> repeats:<#(BOOL)#>]
还有使用block的那几个方面。这里记住只有schedule开头的方法默认加到了NSDefaultRunLoopMode模式下,其它方法都需要调用[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]
去给它指定一个mode
- 3、基于端口的输入源:
举个例子
- (void)testDemo
{
//声明两个端口 随便怎么写创建方法,返回的总是一个NSMachPort实例
NSMachPort *mainPort = [[NSMachPort alloc]init];
NSPort *threadPort = [NSMachPort port];
//设置线程的端口的代理回调为自己
threadPort.delegate = self;
//给主线程runloop加一个端口
[[NSRunLoop currentRunLoop]addPort:mainPort forMode:NSDefaultRunLoopMode];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//添加一个Port
[[NSRunLoop currentRunLoop]addPort:threadPort forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop]runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
});
NSString *s1 = @"hello";
NSData *data = [s1 dataUsingEncoding:NSUTF8StringEncoding];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSMutableArray *array = [NSMutableArray arrayWithArray:@[mainPort,data]];
//过2秒向threadPort发送一条消息,第一个参数:发送时间。msgid 消息标识。
//components,发送消息附带参数。reserved:为头部预留的字节数(从官方文档上看到的,猜测可能是类似请求头的东西...)
[threadPort sendBeforeDate:[NSDate date] msgid:1000 components:array from:mainPort reserved:0];
});
}
//这个NSMachPort收到消息的回调,注意这个参数,可以先给一个id。如果用文档里的NSPortMessage会发现无法取值
- (void)handlePortMessage:(id)message
{
NSLog(@"收到消息了,线程为:%@",[NSThread currentThread]);
//只能用KVC的方式取值
NSArray *array = [message valueForKeyPath:@"components"];
NSData *data = array[1];
NSString *s1 = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"%@",s1);
// NSMachPort *localPort = [message valueForKeyPath:@"localPort"];
// NSMachPort *remotePort = [message valueForKeyPath:@"remotePort"];
}
输出如下:
2017-09-15 22:18:50.235 test[6345:32771911] 收到消息了,线程为:{number = 3, name = (null)}
2017-09-15 22:18:50.236 test[6345:32771911] hello
可以看到我们从主线程往另一个线程发送了消息。这里有一点要注意就是这里The components array consists of a series of instances
of some subclass of NSData(components array里面传值的类型只能是NSData或者NSPort的子类)。
- 4、自定义输入源:
栗子
- (void)testDemo4
{
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"starting thread.......");
_runLoopRef = CFRunLoopGetCurrent();
//初始化_source_context。
bzero(&_source_context, sizeof(_source_context));
//这里创建了一个基于事件的源,绑定了一个函数
_source_context.perform = fire;
//参数
_source_context.info = "hello";
//创建一个source
_source = CFRunLoopSourceCreate(NULL, 0, &_source_context);
//将source添加到当前RunLoop中去
CFRunLoopAddSource(_runLoopRef, _source, kCFRunLoopDefaultMode);
//开启runloop 第三个参数设置为YES,执行完一次事件后返回
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 9999999, YES);
NSLog(@"end thread.......");
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (CFRunLoopIsWaiting(_runLoopRef)) {
NSLog(@"RunLoop 正在等待事件输入");
//添加输入事件
CFRunLoopSourceSignal(_source);
//唤醒线程,线程唤醒后发现由事件需要处理,于是立即处理事件
CFRunLoopWakeUp(_runLoopRef);
}else {
NSLog(@"RunLoop 正在处理事件");
//添加输入事件,当前正在处理一个事件,当前事件处理完成后,立即处理当前新输入的事件
CFRunLoopSourceSignal(_source);
}
});
}
//此输入源需要处理的后台事件
static void fire(void* info){
NSLog(@"我现在正在处理后台任务");
printf("%s",info);
}
输出
2017-09-15 22:39:09.750 test[6384:32816434] starting thread.......
2017-09-15 22:39:11.750 test[6384:32816214] RunLoop 正在等待事件输入
2017-09-15 22:39:11.751 test[6384:32816434] 我现在正在处理后台任务
hello
2017-09-15 22:39:11.752 test[6384:32816434] end thread.......
简单讲解一下。CFRunLoopSourceRef _source这个是自定义输入源中最重要的一个参数。它用来连接runloop与CFRunLoopSourceContext中的一些配置项,注意我们自定义的输入源,必须由我们手动来触发。需要先CFRunLoopSourceSignal(_source);再看当前runloop是否在休眠中,来看是否需要调用CFRunLoopWakeUp(_runLoopRef);(一般都是要调用的)。
以上就是线程间通信的四个方法了。
总结一下知识要点
- runloop的结构,它是由若干个Mode构成,而每个 Mode 又包含若干个 Source/Timer/Observer。
- 除了主线程,其它线程的runloop默认是没有开启的,而runloop的创建其实就是发生在你第一次在线程获取runloop中。
- 运行runloop的那几个函数的区别
- runloop运行的逻辑
- runloop线程保活
- runloop线程通信的四种方式
最后
文章挺长的,如果想要好好了解一下runloop的建议花点时间仔细看看,另外本人的表述能力也确实有限,有什么表述不清或者错误的地方希望大家多多指点。