转自:http://chun.tips/blog/2014/10/20/zou-jin-run-loopde-shi-jie-%5B%3F%5D-:shi-yao-shi-run-loop%3F/
在刚刚接触iOS开发的时候,我们在Xcode的帮助下生成了第一个工程。工程里会包含一个main.m
的文件,默认的代码大致如下:
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([YourAppDelegate class]));
}
}
当我们的程序启动时,以上代码会被调用,主线程也随之开始运行。Run Loop做为主线程的一部分也同时被启动。
UIApplicationMain()方法在这里不仅完成了初始化我们的程序并设置程序Delegate的任务,而且随之开启了主线程的Run Loop, 开始接受处理事件。
Run Loop本质和它的意思一样是运行着的循环,更确切的说是线程中的循环。它用来接受循环中的事件和安排线程工作,并在没有工作时,让线程进入睡眠状态。
下图展示了Run Loop的模型,图片来源于:https://developer.apple.com
从图中可以看出,Run Loop是线程中的一个循环,并对接收到的事件进行处理。我们的代码可以通过提供while或者for循环来驱动Run Loop。在循环中,Run Loop对象来负责事件处理代码(接收事件并且调用事件处理方法)。
Run Loop从两个不同的事件源中接收消息:
Input source
用来投递异步消息,通常消息来自另外的线程或者程序。在接收到消息并调用程序指定方法时,线程中对应的NSRunLoop对象会通过执行runUntilDate:
方法来退出。Timer source
用来投递timer事件(Schedule或者Repeat)中的同步消息。在处理消息时,并不会退出Run Loop。Observer
的概念,可以往Run Loop中加入自己的观察者以便监控Run Loop的运行过程。(详细请继续阅读)Run Loop Mode可以理解为一个集合中包括所有要监视的事件源和要通知的Run Loop中注册的观察者。每一次运行自己的Run Loop时,都需要显示或者隐示的指定其运行于哪一种Mode。在设置Run Loop Mode后,你的Run Loop会自动过滤和其他Mode相关的事件源,而只监视和当前设置Mode相关的源(通知相关的观察者)。大多数时候,Run Loop都是运行在系统定义的默认模式上。
Model Panel可以运行在 Model 模式下。(OS X开发中遇到)
Run Loop Mode 区分基于事件的源,而不是事件的种类。比如你不能通过Run Loop Mode去只选择鼠标点击事件或者键盘输入事件。你可以使用Run Loop Mode去监听端口,暂停计时器或者改变其他源。
Cocoa和Core Foundataion为我们定义了默认的和常用的Mode。Run Loop Mode的名称可以使用字符串来标识,我们也可以使用字符串指定一个Mode名称来自定义Mode。
我们可以给Mode指定任意名称,但是它对应的集合内容不能是任意的。我们需要添加Input source, Timer source 或者 Observer到自己自定义的Mode。
下面列出iOS下一些已经定义的Run Loop Modes:
1) NSDefaultRunLoopMode: 大多数工作中默认的运行方式。
2) NSConnectionReplyMode: 使用这个Mode去监听NSConnection对象的状态,我们很少需要自己使用这个Mode。
3) NSModalPanelRunLoopMode: 使用这个Mode在Model Panel情况下去区分事件(OS X开发中会遇到)。
4) UITrackingRunLoopMode: 使用这个Mode去跟踪来自用户交互的事件(比如UITableView上下滑动)。
5) GSEventReceiveRunLoopMode: 用来接受系统事件,内部的Run Loop Mode。
6) NSRunLoopCommonModes: 这是一个伪模式,其为一组run loop mode的集合。如果将Input source加入此模式,意味着关联Input source到Common Modes中包含的所有模式下。在iOS系统中NSRunLoopCommonMode包含NSDefaultRunLoopMode、NSTaskDeathCheckMode、UITrackingRunLoopMode.可使用CFRunLoopAddCommonMode方法向Common Modes中添加自定义mode。
Run Loop运行时只能以一种固定的Mode运行,只会监控这个Mode下添加的Timer source和Input source。如果这个Mode下没有添加事件源,Run Loop会立刻返回。
Run Loop不能在运行在NSRunLoopCommonModes,因为NSRunLoopCommonModes是个Mode集合,而不是一个具体的Mode。我们可以在添加事件源的时候使用NSRunLoopCommonModes,只要Run Loop运行在NSRunLoopCommonModes中任何一个Mode,这个事件源都可以被触发。
Input source有两个不同的种类: Port-Based Sources
和 Custom Input Sources
。Run Loop本身并不关心Input source是哪一种类型。系统会实现两种不同的Input source供我们使用。这两种不同类型的Input source的区别在于:Port-Based Sources由内核自动发送,Custom Input Sources需要从其他线程手动发送。
走进Run Loop世界系列的第二章会专门讨论如何自定义事件源。
我们可以使用Core Foundation里面的CFRunLoopSourceRef类型相关的函数来创建custom input source。
通过内置的端口相关的对象和函数,配置基于端口的Input source。 (比如在主线程创建子线程时传入一个NSPort对象,主线程和子线程就可以进行通讯。NSPort对象会负责自己创建和配置Input source。)
Cocoa框架为我们定义了一些Custom Input Sources,允许我们在线程中执行一系列selector
方法。
//在主线程的Run Loop下执行指定的 @selector 方法
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:
//在当前线程的Run Loop下执行指定的 @selector 方法
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:
//在当前线程的Run Loop下延迟加载指定的 @selector 方法
performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:
//取消当前线程的调用
cancelPreviousPerformRequestsWithTarget:
cancelPreviousPerformRequestsWithTarget:selector:object:
和Port-Based Sources一样,这些selector的请求会在目标线程中序列化,以减缓线程中多个方法执行带来的同步问题。
和Port-Based Sources不一样的是,一个selector方法执行完之后会自动从当前Run Loop中移除。
Timer source在预设的时间点同步的传递消息。Timer是线程通知自己做某件事的一种方式。
Foundation中 NSTimer Class提供了相关方法来设置Timer source。需要注意的是除了scheduledTimerWithTimeInterval
开头的方法创建的Timer都需要手动添加到当前Run Loop中。(scheduledTimerWithTimeInterval 创建的timer会自动以Default Mode加载到当前Run Loop中。)
Timer在选择使用一次后,在执行完成时,会从Run Loop中移除。选择循环时,会一直保存在当前Run Loop中,直到调用invalidated方法。
事件源是同步或者异步的事件驱动时触发,而Run Loop Observer则在Run Loop本身进入某个状态时得到通知:
Observer需要使用Core Foundataion框架。和Timer一样,Run Loop Observers也可以使用一次或者选择repeat。如果只使用一次,Observer会在它被执行后自己从Run Loop中移除。而循环的Observer会一直保存在Run Loop中。
Run Loop本质是一个处理事件源的循环。我们对Run Loop的运行时具有控制权,如果当前没有时间发生,Run Loop会让当前线程进入睡眠模式,来减轻CPU压力。如果有事件发生,Run Loop就处理事件并通知相关的Observer。具体的顺序如下:
基于非Timer的Input source事件被处理后,Run Loop在将要退出时发送通知。基于Timer source处理事件后不会退出Run Loop。
我们应该只在创建辅助线程的时候,才显示的运行一个Run Loop。对于辅助线程,我们仍然需要判断是否需要启动Run Loop。比如我们使用一个线程去处理一个预先定义的长时间的任务,我们应当避免启动Run Loop。下面是官方Document提供的使用Run Loop的几个场景:
selector
相关方法使用Core Foundation中的方法通常是线程安全的,可以被任意线程调用。如果修改了Run Loop的配置然后需要执行某些操作,我们最好是在Run Loop所在的线程中执行这些操作。
使用Foundation中的NSRunLoop类来修改自己的Run Loop,我们必须在Run Loop的所在线程中完成这些操作。在其他线程中给Run Loop添加事件源或者Timer会导致程序崩溃。
示例代码可以在GitHub上查看:https://github.com/yechunjun/RunLoopDemo
1.使用NSRunLoop的currentRunLoop可以获得当前线程的Run Loop对象。(或者 CFRunLoopGetCurrent)
NSRunLoop *currentThreadRunLoop = [NSRunLoop currentRunLoop];
// 或者
CFRunLoopRef currentThreadRunLoop = CFRunLoopGetCurrent();
2.在配置Run Loop之前,我们必须添加一个事件源或者Timer source给它。如果Run Loop没有任何源需要监视的话,会立刻退出。同样的我们可以给Run Loop注册Observer。
- (void)main
{
@autoreleasepool {
NSLog(@"Thread Enter");
NSRunLoop *currentThreadRunLoop = [NSRunLoop currentRunLoop];
// 创建一个 Run Loop Observer,并添加到当前Run Loop中, 设置Mode为Default
CFRunLoopObserverContext context = {0, (__bridge void *)(self), NULL, NULL, NULL};
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ¤tRunLoopObserver, &context);
if (observer) {
CFRunLoopRef runLoopRef = currentThreadRunLoop.getCFRunLoop;
CFRunLoopAddObserver(runLoopRef, observer, kCFRunLoopDefaultMode);
}
// 创建一个Timer,重复调用来驱动Run Loop
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(handleTimerTask) userInfo:nil repeats:YES];
NSInteger loopCount = 10;
// 执行Run Loop10次后退出,每次Run Loop返回的时候检查是否有使线程退出的条件成立
do {
NSLog(@"LoopCount: %ld", loopCount);
[currentThreadRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
loopCount --;
} while (loopCount);
NSLog(@"Thread Exit");
}
}
上述代码中,我们可以通过currentRunLoopObserver
的回调函数查看当前Run Loop的状态。
我们一般可以通过这几张方式启动Run Loop:
- (void)run;
- (void)runUntilDate:(NSDate *)limitDate;
- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;
退出Run Loop一般如下:
CFRunLoopStop
来显式的停止Run loop。无条件启动的Run Loop中调用这个方法退出Run Loop。 尽管移除Run Loop的Input source和Timer也可能导致Run loop退出,但这并不是可靠的退出Run loop的方法。一些状态下系统会添加Input source到Run loop里面来处理所需事件。由于我们的代码未必会考虑到这些Input source,这样可能导致无法移除这些事件源,从而导致Run loop不能正常退出。
为当前长时间运行的线程配置Run Loop的时候,最好添加至少一个Input source到Run Loop以接收消息。虽然我们可以使用Timer来进入Run Loop,但是一旦Timer触发后,它通常就变为无效了,这会导致Run Loop退出。虽然附加一个循环的Timer可以让Run Loop运行一个相对较长的周期,但是这也会导致周期性的唤醒线程,这实际上是轮询(polling)的另一种形式而已。与之相反,Input source会一直等待某事件发生,在事件发生前它会让线程处于休眠状态。
在这里,我们做了一个测试。一个是产生一个普通的Thread执行任务,线程完成之后再继续运行。另一个是启动一个线程执行任务,并且使用Run Loop,等待线程完成之后再继续执行任务。
// Normal Thread Task
- (void)handleNormalThreadButtonTouchUpInside
{
NSLog(@"Enter handleNormalThreadButtonTouchUpInside");
self.normalThreadDidFinishFlag = NO;
NSThread *normalThread = [[NSThread alloc] initWithTarget:self selector:@selector(handleNormalThreadTask) object:nil];
[normalThread start];
while (!self.normalThreadDidFinishFlag) {
[NSThread sleepForTimeInterval:0.5];
}
NSLog(@"Exit handleNormalThreadButtonTouchUpInside");
}
- (void)handleNormalThreadTask
{
NSLog(@"Enter Normal Thread");
for (NSInteger i = 0; i < 5; i ++) {
NSLog(@"In Normal Thread, count = %ld", i);
sleep(1);
}
_normalThreadDidFinishFlag = YES;
NSLog(@"Exit Normal Thread");
}
在Normal Thread中,在执行过程中UI线程被阻塞,直到线程执行完成时,才能够正常响应UI线程事件。
// Thread with Run Loop
- (void)handleRunLoopThreadButtonTouchUpInside
{
NSLog(@"Enter handleRunLoopThreadButtonTouchUpInside");
self.runLoopThreadDidFinishFlag = NO;
NSThread *runLoopThread = [[NSThread alloc] initWithTarget:self selector:@selector(handleRunLoopThreadTask) object:nil];
[runLoopThread start];
while (!self.runLoopThreadDidFinishFlag) {
NSLog(@"Begin RunLoop");
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
NSLog(@"End RunLoop");
}
NSLog(@"Exit handleRunLoopThreadButtonTouchUpInside");
}
- (void)handleRunLoopThreadTask
{
NSLog(@"Enter Run Loop Thread");
for (NSInteger i = 0; i < 5; i ++) {
NSLog(@"In Run Loop Thread, count = %ld", i);
sleep(1);
}
#if 0
// 错误示范
_runLoopThreadDidFinishFlag = YES;
// 这个时候并不能执行线程完成之后的任务,因为Run Loop所在的线程并不知道runLoopThreadDidFinishFlag被重新赋值。Run Loop这个时候没有被任务事件源唤醒。
// 你会发现这个时候点击屏幕中的UI,线程将会继续执行。 因为Run Loop被UI事件唤醒。
// 正确的做法是使用 "selector"方法唤醒Run Loop。 即如下:
#else
[self performSelectorOnMainThread:@selector(updateRunLoopThreadDidFinishFlag) withObject:nil waitUntilDone:NO];
#endif
NSLog(@"Exit Run Loop Thread");
}
- (void)updateRunLoopThreadDidFinishFlag
{
self.runLoopThreadDidFinishFlag = YES;
}
在Run Loop被启动之后,Thread执行任务时,UI线程能够正常接收到事件,不会被阻塞。Thread完成任务之后,继续执行下面的任务。
注意代码中的错误示范,正确的实现方式,需要在当前线程中使用上文提到的selector
系列方法激活目标线程的Run Loop来响应事件。
1) NSTimer, NSURLConnection和NSStream默认运行在Default Mode下,UIScrollView在接收到用户交互事件时,主线程Run Loop会设置为UITrackingRunLoopMode下,这个时候NSTimer不能fire,NSURLConnection的数据也无法处理。
// 在UITableViewController中启动一个NSTimer,每隔0.5秒刷新Label上的text,刷新100次后暂停。
@interface TestTableViewController ()
@property (nonatomic, strong) UILabel *testLabel;
@property (nonatomic) NSInteger count;
@property (nonatomic, strong) NSTimer *testTimer;
@end
@implementation TestTableViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.count = 0;
self.testLabel = [[UILabel alloc] initWithFrame:CGRectMake(20, 100, 100, 50)];
[self.view addSubview:self.testLabel];
self.testTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(updateTestLabel) userInfo:nil repeats:YES];
}
- (void)updateTestLabel
{
self.count ++;
self.testLabel.text = [NSString stringWithFormat:@"%ld", self.count];
if (self.count == 100) {
[self.testTimer invalidate];
self.testTimer = nil;
}
}
@end
上述代码,在正常情况下Label可以刷新text,但是在用户拖动TableView时,label将不在更新,直到手指离开屏幕。 解决方法,一种是修改Timer运行的Run Loop模式,将其加入NSRunLoopCommonModes中。
/* self.testTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(updateTestLabel) userInfo:nil repeats:YES]; */
self.testTimer = [NSTimer timerWithTimeInterval:0.5 target:self selector:@selector(updateTestLabel) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.testTimer forMode:NSRunLoopCommonModes];
另外一种解决方法是在另外一个线程中处理Timer事件,在perform到主线程去更新Label。
NSURLConnection和NSTimer也大致如此,其中注意NSURLConnection要使用- (id)initWithRequest:(NSURLRequest *)request delegate:(id)delegate startImmediately:(BOOL)startImmediately
生成NSURLConnection对象,并且第三个参数要设置为NO。之后再用- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode
设置Run Loop与其模式。
关于NSURLConnection的最佳实践可以参考AFNetworking。
2)NSTimer的构造方法会对传入的target对象强引用,直到这个timer对象invalidated。在使用时需要注意内存问题,根据需要,要在适当的地方调用invalidated方法。
3) 运行一次的Timer源也可能导致Run Loop退出:一次的Timer在执行完之后会自己从Run Loop中删除,如果使用while来驱动Run Loop的话,下一次再运行Run Loop就可能导致退出,因为此时已经没有其他的源需要监控。