A NSRunLoop object processes input for sources such as mouse and keyboard events from the window system, NSPort objects, and NSConnection objects. A NSRunLoop object also processes NSTimer events.
Your application neither creates or explicitly manages NSRunLoop objects. Each NSThread object—including the application’s main thread—has an NSRunLoop object automatically created for it as needed. If you need to access the current thread’s run loop, you do so with the class method currentRunLoop.
Note that from the perspective of NSRunLoop, NSTimer objects are not “input”—they are a special type, and one of the things that means is that they do not cause the run loop to return when they fire.
NSRunLoop对象处理输入源例如系统的鼠标和键盘的事件,NSPort对象,和NSConnection对象。NSRunLoop对象也处理NSTimer事件。每个NSThread对象——包括应用的主线程,都有一个按需自动创建的NSRunLoop对象。通过currentRunLoop
方法,获取当前线程的runloop
内容来自Runloop
Runloop
是什么?Runloop
还是比较顾名思义的一个东西,说白了就是一种循环,只不过它这种循环比较高级。一般的 while
循环会导致 CPU
进入忙等待状态,而 Runloop
则是一种“闲”等待,这部分可以类比 Linux
下的 epoll
。当没有事件时,Runloop
会进入休眠状态,有事件发生时, Runloop
会去找对应的 Handler
处理事件。Runloop
可以让线程在需要做事的时候忙起来,不需要的话就让线程休眠。
盗一张苹果官方文档的图,也是几乎每个讲 Runloop
的文章都会引用的图,大体说明了 Runloop
的工作模式:
Runloop
和线程是绑定在一起的。每个线程(包括主线程)都有一个对应的 Runloop
对象。我们并不能自己创建 Runloop
对象,但是可以获取到系统提供的 Runloop
对象。
主线程的 Runloop
会在应用启动的时候完成启动,其他线程的 Runloop
默认并不会启动,需要我们手动启动。
这两个都是 Runloop
事件的来源,其中 Input Source
又可以分为三类
Port-Based Sources
,系统底层的 Port
事件,例如 CFSocketRef
,在应用层基本用不到Custom Input Sources
,用户手动创建的 Source
Cocoa Perform Selector Sources
, Cocoa
提供的 performSelector
系列方法,也是一种事件源Timer Source
顾名思义就是指定时器事件了。
在监视与被监视中,Runloop
要处理的事情还挺复杂的。为了让 Runloop
能专心处理自己关心的那部分事情,引入了 Runloop Mode
概念。
如图所示,Runloop Mode
实际上是 Source
,Timer
和 Observer
的集合,不同的 Mode
把不同组的 Source
,Timer
和 Observer
隔绝开来。Runloop
在某个时刻只能跑在一个 Mode
下,处理这一个 Mode
当中的 Source
,Timer
和 Observer
。
苹果文档中提到的 Mode
有五个,分别是:
内容来自RunLoop详解
NSConnectionReplyMode
处理NSConnection
对象相关事件,系统内部使用,这个mode
表明NSConnection
对象等待reply
,用户基本不会使用NSModalPanelRunLoopMode
处理modal panels
事件,需要等待处理的input source
为modal panel
时设置,比如NSSavePanel
和NSOpenPanel
NSEventTrackingRunLoopMode
NSRunLoopCommonModes
NSDefaultRunLoopMode
iOS 中公开暴露出来的只有 NSDefaultRunLoopMode
和 NSRunLoopCommonModes
。 NSRunLoopCommonModes
实际上是一个 Mode
的集合,默认包括 NSDefaultRunLoopMode
和 NSEventTrackingRunLoopMode
。
日常开发中,与 runLoop
接触得最近可能就是通过 NSTimer
了。一个 Timer
一次只能加入到一个 RunLoop
中。我们日常使用的时候,通常就是加入到当前的 runLoop
的 default mode
中,而 ScrollView 在用户滑动时,主线程 RunLoop
会转到 UITrackingRunLoopMode
。而这个时候, Timer
就不会运行。
有如下两种解决方案:
第一种: 设置RunLoop Mode
,例如NSTimer
,我们指定它运行于 NSRunLoopCommonModes
,这是一个Mode
的集合。注册到这个 Mode
下后,无论当前 runLoop
运行哪个 mode
,事件都能得到执行。
第二种: 另一种解决Timer
的方法是,我们在另外一个线程执行和处理 Timer
事件,然后在主线程更新UI
。
在 AFNetworking 3.0
中,就有相关的代码,如下:
- (void)startActivationDelayTimer {
self.activationDelayTimer = [NSTimer
timerWithTimeInterval:self.activationDelay target:self selector:@selector(activationDelayTimerFired) userInfo:nil repeats:NO];
[[NSRunLoop mainRunLoop] addTimer:self.activationDelayTimer forMode:NSRunLoopCommonModes];
}
这里就是添加了一个计时器,由于指定了 NSRunLoopCommonModes
,所以不管 RunLoop
出于什么状态,都执行这个计时器任务。
一些测试
为了把NSTimer
添加到一个子线程中,创建一个WZThread
类,继承自NSThread
,重写dealloc
方法:
- (void)dealloc
{
NSLog(@"WZWZThread销毁");
}
如下,创建子线程,并把NSTimer
添加到其中,如下:
WZThread *thread = [[WZThread alloc] initWithBlock:^{
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval: 1.0 target: self selector: @selector(timerMethod) userInfo: nil repeats: YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
NSLog(@"当前线程:%@", [NSThread currentThread]);
}];
[thread start];
启动后,发现NSTimer
要执行的方法并没有运行,控制台输出:
2017-06-21 10:43:11.712 RunTimeTest[5690:294930] 当前线程:0x608000275900>{number = 5, name = (null)}
2017-06-21 10:43:11.713 RunTimeTest[5690:294930] WZWZThread销毁
表示WZWZThread
已经销毁,这里是因为子线程创建出来后,执行完毕就异步销毁了。
怎么让子线程常驻?
每一个子线程都一个RunLoop,默认是不启动的,所以子线程执行完代码后就挂掉了
如下:
WZThread *thread = [[WZThread alloc] initWithBlock:^{
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval: 1.0 target: self selector: @selector(timerMethod) userInfo: nil repeats: YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
NSLog(@"当前线程:%@", [NSThread currentThread]);
}];
[thread start];
- (void)timerMethod
{
NSLog(@"timerMethod");
NSLog(@"%@", [NSThread currentThread]);
}
[[NSRunLoop currentRunLoop] run]
后,timer正常运行
2017-06-21 11:03:05.284 RunTimeTest[5785:304973] timerMethod
2017-06-21 11:03:05.285 RunTimeTest[5785:304973] 0x60800027c1c0>{number = 5, name = (null)}
但查看控制台输出,会发现NSLog(@"当前线程:%@", [NSThread currentThread]);
并没有运行
原因是什么呢?
参考深入研究 Runloop 与线程保活
所以可以通过设置一个flag来退出循环,如下:
WZThread *thread = [[WZThread alloc] initWithBlock:^{
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval: 1.0 target: self selector: @selector(timerMethod) userInfo: nil repeats: YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
while(!self.isFinished) {
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
}
NSLog(@"当前线程:%@", [NSThread currentThread]);
}];
[thread start];
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
self.finished = YES;
}
控制台输出如下:
2017-06-21 11:39:23.990 RunTimeTest[5879:320368] timerMethod
2017-06-21 11:39:23.991 RunTimeTest[5879:320368] 0x608000275580>{number = 5, name = (null)}
2017-06-21 11:39:24.989 RunTimeTest[5879:320368] timerMethod
2017-06-21 11:39:24.990 RunTimeTest[5879:320368] 0x608000275580>{number = 5, name = (null)}
2017-06-21 11:39:25.512 RunTimeTest[5879:320368] 当前线程:0x608000275580>{number = 5, name = (null)}
2017-06-21 11:39:25.513 RunTimeTest[5879:320368] WZWZThread销毁
考验英文水平的时候到了,首先来看一段官方文档对于如何启动 runloop
的介绍,它的启动方式一共有三种:
Unconditionally
With a set time limit
In a particular mode
这三种进入方式分别对应了三种方法,其中第一种就是我们目前使用的:
run
runUntilDate
runMode:beforeDate:
接下来分别是对三种方式的介绍,文字比较啰嗦,这里我简单总结一下,有兴趣的读者可以直接看原文。
runloop
,结束 runloop
的唯一方式是 kill
它runloop
会在处理完事件或超时后结束,此时我们可以选择重新开启 runloop
。这种方式要优于前一种runloop
以哪种模式运行。查看 run
方法的文档还可以知道,它的本质就是无限调用 runMode:beforeDate:
方法,同样地,runUntilDate:
也会重复调用 runMode:beforeDate:
,区别在于它超时后就不会再调用。
总结来说,runMode:beforeDate:
表示的是 runloop
的单次调用,另外两者则是循环调用。
相比于 runloop
的启动,它的退出就比较简单了,只有两种方法:
如果你使用方法二或三来启动 runloop
,那么在启动的时候就可以设置超时时间。然而考虑到目标是:“利用 runloop
进行线程保活”,所以我们希望对线程和它的 runloop
有最精确的控制,比如在完成任务后立刻结束,而不是依赖于超时机制。
好在根据文档的描述,我们还可以使用 CFRunLoopStop()
方法来手动结束一个 runloop
。注意文档中在介绍利用 CFRunLoopStop()
手动退出时有下面这句话:
The difference is that you can use this technique on run loops you started unconditionally.
这里的解释非常容易产生误会,如果在阅读时没有注意到 exit
和 terminate
的微小差异就很容易掉进坑里,因为在 run
方法的文档中还有这句话:
If you want the run loop to terminate, you shouldn’t use this method
总的来说,如果你还想从 runloop
里面退出来,就不能用 run
方法。根据实践结果和文档,另外两种启动方法也无法手动退出。
正确的做法
难道子线程中开启了 runloop
就无法结束并释放了么?这显然是一个不合理的结论,经过一番查找,终于在这篇文章里找到了答案,它给出了使用 CFRunLoopStop()
无效的原因:
CFRunLoopStop()
方法只会结束当前的 runMode:beforeDate:
调用,而不会结束后续的调用。
这也就是为什么 Runloop
的文档中说 CFRunLoopStop()
可以 exit
(退出) 一个 runloop
,而在 run
等方法的文档中又说这样会导致 runloop
无法 terminate
(终结)。
文章中给出的方案是使用 CFRunLoopRun()
启动 runloop
,这样就可以通过 CFRunLoopStop()
方法结束。而文档则推荐了另一种方法:
BOOL shouldKeepRunning = YES; // global
NSRunLoop *theRL = [NSRunLoop currentRunLoop];
while (shouldKeepRunning && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);
runLoop的终极大杀器
CFRunLoopObserverRef
CFRunLoopObserverRef
是观察者,能够监听RunLoop的状态改变
可以监听的时间点有以下几个:
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 1
kCFRunLoopBeforeTimers = (1UL << 1), // 2
kCFRunLoopBeforeSources = (1UL << 2), // 4
kCFRunLoopBeforeWaiting = (1UL << 5), // 32
kCFRunLoopAfterWaiting = (1UL << 6), // 64
kCFRunLoopExit = (1UL << 7), // 128
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
Observer的使用步骤
添加Observer
添加观察者:监听RunLoop的状态
释放Observer
// 创建observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
NSLog(@"----监听到RunLoop状态发生改变---%zd", activity);
});
// 添加观察者:监听RunLoop的状态
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
// 释放Observer
CFRelease(observer);
-(void)observer{
//创建一个监听对象
/*
第一个参数:分配存储空间的
第二个参数:要监听的状态 kCFRunLoopAllActivities 所有状态
第三个参数:是否要持续监听
第四个参数:优先级
第五个参数:回调
*/
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"runloop进入");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"runloop要去处理timer");
break;
case kCFRunLoopBeforeSources:
NSLog(@"runloop要去处理Sources");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"runloop要睡觉了");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"runloop醒来啦");
break;
case kCFRunLoopExit:
NSLog(@"runloop退出");
break;
default:
break;
}
});
//给runloop添加监听者
/*
第一个参数:要监听哪个runloop
第二个参数:监听者
第三个参数:要监听runloop在哪种运行模式下的状态
*/
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
[NSTimer scheduledTimerWithTimeInterval:5.0 target:self selector:@selector(run1) userInfo:nil repeats:YES];
CFRelease(observer);
}
上面的 Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。