RunLoop介绍
RunLoop和线程有什么关系?
总的来说,RunLoop正如其名,loop表示某种循环,和run放在一起就表示一直在运行着的循环。实际上,runloop和线程是紧密相连的,可以这样说runloop是为了线程而生,没有线程,它就没有存在的必要。Runloops是线程的基础框架部分,Cocoa和CoreFoundation都提供了runloop对象方便配置和管理线程的runloop(以下都以Cocoa为例)。每个线程,包括程序的主线程(main thread)都有与之对应的runloop对象。
*RunLoop和线程之间的关系
- 1.主线程的runloop默认是启动的。
iOS的应用程序里面,程序启动后会有一个如下的main()函数
int main(int argc, char *argv[]) {
@autoreleasepool {
return UIApplacationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
重点是UIApplicationMain()函数,这个方法会为main thread设置一个NSRunLoop对象,这就解释了:为什么我们的应用可以在无人操作的时候休息,需要让他干活的时候又能立马响应。
2.对其它线程来说,runloop默认是没有启动的,如果你需要更多的线程交互则可以手动配置和启动,如果线程只是去执行一个长时间的已确定的任务则不需要。
-
3.在任何一个Cocoa程序的线程中,都可以通过以下代码来获取到当前线程的runloop:
OC:NSRunLoop *runloop = [NSRunLoop currentRunLoop];
Swift:let runloop = RunLoop.current -
4.Cocoa中的NSRunLoop类并不是安全的。
我们不能再一个线程中去操作另外一个线程的runloop对象,那很可能造成意想不到的后果。不过幸运的是CoreFoundation中的不透明类CFRunLoopRef是安全的,而且两种类型的runloop完全可以混用。Cocoa中的NSRunLoop类可以通过实例方法:-(CFRunLoopRef)getCFRunLoop;
获取对应的CFRunLoopRef类,来达到线程安全的目的。
-
5.RunLoop的管理并不完全是自动的。
我们仍必须设计线程代码以在合适的时候启动runloop并正确响应输入事件,当然前提是线程中需要用到runloop。而且,我们还需要使用while/for语句来驱动runloop能够循环运行,下面的代码就成功的驱动了一个runloop:BOOL isRunning = NO;
do {
isRunning = [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
NSLog(@"%d", isRunning);
} while (isRunning); 6.RunLoop同时也负责autoreleasepool的创建和释放。在使用手动管理内存的项目中,会经常用到很多自动释放的对象,如果这些对象不能被即时释放掉,会造成内存占用量急剧增大。Runloop就会为我们做了这些工作,每当一个运行循环结束的时候,他都会释放一次autoreleasepool,同时pool中的所有自动释放类型变量都会被释放掉。
-
7.RunLoop的优点。一个runloop就是一个时间处理循环,用来不停地监听和处理输入事件并将其分配到对应的目标上进行处理。如果仅仅是想实现这个功能,你可能会想一个while循环不就可以实现了吗,用的着费老大劲来做个那么复杂的机制。显然,苹果的架构设计师不是吃干饭的,你想得到的他们早就想过了。
首先,NSRunLoop是一种更加高明的消息处理模式,他就高明在对消息处理过程进行了更好地抽象和封装,这样才能使的你不用处理一些很琐碎很底层的具体消息的处理,在NSRunLoop中每一个消息就被打包在input source或者timer source中了。
其次,也是很重要的一点,使用runloop可以使你的线程在有工作的时候工作,没有工作的时候休眠,这可以大大节省系统资源。
*RunLoop的相关知识点
-
输入事件来源
RunLoop接收输入事件来自两种不同的来源:输入源(input source)和定时源(timer source).两种源都使用程序的某一特定的处理例程来处理到达的事件。下图显示了runloop的概念结构以及各种源。
需要说明的是,当你创建输入源,你需要将其分配给runloop中的一个或多个模式。模式只会在特定事件影响监听的源。大多数情况下,runloop运行在默认模式下,但是你也可以使其运行在自定义模式。若某一源在当前模式下不被监听,那么任何其生成的消息只在runloop运行在其关联的模式下才会被传递。
* 输入源(input source)
传递异步事件,通常消息来自其他线程和程序。输入源传递异步消息给相应的处理例程,并调用runUntileDate:方法来退出(在线程里面相关的NSRunLoop对象调用)。
* 基于端口的输入源
基于端口上午输入源由内核自动发送。Cocoa和CoreFoundation内置支持使用端口相关的对象和函数来创建的基于端口的源。例如,在Cocoa里面你从来不需要直接创建输入源。你只要简单的创建端口对象,并使用NSPort的方法把该端口添加到runloop.端口对象会自己处理创建和配置输入源。
在CoreFoundtaion,你必须人工创建端口和她的runloop源。我们可以使用端口相关的函数(CFMachPortRef,CFMessagePortRef,CFSocketRef)来创建合适的对象。下面的例子展示了如何创建一个基于端口的输入源,将其添加到runloop并启动:
void creatPortSource() {
CFMessagePortRef port = CFMessagePortCreateLocal(kCFAllocatorDefault, "come.someport", myCallBackFunc, nil, nil);
CFRunLoopSourceRef source = CFMessagePortCreateRunLoopSource(kCFAllocatorDefault, port, 0);
CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
while (pageStillLoading) {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
CFRunLoopRun();
[pool release];
}
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
CFRelease(source);
}
* 自定义输入源
自定义的输入源需要人工从其他线程发送。为了创建自定义输入源,必须使用CoreFoundation里面的CFRunLoopSourceRef类型相关的函数来创建。你可以使用回调函数来配置自定义输入源。CoreFundation会在配置源的不同地方调用回调函数,处理输入事件,在源从runloop移除的时候清理它。
除了定义在事件到达时自定义输入源的行为,你也必须定义消息传递机制。源的这部分运行在单独的线程里面,并负责在数据等待处理的时候传递数据给源并通知它处理数据。消息传递机制的定义取决于你,但最好不要过于复杂。创建并启动自定义输入源的示例如下:
void createCustomSource()
{
CFRunLoopSourceContext context = {0,NULL, NULL,NULL, NULL,NULL, NULL,NULL, NULL,NULL};
CFRunLoopSourceRef source =CFRunLoopSourceCreate(kCFAllocatorDefault,0, &context);
CFRunLoopAddSource(CFRunLoopGetCurrent(), source,kCFRunLoopDefaultMode);
while (pageStillLoading) {
NSAutoreleasePool *pool = [[NSAutoreleasePoolalloc] init];
CFRunLoopRun();
[pool release];
}
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source,kCFRunLoopDefaultMode);
CFRelease(source);
}
* Cocoa上的Selector源
除了基于端口的源,Cocoa定义了自定义输入源,允许你在任何线程执行selector()方法。和基于端口的源一样,执行selector请求会在目标线程上序列化,减缓许多在线程上允许多个方法容易引起的同步问题。不像基于端口的源,一个selector执行完成后会自动从runloop里面移除。
当在其他线程上面执行selector时,目标线程必须有一个活动的runloop。对于你创建的线程,这就意味着线程在你显式的启动runloop之前是不会执行selector方法的,而是一直处于休眠状态。
NSObject类提供了类似如下的selector方法:
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array
-
定时源(timer source)
定时源在预设的时间点同步方式传递消息,这些消息都会发生在特定时间或者重复的时间间隔。定时源则直接传递消息给处理例程,不会立即退出runloop.
需要注意的是,尽管定时器可以产生基于事件的通知,但它并不是实时机制,和输入源一样,定时器也和你的runloop的特定模式相关。如果定时器所在的模式当前未被runloop监视,那么定时器将不会开始知道runloop运行在相应的模式下。类似的,如果定时器在runloop处理某一事件期间开始,定时器会一直等待直到下次runloop开始相应的处理程序。如果runloop不在运行,那定时器也将永远不启动。
创建定时器的两种方法:
-
方法一:
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(backgroundThreadFire:) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
-
方法二
[NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(backgroundThreadFire:) userInfo:nil repeats:YES];
-
-
RunLoop观察者
源是在合适的听不或异步事件发生时触发,而runloop观察着则是在runloop本身运行的特定时候触发。你可以使用runloop观察者来为处理某一特定事件或是进入休眠的线程做准备。你可以将runloop观察者和以下事件关联:
1.RunLoop入口
2.RunLoop何时处理一个定时器
3.RunLoop何时处理一个输入源
4.RunLoop何时进入睡眠状态
5.RunLoop何时被唤醒,但在唤醒之前要处理的事件
6.RunLoop终止
和定时器类似,在创建的时候你可以指定runloop观察者可以只用一次或循环使用。若只用一次,那么在它启动后,会把它自己从runloop里面移除,而循环的观察者则不会。定义观察者并把它添加到runloop,只能使用CoreFoundation.下面的例子演示了如何创建runloop的观察者:
- (void)addObserverToCurrentRunLoop { NSRunLoop *myRunLoop = [NSRunLoop currentRunLoop]; CFRunLoopObserverContext context = {0, (__bridge void *)(self), nil, nil, nil}; CFRunLoopObserverRef observe = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopBeforeTimers, YES, 0, nil, &context); if (observe) { CFRunLoopRef cfloop = [myRunLoop getCFRunLoop]; CFRunLoopAddObserver(cfloop, observe, kCFRunLoopDefaultMode); } } 其中,kCFRunLoopBeforeTimers表示选择监听定时器触发前处理事件,后面的YES表示循环监听。
-
RunLoop的事件队列
每次运行runloop,线程的runloop会自动处理之前未处理的消息,并通知相关的观察者。具体的顺序如下:
1.通知观察者runloop已经启动
2.通知观察者任何即将要开始的定时器
3.通知观察者任何即将启动的非基于端口的源
4.启动任何准备好的非基于端口的源
5.如果基于端口的源准备好并处于等待状态,立即启动;并进入步骤9
6.通知观察者线程进入休眠
7.将线程置于休眠直到任一下面的事件发生:
* 某一事件到达基于端口的源 * 定时器启动 * RunLoop设置的时间已经超时 * RunLoop被显式唤醒
8.通知观察者线程将被唤醒
9.处理未处理的事件
* 如果用户定义的定时器启动,处理定时器事件并重启runloop.进入步骤2 * 如果输入源启动,传递相应的消息 * 如果runloop被显式唤醒而且时间还没超时,重启runloop.进入步骤2
10.通知观察者runloop结束
因为定时器和输入源的观察者是在相应的事件发生之前传递消息,所以通知的时间和实际事件发生的时间之间可能存在误差。如果需要精确时间控制,你可以使用休眠和唤醒通知来帮助你校对实际发生事件的时间。
因为当你运行runloop时定时器和其它周期性事件经常需要被转换,撤销runloop也会终止消息传递。典型的例子就是鼠标路径追踪。因为你的代码直接获取到消息而不是经由程序传递,因此活跃的定时器不会开始直到鼠标追踪结束并将控制权交给程序。
runloop可以有runloop对象显式唤醒。其他消息也可以唤醒runloop.例如,添加新的非基于端口的源会唤醒runloop从而可以立即处理输入源而不需要等待其他事件发生后再处理。
从这个事件队列中可以看出:
- 如果是事件到达,消息会被传递给相应的处理程序来处理,runloop处理完当次事件后,runloop会退出,而不管之前预定的时间到了没有。你可以重新启动runloop来等待下一个事件。
- 如果线程中有需要处理的源,但是响应的事件没有到来的时候,线程就会休眠等待相应事件的发生。这就是为什么runloop可以做到让线程有工作的时候忙于工作,而没有工作的时候处于休眠状态。
*RunLoop的mode作用是什么?
mode主要是用来指定事件在运行循环中的优先级的,分为:
- NSDefaultRunLoopMode (kCFRunLoopDefaultMode) : 默认,空闲状态
- UITrackingRunLoopMode: ScrollView滑动时
- UIInitialzationRunLoopMode: 启动时
- NSRunLoopCommonModes (kCFRunLoopCommonModes) :Mode集合
苹果公开提供的Mode有两个:
1.NSDefaultRunLoopMode (kCFRunLoopDefaultMode)
2.NSRunLoopCommomodes (kCFRunLoopCommonModes)
以+ scheduledTimerWithTimeInterval...的方式触发的timer,在滑动页面上的列表时,timer会暂停回调,为什么?如何解决?
RunLoop只能运行在一种mode下,如果要换mode,当前的loop也需要停下重启成新的。基于这个机制,ScrollView在滚动过程中NSDefaultRunLoopMode (kCFRunLoopDefaultMode)的mode会切换到UITrackingRunLoopMode来保证ScrollView的流畅滑动:只能在NSDefaultRunLoopMode的模式下处理的事件会影响ScrollView的滑动。
如果我们把一个NSTimer对象以NSDefaultRunLoopMode (kCFRunLoopDefaultMode)添加到主运行循环中的时候,ScrollView滚动过程中会因为mode的切换,而导致NSTimer将不再被调度。
同时因为mode还是可定制的。所以:
Timer计时会被scrollView的滑动影响的问题可以通过将timer添加到NSRunLoopCommomModes (kCFRunLoopCommonModes)来解决。 代码如下:
// 将timer添加到NSDefaultRunLoopMode
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTick:) userInfo:nil repeats:YES];
// 再将timer添加到NSRunLoopCommonModes
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerTick:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
猜想RunLoop内部是如何实现的?
一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机退出,让线程能随时处理但并不退出,通常的代码逻辑是这样的:
function loop() {
initialize();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}
或用伪代码来展示下:
int main (int argc, char *srgv[]) {
// 程序一直运行状态
while (AppIsRunning) {
// 睡眠状态,等待唤醒时间
id whoWakesMe = SleepForWakingUp();
// 得到唤醒事件
id event = GetEvent(whoWakesMe);
// 开始处理事件
HandleEvent(event);
}
return 0;
}
什么时候使用RunLoop?
仅当在为你的程序创建辅助线程的时候,你才需要显式进行一个runloop。RunLoop是程序主线程基础设施的关键部分。所以,Cocoa和Carbon程序提供了代码运行主程序的循环并自动启动runloop.iOS程序中UIApplication的run方法(或Mac OS X中的NSApplication)作为程序启动步骤的一部分,它在程序正常启动的时候就会启动程序的主循环。类似的,RunApplicationEventLoop函数为Carbon程序启动主循环。如果你使用Xcode提供的模板创建你的程序,那你永远不需要自己去显式的调用这些例程。
对于辅助线程,你需要判断一个runloop是否是必须的。如果是必须的,那么你要自己配置并启动它。你不需要再任何情况下都去启动一个线程的runloop。比如,你使用线程来处理一个月线定义的长时间运行的任务时,你应该避免启动runloop。runloop在你要和线程有更多的交互时才需要,比如以下情况:
1.使用端口或自定义输入源来和其它线程通信
2.使用线程的定时器
3.Cocoa中使用任何performSelector...方法
4.使线程周期性工作
如果你决定在程序中使用runloop,那么他的配置和启动都很简单。和所有线程编程一样,你需要好在辅助线程退出线程的情形。让线程自然退出往往比强制关闭它更好。