RunLoop是什么
什么是RunLoop,顾名思义,RunLoop就是在‘跑圈’,其本质是一个do while循环。
一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线程能随时处理事件但并不退出,通常的代码逻辑是这样的:
do {
//接受消息->等待->处理
}while(message != quit)
线程执行了这个函数后,就会一直处于这个函数内部 “接受消息->等待->处理” 的循环中,直到这个循环结束(比如传入 quit 的消息),函数返回。
RunLoop有什么
在 CoreFoundation 中关于 RunLoop 有 5 个类:
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
一个Runloop对应一条线程,一个runloop里面可以有多个CFRunLoopModeRef(模式)。
同一个时刻,RunLoop只能是在一个mode上面的运行。如果需要切换mode,只能是退出currentMode,切换到指定的 mode。
每一个mode又可以包含多个 source/timer/observer。不同mode里面的子元素,互不影响。
5个类的对应关系大概是:
RunLoopMode
一个 run loop mode 是若干个 source、timer 和 observer 的集合。它能帮我们过滤掉一些不想要的事件。
一个 RunLoop 在某个 mode 下运行时,不会接收和处理其他 mode 的事件。
要保持一个 mode 活着,就必须往里面添加至少一个 source、timer 或 observer 。
NSDefaultRunLoopMode是默认的模式,程序运行的大多时候都处于该 mode 下
UITrackingRunLoopMode是滑动 tableView 或 scrollerView 时为了界面流畅而用的 mode
例如:
NSDefaultRunLoopMode有需要执行的item时,Loop会执行该item
如果UITrackingRunLoopMode有需要执行的item时,Loop会切换到UITrackingRunLoopMode执行相应的item,而NSDefaultRunLoopMode下的item会暂停执行
当UITrackingRunLoopMode的item执行完毕时,Loop会继续执行NSDefaultRunLoopMode的item
CFRunLoop里面有一个伪mode叫做 kCFRunLoopCommonModes,它不是一个真正的 mode,而是若干个 mode 的集合。
只要加入到了 CommonModes 里面,就相当于添加到了它里面所有的 mode 中。
RunLoopSource
source 是事件产生的地方(输入源),虽然官方文档在概念上把 source 分为三类:Port-Based Sources,Custom Input Sources,Cocoa Perform Selector Sources。
但在源码中 source 只有两个版本:source0 和 source1,它们的区别在于它们是怎么被标记 (signal) 的。
source0 是app内部的消息机制,使用时需要调用 CFRunLoopSourceSignal()来把这个 source 标记为待处理,然后掉用 CFRunLoopWakeUp() 来唤醒 RunLoop,让其处理这个事件。
source1 是基于 mach_ports 的,用于通过内核和其他线程互相发送消息。
iOS / OSX 都是基于 Mach 内核,Mach 的对象间的通信是通过消息在两个端口(port)之间传递来完成。
很多时候我们的 app 都是处于什么事都不干的状态,在空闲前指定用于唤醒的 mach port 端口,然后在空闲时被 mach_msg() 函数阻塞着并监听唤醒端口,mach_msg() 又会调用 mach_msg_trap() 函数从用户态切换到内核态,这样系统内核就将这个线程挂起,一直停留在 mach_msg_trap 状态。直到另一个线程向内核发送这个端口的 msg 后,trap 状态被唤醒,RunLoop 继续开始干活。
其实,总结下来,事件产生的地方就是source(输入源),运用发消息的机制,让事件可以唤醒休眠的RunLoop执行。
示例代码:
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
self.source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_source_set_timer(self.source, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0);
dispatch_source_set_event_handler(self.source, ^{
NSLog(@"%@", [NSThread currentThread]);
});
dispatch_resume(self.source);
RunLoopTimer
这里的 timer 看起来名字是用C语言的样式,其实是完全等价于我们在OC里面的计时器NSTimer。
所以,在平时编程的过程中,我们最开始意识到有 runloop 这回事,就是了使用 NSTimer 的时候。这一点在后面的具体场景应用会详细提到的。
另外,这个 CFRunLoopTimerRef,还可以由方法 performSelector:afterDelay:来触发。(因为,本质上 afterDelay, 底层就是启动了 timer ,不然怎么检测具体时间,然后调用回调方法呢。)
示例代码:
NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(doSomething) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
- (void)addTimer {
NSThread *thread = [[NSThread alloc] initWithBlock:^{
NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(doSomething) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
}];
[thread start];
}
- (void)doSomething {
NSLog(@"do something");
if (_finished) {
[NSThread exit];
}
}
RunLoopObserver
主要用途就是监听 RunLoop 的状态变化。
它可以监听RunLoop的7种状态:
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
// 即将进入 loop
kCFRunLoopEntry = (1UL << 0),
// 即将处理 timer
kCFRunLoopBeforeTimers = (1UL << 1),
// 即将处理 source
kCFRunLoopBeforeSources = (1UL << 2),
// 即将 sleep
kCFRunLoopBeforeWaiting = (1UL << 5),
// 刚被唤醒,退出 sleep
kCFRunLoopAfterWaiting = (1UL << 6),
// 即将退出
kCFRunLoopExit = (1UL << 7),
// 全部的活动
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
示例代码:
static void observerCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
UIViewController *controller = (__bridge UIViewController *)info;
}
- (void)addRunLoopObserver {
CFRunLoopObserverContext context = {
0,
(__bridge void *)self,
&CFRetain,
&CFRelease,
NULL
};
static CFRunLoopObserverRef observerRef;
observerRef = CFRunLoopObserverCreate(NULL, kCFRunLoopAfterWaiting, YES, 0, observerCallBack, &context);
CFRunLoopRef runLoopRef = CFRunLoopGetCurrent();
CFRunLoopAddObserver(runLoopRef, observerRef, kCFRunLoopCommonModes);
CFRelease(observerRef);
}
RunLoop如何运行
1. 获取RunLoop
要获取主线程或当前线程对应的 RunLoop,只能通过 CFRunLoopGetMain 或 CFRunLoopGetCurrent 函数。
runloop 和 pthread_t (也就是线程)是一一对应的。
这样一一对应的关系是保存在一个全局的 dictionary 中的。
内部产生 runloop 的机制,与我们 iOS 开发中常用的懒加载很相似。
只有到了第一次要使用的时候,才会创建。当线程销毁的时候,也销毁相应的 runloop。
2. runloop 运行逻辑
runloop 整个的运行逻辑都是在于三个重要的对象如何运作:source (输入源)、timer (定时器)、observer (观察者)。
上面的关于 runloop 的相关类里面有过介绍,observer 时刻监听,整个 runloop 的7种 状态的变化。
在上面 7 种状态里面,对应着不同的处理。
RunLoop和线程之间的关系
虽然RunLoop与Thread的关系十分密切,但是并不是每个Thread都拥有一个RunLoop的
在iOS中,除了系统会为主线程自动创建一个RunLoop,在子线程中,我们需要手动获取线程对应的RunLoop:
[NSRunLoop mainRunLoop]
[NSRunLoop currentRunLoop]
- RunLoop和Thread是一一对应的(key: pthread value:runLoop)
- Thread默认是没有对应的RunLoop的,仅当主动调用Get方法时,才会创建
- 所有Thread线程对应的RunLoop被存储在全局的__CFRunLoops字典中。同时,主线程在static CFRunLoopRef __main,子线程在TSD中,也存储了线程对应的RunLoop,用于快速查找
这里有一点要弄清,Thread和RunLoop不是包含关系,而是平等的对应关系。Thread的若干功能,是通过RunLoop实现的
另一点是,RunLoop自己是不会Run的,需要我们手动调用Run方法(Main RunLoop会由系统启动),我们的RunLoop才会跑圈。静止(注意,这里的静止不是休眠的意思)的RunLoop是不会做任何事情的
示例代码:
- (void)multithread {
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
NSLog(@"step 1 %@", [NSThread currentThread]);
[self performSelector:@selector(doSomething) withObject:self afterDelay:3];
NSLog(@"step 2 %@", [NSThread currentThread]);
[[NSRunLoop currentRunLoop] run];
NSLog(@"step 3 %@", [NSThread currentThread]);
});
}
- (void)doSomething {
NSLog(@"do something %@", [NSThread currentThread]);
}
运行结果:
step 1 <NSThread: 0x6000027cf480>{number = 3, name = (null)}
step 2 <NSThread: 0x6000027cf480>{number = 3, name = (null)}
do something <NSThread: 0x6000027cf480>{number = 3, name = (null)}
step 3 <NSThread: 0x6000027cf480>{number = 3, name = (null)}
从中可以看出:
- 每个线程都有一个RunLoop,默认为休眠状态,需手动唤醒
[NSRunLoop run]
唤醒RunLoop,并使线程无法继续执行- timer结束后,RunLoop进入休眠状态,线程继续执行