RunLoop
——-一次偶然的机会和以前同事群里面聊天,他们要让我写一篇关于RunLoop的文章,作为内部分享所用,于是我就开始准备看了很多大量的资料,官方文档连接为:
https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html
。其中有一篇在CocoaChina上写的最为实用,不难看出该作者功底还是很深,连接
http://www.cocoachina.com/ios/20150601/11970.html
,
当我把该文章分享给同事时,他们大部分人都表示看的不太懂,让我给一篇更加浅显的,我只有抱着死磕自己的态度,继续前行。
那我们就直接进入主题聊聊RunLoop
一:首先我们看看RunLoop到底干什么用的
a. 一个程序之所以能够持续不断的运行就是因为RunLoop
b.事件循环(Event Loop),能够处理各种事件,比如:定时器事件,PerformSelecter事件,手势识别,触摸事件等;
c. 他就像你的保姆一样,让她干活的时候她就干活,不让她干活的时候她就会主动休息,时刻准备着为你服务(里面相当于一个do–while循环,后面将详细介绍)。
二:OSX/iOS 系统中,有两种方式来访问和使用RunLoop
第一种.CFRunLoopRef代表着RunLoop对象,隶属于CoreFoundation 框架内的,纯C语言的IPA,这些IPA都是线程安全的。
第二种.NSRunLoop是基于CFRunLoopRef的OC包装,而且不是线程安全的。
三.RunLoop与线程的关系
a. 每一条线程都有对应的RunLoop对象,就像主线程一样,它只所以不死就是因为对应的RunLoop对象不死。所以如果你想让一个子线程不死,只要保证对应的RunLoop对象不死即可。线程和RunLoop对象是保存在全局的字典中的,以线程作为key,以RunLoop作为值。
b. RunLoop在第一次或取时被创建(苹果不允许直接创建,只能被获取,后面将会详细介绍),线程执行任务完将会被销毁。而且程序一起动就会默认创建了主线程,这时主线程对应的RunLoop对象也已经创建完毕,子线程的RunLoop对象需要你手动创建(说创建,其实就是直接获取,后面将详细介绍,这个东西不能急)。
四.由于RunLoop不断处理用户的交互事件,那么我们来探讨一下它是怎样处理的,首先,我们先了解一下牵扯到的类有哪些:
在 CoreFoundation 里面关于 RunLoop 有5个类:
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
看到这些类之后,你可能就蒙逼了,这个很正常。下面我们将详细介绍这些类是干什么的。先看一幅图:
到这里你只需要知道各个类的对应关系。
// [[NSRunLoop currentRunLoop] runMode:<#(NSString *)#> beforeDate:<#(NSDate *)#>];
当使用RunLoop时,需要传人一个Mode.这时系统为我们注册了5个Mode:
一. UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响.
二. kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行.
三. GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常不用.
四.UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用.
五.kCFRunLoopCommonModes: 这是一个占位用的Mode,你可以理解它就是一个标记或标签,用于标记上面的某些模式,不是一种真正的Mode,(使用时基本代表默认模式和跟踪模式,在主线程中,后面会讲到).
现在你已经对Mode有一定的了解,先别管会不会用,后面将详细讲解用法。
五:我们看看Source是干什么的
先看看苹果官方在文档里面给的一幅图片:
这时看这幅图也许有些蒙蒙的。先看右上角Input Sources 这部分就展示了Source的作用:我们称Source为事件源或输入源。现在你还是只需要知道Source就是一些事件,比如按钮点击事件,触摸事件,performSelector事件等。
我们通常把Source分为:Source0和Source1(这里我们是以函数调用栈来划分的,还有以其他方式划分我们先不予介绍)
Source0:非基于Port的
Source1:基于Port的,通过内核和其他线程通信,把接收和分发的系统事件交给Source0处理。
到这个时候,你只需要知道Source就是代表着一些事件,当有事件就会通知RunLoop这时RunLoop就会跑一遍,用来处理这些事件。当没有事件的时候他就会制动进入休息状态(后面将详细介绍处理过程)。
六:既然说到事件,那我们继续聊聊CFRunLoopTimerRef是干什么的
CFRunLoopTimerRef是基于时间的触发器。你可以认为他就是NSTimer。
说到NSTimer大家肯定就比较熟悉了,那我们就从熟悉的来。上代码
//创建定时器对象
NSTimer *timer = [NSTimer timerWithTimeInterval:3.0 target:self selector:@selector(goOn) userInfo:nil repeats:YES];
//把定时器添加RunLoop中,给个默认模式
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
原理:当定时器加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行定时器要执行的方法。
前面我们就已经提到,RunLoop在某一时刻只能在一种模式下。而该模式又包括相应的NSTimer对象,当把NSTimer添加到默认模式下时,这时定时器就会在默认模式下工作,当RunLoop由于某种原因要改变模式时,该定时器就会失效。
比如当有定时器时,用户又在拖拽tableview,此时RunLoop会从NSDefaultRunLoopMode切换到UITrackingRunLoopMode模式,这时定时器就会失效,如果不想让定时器失效,可以把定时器添加在跟踪模式下。当然你也可以这样干比如:
//创建定时器对象
NSTimer *timer = [NSTimer timerWithTimeInterval:3.0 target:self selector:@selector(goOn) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
//在前面我们讲到有五种模式,其中NSRunLoopCommonModes不是一种真正的模式,它就是一种标记,表示如果其它四种之一被标记为CommonModes,就会在该模式下工作。在主线程RunLoop中kCFRunLoopDefaultMode 和 UITrackingRunLoopMode这两个 Mode 都已经被标记为"Common"属性,所以该代码在这两种模式下都能正常工作。此时你无论怎样拖拽tableview,定时器都会正常工作。
读到这里你已经对NSTimer有了一个基本的认知,最起码要知道定时器的事件也是交给RunLoop处理的。
七. 接下来谈谈CFRunLoopObserverRef是干什么用的
作用:CFRunLoopObserverRef能够监听RunLoop的状态的改变,我们称之为观察者。
既然它是监听RunLoop状态的,那么我们看看RunLoop到底都有什么状态:
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出Loop
也就是说当RunLoop的这些状态发生改变的时候,Observer就会立即知道。
八.你已经对RunLoop有了基本的了解,下面我们看看它的整个循环过程,
先看一幅图,下面将一步一步解析:
1.当RunLoop即将启动或已经启动时会通知观察者。
// 1. 通知 Observers: RunLoop 即将进入 loop。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
// 内部函数,进入loop
__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {}
2.当即将要处理定时器事件时会通知观察者。
// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
3.当即将启动非基于端口的源时会通知观察者。
// 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
// 执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
4.启动准备好的非基于端口的源。
// 4. RunLoop 触发 Source0 (非port) 回调。
sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
// 执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
5.当基于端口的源已准备好时,就会立即启动,然后处理一些未处理的事件(跳到第九步)。
// 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.线程休眠直到下面任何事件发生:
a. 定时器启动
b. RunLoop设置的时间已经超时
b. 外部手动唤醒
d. 某一事件到达基于端口的源
// 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.通知观察者线程被唤醒。
// 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
9.处理事件:
a. 如果用户定义的定时器启动,就处理定时器事件并重启RunLoop.跳到第二步。
b. 如果RunLoop被显示唤醒并且还没有超时,重启RunLoop.跳到第二步。
c. 如果输入源启动,传递相应的消息。
// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。
if (msg_is_timer) {
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
}
// 9.2 如果有dispatch到main_queue的block,执行block。
else if (msg_is_dispatch) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}
// 9.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);
10.RunLoop结束,通知观察者。
// 10. 通知 Observers: RunLoop 即将退出。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}
上面基本上是RunLoop的整个运作过程,其实在第一步之前,也就是当RunLoop即将启动或已经启动通知观察者之前,RunLoop还要找到对应的Mode,以及判断Mode里面有没有source/timer/observer,如果没有就会直接返回。
// 首先根据modeName找到对应mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
// 如果mode里没有source/timer/observer, 直接返回。
if (__CFRunLoopModeIsEmpty(currentMode)) return;
九:RunLoop常见获取方式
a. Core Foundation
CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
CFRunLoopGetMain(); // 获得主线程的RunLoop对象
b. Foundation
[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
十:当需要有些子线程不死,并且在子线程中频繁的做一些耗时操作时,我们可以让一个子线程不死。子线程的特性:默认情况下当子线程把耗时操作做完之后就会销毁。现在先明确一下需求,我们想当一个子线程执行一个耗时操作之后不让它销毁,也就是还可以在该线程中继续执行其他的耗时操作。在上面我们讲RunLoop与线程的关系时,要想让子线程不死,只需让对应的RunLoop对象不死即可。我们模拟一下该情况:
@interface ViewController ()
/**线程对象为了更好的拿到该线程来使用它,不要误认为把线程搞一个强引用,该线程就不会销毁,如果不信你可以自己尝试一下,我已经尝试过啦,会立即崩溃*/
@property (nonatomic,strong)NSThread *thread ;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
//创建一个线程,并让它执行love方法
self.thread = [[NSThread alloc]initWithTarget:self selector:@selector(love) object:nil];
//启动线程,这时就会执行对应的love方法
[self.thread start];
}
- (void)love
{
NSLog(@"%s",__func__);
//1.给RunLoop对象增加一个port,相当于增加一个Source,看到这如果你对AFNetworking有一定的了解的话,你会发现该作者就是这样干的
[[NSRunLoop currentRunLoop]addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
//2.让RunLoop跑起来
[[NSRunLoop currentRunLoop]run];
//注意:如果没有1,该RunLoop就会立即退出,因为RunLoop首先会判断Mode里面有没有source/timer/observer,如果没有就会直接退出,在主线程的RunLoop中系统已经默认加了。
//如果没有上面的1和2,该线程执行完love方法之后,就会立即销毁,当点击屏幕下面的touchesBegan:也就没有反应。
}
//就是因为在love里面相当于创建了RunLoop对象,所以每次点击屏幕时就会执行go方法。
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//在onThread:这个线程里面执行performSelector:这个方法,withObject:参数,waitUntilDone:是否等待
[self performSelector:@selector(go) onThread:self.thread withObject:nil waitUntilDone:NO];
}
- (void)go
{
NSLog(@"%s",__func__);
}