文章概要:
本文从4个方面讲解了RunLoop:(1)RunLoop介绍;(2)何时使用RunLoop;(3)RunLoop的使用;(4)常见问题。本文算是一个读别人博客的笔记,不是原创,只是按照自己的思路整理了一下。
参考资料:
1. RunLoop介绍:
RunLoop的本质:线程中的循环。
它用来接受循环中的事件和安排线程工作,并在没有工作时,让线程进入睡眠状态(休息时,RunLoop会让CPU释放出来去做其他的事情)。
以下是Event Loop的逻辑:
function loop() { initialize(); do { var message = get_next_message(); process_message(message); } while (message != quit); }
RunLoop 实际上就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行上面 Event Loop 的逻辑。线程执行了这个函数后,就会一直处于这个函数内部 "接受消息->等待->处理" 的循环中,直到这个循环结束(比如传入 quit 的消息),函数返回。
RunLoop和线程的关系:
线程和 Run Loop 之间是一一对应的。Run Loop是线程的基础架构部分。每个线程都有对应的Run Loop。
在任何一个Cocoa程序的线程中,都可以通过以下代码来获取到当前线程的Run Loop:
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
ios程序启动执行的main函数如下
int main(int argc, char * argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([CCAppDelegate class])); } }
其中,UIApplicationMain()函数会为main thread设置NSRunLoop对象,也就是说程序启动时,主线程的RunLoop对象启动,所以,我们的App可以在无人操作时休息,有人操作时立马响应。对其它线程来说,run loop默认是没有启动的。也就是说,线程刚创建时并没有 RunLoop,如果不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。只能在一个线程的内部获取其 RunLoop(主线程除外)。
RunLoop Mode: 一个集合(包括:所有要监视的事件源和要通知的Run Loop中注册的观察者)
RunLoop Mode包含Source、Observer、Timer
- Source(事件源,输入源,基于端口事件源例如键盘触摸等)
- Observer(观察者,观察当前RunLoop运行状态)
- Timer(定时器事件源)
RunLoop Mode的种类有:
- NSDefaultRunLoopMode(kCFRunLoopDefaultMode):默认,空闲状态
- UITrackingRunLoopMode:使用这个Mode去跟踪来自用户交互的事件,比如 ScrollView滑动时。
- UIInitializationRunLoopMode:启动时
- NSRunLoopCommonModes(kCFRunLoopCommonModes):占位用的Mode,作为标记kCFRunLoopDefaultMode和UITrackingRunLoopMode用。
RunLoop以一种固定的Mode运行,只会监控这个Mode下添加的Input source和Timer source,如果这个Mode下没有添加事件源,RunLoop就会立即返回。Run Loop被事件源触发,然后Run Loop中注册的观察者得到通知,执行对应的函数。
Source:
事件源的分类如下所示:
RunLoop事件源分类
其中,Input source分为两种:
Source0:非基于Port的,用于用户主动触发的事件(自定义输入源,点击button 或点击屏幕)
Source1:基于Port的 通过内核和其他线程相互发送消息(基于端口的输入源,与内核相关)
- 输入源传递异步事件,通常消息来自于其他线程或程序。
- 定时源则传递同步事件,发生在特定时间或者重复的时间间隔。
其中,Port-Based sources是系统的源,Custom input sources是自定义输入源,除了可以通过CFRunLoopSourceCreate创建自定义输入源,还有一种selector源也是属于Custom input sources:
//(1) 在主线程的Run Loop下执行指定的 @selector 方法 performSelectorOnMainThread:withObject:waitUntilDone: performSelectorOnMainThread:withObject:waitUntilDone:modes: //(2) 在当前线程的Run Loop下执行指定的 @selector 方法 performSelector:onThread:withObject:waitUntilDone: performSelector:onThread:withObject:waitUntilDone:modes: //(3) 在当前线程的Run Loop下延迟加载指定的 @selector 方法 performSelector:withObject:afterDelay: performSelector:withObject:afterDelay:inModes: //(4) 取消当前线程的调用 cancelPreviousPerformRequestsWithTarget: cancelPreviousPerformRequestsWithTarget:selector:object:
上面的(2)(3)performSelector方法,里面含有waitUntilDone、afterDelay字段,当调用NSObject的performSelector方法后,实际上其内部会创建一个 Timer并添加到当前线程的RunLoop中。所以如果当前线程没有RunLoop,则这个方法会失效。
举例:
/* (1) CFRunLoopSourceContext context = {0,op,NULL,NULL,NULL,NULL,NULL, ScheduleCallBack, CancelCallBack, PerformCallBack}; op即是我们实际加入到runloop的对象(包含我们要处理的task)剩下的三个回调函数分别在该source task 开始,取消,和结束的时候的情况下激发的,一般我们在schedulecallback里面就可以执行我们所需要执行的task。 (2) CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, kPriority, &context); kPriority是source run loop的优先级,如果始终定义为0,可能会导致有些task被忽略,所以还是可以人为的设定一些优先级。 */ CFRunLoopRef runLoop = CFRunLoopGetCurrent(); CFRunLoopSourceContext delegateSourceContext = {0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, HandleDelegateSource}; delegateSource = CFRunLoopSourceCreate(NULL, 0, &delegateSourceContext); CFRunLoopAddSource(runLoop, delegateSource, delegateSourceRunLoopMode); //将delegateSource 标记为待处理: CFRunLoopSourceSignal(delegateSource); //唤醒 RunLoop,让其处理这个事件: CFRunLoopWakeUp(CFRunLoopGetMain());
Observer:
RunLoopObserver
Observer的创建和使用如下:
//创建监听者 /* 第一个参数 CFAllocatorRef allocator:分配存储空间 CFAllocatorGetDefault()默认分配 第二个参数 CFOptionFlags activities:要监听的状态 kCFRunLoopAllActivities 监听所有状态 第三个参数 Boolean repeats:YES:持续监听 NO:不持续 第四个参数 CFIndex order:优先级,一般填0即可 第五个参数 :回调 两个参数observer:监听者 activity:监听的事件 */ /* 所有事件 typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { kCFRunLoopEntry = (1UL << 0), // 即将进入RunLoop kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理Timer kCFRunLoopBeforeSources = (1UL << 2), // 即将处理Source kCFRunLoopBeforeWaiting = (1UL << 5), //即将进入休眠 kCFRunLoopAfterWaiting = (1UL << 6),// 刚从休眠中唤醒 kCFRunLoopExit = (1UL << 7),// 即将退出RunLoop kCFRunLoopAllActivities = 0x0FFFFFFFU }; */ CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { switch (activity) { case kCFRunLoopEntry: NSLog(@"RunLoop进入"); break; case kCFRunLoopBeforeTimers: NSLog(@"RunLoop要处理Timers了"); 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添加监听者 /* 第一个参数 CFRunLoopRef rl:要监听哪个RunLoop,这里监听的是主线程的RunLoop 第二个参数 CFRunLoopObserverRef observer 监听者 第三个参数 CFStringRef mode 要监听RunLoop在哪种运行模式下的状态 */ CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode); /* CF的内存管理(Core Foundation) 凡是带有Create、Copy、Retain等字眼的函数,创建出来的对象,都需要在最后做一次release GCD本来在iOS6.0之前也是需要我们释放的,6.0之后GCD已经纳入到了ARC中,所以我们不需要管了 */ CFRelease(observer);
3. 何时使用RunLoop?
Run Loop的优点
(1)NSRunLoop是一种消息处理模式,它对消息处理的过程进行了封装,使App程序员不用处理一些很琐碎很低层次的具体消息的处理,在NSRunLoop中每一个消息就被打包在input source或者是timer source中了。
(2)使用run loop可以使你的线程在有工作的时候工作,没有工作的时候休眠,这可以大大节省系统资源。
何时使用Run Loop?
在创建辅助线程的时候,才显式的运行一个Run Loop。对于辅助线程,我们仍然需要判断是否需要启动Run Loop。下面是官方Document提供的使用Run Loop的几个场景:
- 需要使用Port-Based Input Source或者Custom Input Source和其他线程通讯时
- 需要在线程中使用Timer
- 需要在线程中使用上文提到的
selector
相关方法(Cocoa框架为我们定义了一些Custom Input Sources,允许我们在线程中执行一系列selector
方法) - 需要让线程执行周期性的工作
4. RunLoop的使用
(1)两个自动获取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()); }
内部逻辑说明:
(1.1)线程和RunLoop是一一对应的;
(1.2)创建:有线程不一定会有RunLoop,RunLoop的创建发生在第一次获取时;
(1.3)销毁:线程销毁时,销毁RunLoop;
(2) 启动RunLoop 和 退出RunLoop
启动RunLoop (1)- (void)run; (2)- (void)runUntilDate:(NSDate *)limitDate;设置超时时间(程序看整个生命周期) (3)- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;特定的Mode(程序看启动停止) 退出RunLoop (1)设置超时时间;(上面的2) (2)使用CFRunLoopStop方法通知RunLoop停止。(上面的1、3)
6. 常见问题
(1)以+ scheduledTimerWithTimeInterval...的方式触发的timer,在滑动页面上的列表时,timer会暂定回调,为什么?如何解决?
- (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;
主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。这两个 Mode 都已经被标记为”Common”属性。DefaultMode 是 App 平时所处的状态,TrackingRunLoopMode 是追踪 ScrollView 滑动时的状态。
RunLoop只能运行在一种mode下,如果要换mode,当前的loop也需要停下重启成新的。利用这个机制,ScrollView滚动过程中NSDefaultRunLoopMode(kCFRunLoopDafaultMode)的mode会切换到UITrackingRunLoopMode来保证ScrollView的流畅滑动:只能在NSDefaultRunLoopMode模式下处理的事件会影响scrollView的滑动。
如果我们把一个NSTimer对象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主运行循环中的时候,ScrollView滚动过程中会因为mode的切换,而导致NSTimer将不再被调度。
同时因为mode还是可定制的,所以:Timer计时会被scrollView的滑动影响的问题可以通过将timer添加到NSRunLoopCommonModes(kCFRunLoopCommonModes)来解决。
验证代码如下,其中TestTimerAndScrollController是UITableViewController:
typedef NS_ENUM(NSUInteger, LVRunLoopTimerTestMethodType) { LVRunLoopTimerTestMethodTypeRunLoopModeTypeDefault, LVRunLoopTimerTestMethodTypeRunLoopModeTypeCommonModes, }; @interface TestTimerAndScrollController () @property (nonatomic, strong) UILabel *testLabel; @property (nonatomic) NSInteger count; @property (nonatomic, strong) NSTimer *testTimer; @end @implementation TestTimerAndScrollController - (void)viewDidLoad { [super viewDidLoad]; self.count = 0; self.testLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 100, self.tableView.frame.size.width, 50)]; self.testLabel.backgroundColor = [UIColor lightGrayColor]; self.testLabel.font = [UIFont systemFontOfSize:40]; self.testLabel.textAlignment = NSTextAlignmentCenter; [self.view addSubview:self.testLabel]; LVRunLoopTimerTestMethodType type = LVRunLoopTimerTestMethodTypeRunLoopModeTypeDefault; switch (type) { case LVRunLoopTimerTestMethodTypeRunLoopModeTypeDefault: self.testTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(updateTestLabel) userInfo:nil repeats:YES]; break; case LVRunLoopTimerTestMethodTypeRunLoopModeTypeCommonModes: self.testTimer = [NSTimer timerWithTimeInterval:0.5 target:self selector:@selector(updateTestLabel) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:self.testTimer forMode:NSRunLoopCommonModes]; break; default: break; } } - (void)updateTestLabel { self.count ++; self.testLabel.text = [NSString stringWithFormat:@"%ld", self.count]; if (self.count == 100) { [self.testTimer invalidate]; self.testTimer = nil; } }
同样的,imageView在设置image时也是在NSDefaultRunLoopMode下执行的,如果在UIScrollView中滑动,我们需要把其设置为UITrackingRunLoopMode模式,否则image不会展示:
[imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@""] afterDelay:2 inModes:@[NSDefaultRunLoopMode,UITrackingRunLoopMode]];
(2)runloop和线程有什么关系?
总的说来,Run loop,正如其名,loop表示某种循环,和run放在一起就表示一直在运行着的循环。实际上,run loop和线程是紧密相连的,可以这样说run loop是为了线程而生,没有线程,它就没有存在的必要。Run loops是线程的基础架构部分, Cocoa 和 CoreFundation 都提供了 run loop 对象方便配置和管理线程的 run loop (以下都以 Cocoa 为例)。每个线程,包括程序的主线程( main thread )都有与之相应的 run loop 对象。
runloop 和线程的关系:
(2.1) 主线程的run loop默认是启动的。
iOS的应用程序里面,程序启动后会有一个如下的main()函数:
int main(int argc, char * argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } }
重点是UIApplicationMain()函数,这个方法会为main thread设置一个NSRunLoop对象,这就解释了:为什么我们的应用可以在无人操作的时候休息,需要让它干活的时候又能立马响应。
(2.2) 对其它线程来说,run loop默认是没有启动的,如果你需要更多的线程交互则可以手动配置和启动,如果线程只是去执行一个长时间的已确定的任务则不需要。
(2.3) 在任何一个 Cocoa 程序的线程中,都可以通过以下代码来获取到当前线程的 run loop 。
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
(3)runloop的mode作用是什么?
model 主要是用来指定事件在运行循环中的优先级的,分为:
NSDefaultRunLoopMode(kCFRunLoopDefaultMode):默认,空闲状态
UITrackingRunLoopMode:ScrollView滑动时
UIInitializationRunLoopMode:启动时
NSRunLoopCommonModes(kCFRunLoopCommonModes):Mode集合
苹果公开提供的 Mode 有两个:
NSDefaultRunLoopMode(kCFRunLoopDefaultMode)
NSRunLoopCommonModes(kCFRunLoopCommonModes)
Run Loop Mode可以理解为一个集合中包括所有要监视的事件源和要通知的Run Loop中注册的观察者。每一次运行自己的Run Loop时,都需要显式或者隐式的指定其运行于哪一种Mode。在设置Run Loop Mode后,你的Run Loop会自动过滤和其他Mode相关的事件源,而只监视和当前设置Mode相关的源(通知相关的观察者)。