RunLoop顾名思义也就是运行循环,在程序运行过程中循环执行某些任务。RunLoop的应用范畴包括以下几个方面:
- 定时器(Timer)、PerformSelector
- GCD Async Main Queue
- 事件响应、手势识别、界面刷新
- 网络请求
- AutoreleasePool
如果没有RunLoop的话,程序执行完之后就会立即退出。而使用RunLoop后,程序执行完成后并不会马上退出,而是保持运行状态。比如iOS项目中的main函数中UIApplicationMain函数内部就实现了RunLoop。
1 | int main(int argc, char * argv[]) { |
UIApplicationMain内部逻辑的伪代码大致如下:
1 | int main(int argc, char * argv[]) { |
综上所述,RunLoop的基本作用是:
- 保持程序的持续运行;
- 处理App中的各种事件(比如触摸事件、定时器事件等);
- 节省CPU资源,提高程序性能:有待执行任务时执行任务,不执行任务时休眠。
RunLoop对象
iOS中有2套API来访问和使用RunLoop对象。其中一套基于OC的Foundation:NSRunLoop,另一套是基于C语言的Core Foundation:CFRunLoopRef。其中的NSRunLoop和CFRunLoopRef都代表着RunLoop对象。NSRunLoop是基于CFRunLoopRef的一层OC包装(这也是使用OC方式和C语言方式打印出来的主线程不同的原因,主线程真正的地址是使用C语言打印出来的地址)。CFRunLoopRef是开源的。CFRunLoopRef下载链接
获取当前线程的RunLoop对象:
1 | //OC方法,获取当前线程的RunLoop对象 |
获取主线程的RunLoop对象:
1 | NSRunLoop *runloop = [NSRunLoop mainRunLoop]; |
RunLoop与线程的关系
- 每条线程都有唯一的一个与之对应的RunLoop对象。
- RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value。
- 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取该线程时创建与之对应的RunLoop对象。
- RunLoop会在线程结束时销毁。
- 主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop。
RunLoop底层实现
Core Foundation中关于RunLoop的5个类:
- CFRunLoopRef
- CFRunLoopModeRef
- CFRunLoopSourceRef
- CFRunLoopTimerRef
- CFRunLoopObserverRef
RunLoop在源码中的定义:
1 | struct __CFRunLoop { |
我们将主要的成员变量挑选出来,简化一下:
1 | struct __CFRunLoop { |
- CFMutableSetRef _modes:_modes是一个类似数组的集合,但是其所储存的数据类型是CFRunLoopModeRef,而且是无序的。
- CFRunLoopModeRef _currentMode:当前模式。
CFRunLoopModeRef的定义如下:
1 | typedef struct __CFRunLoopMode *CFRunLoopModeRef; |
那么__CFRunLoopMode又是怎么定义的呢?
1 | struct __CFRunLoopMode { |
- CFMutableSetRef _sources0 和 CFMutableSetRef _sources1:这两个集合中存储的是 CFRunLoopSourceRef类型的数据。
- CFMutableArrayRef _observers:该集合中存储的是CFRunLoopObserverRef类型的数据。
- CFMutableArrayRef _timers:存储的是CFRunLoopTimerRef类型的数据。
综上所述可知,开头列出的5个类它们之间的关系是:CFRunLoopRef(__CFRunLoopMode)中存储着CFRunLoopModeRef类型的数据,CFRunLoopModeRef中存储着CFRunLoopSourceRef、 CFRunLoopTimerRef、CFRunLoopObserverRef类型的数据。
CFRunLoopModeRef
CFRunLoopModeRef代表RunLoop的运行模式。一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer。RunLoop启动时只能选择其中的一个Mode作为CurrentMode。如果需要切换Mode,只能退出当前Loop,再重新选择一个Mode进入,不同Mode中的Source0/Source1/Timer/Observer彼此分隔,互不影响。如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出该Mode。
CFRunLoopModeRef的5种Mode
CFRunLoopModeRef中一共有5种Mode,分别是:
- kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默认Mode,通常主线程是在这个Mode下运行。
- UITrackingRunLoopMode:界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响。
kCFRunLoopCommonModes:通用模式,一旦设置此模式表示同时监听kCFRunLoopDefaultMode和UITrackingRunLoopMode这两种模式。其实kCFRunLoopMode并不是一种真的模式,它只是一个标记。将定时器的RunLoop模式设置为kCFRunLoopCommonModes意味着定时器(NSTimer)可以在“CFMutableSetRef commonModes数组中存放的模式”下运行,而且kCFRunLoopDefaultMode和UITrackingRunLoopMode都存放在CFMutableSetRef commonModes数组中。
UIInitializationRunLoopMode:在刚启动 App 时进入的第一个 Mode,启动完成后就不再使用。
- GSEventReceiveRunLoopMode:用来接受系统事件的内部 Mode,通常用不到。
CFRunLoopModeRef的5种Mode中最常见常用的模式是kCFRunLoopDefaultMode(NSDefaultRunLoopMode)和UITrackingRunLoopMode这2种。
添加Observer监听RunLoop的所有状态
RunLoop的所有状态包括:
1 | /* Run Loop Observer Activities */ |
添加Observer监听RunLoop的所有状态,代码如下:
1 | CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { |
RunLoop的运行逻辑
RunLoop的大体上的运行逻辑其实就是循环处理某种模式下的Source0、Source1、Timers、Observers。那么Source0、Source1、Timers、Observers具体表示什么呢?
- Source0:
(1)触摸事件处理;
(2)performSelector:onThread:方法
- Source1:
(1)基于Port的线程间通信;
(2)系统事件捕捉
- Timers:
(1)NSTimers
(2)performSelector:WithObject:afterDelay:
- Observers:
(1)用于监听RunLoop的状态
(2)UI刷新(BeforeWaiting在休眠之前刷新UI)
(3)Autoreleasepool(BeforeWaiting)
RunLoop具体的运行逻辑是这样的:
- (1)通知Observers:进入RunLoop;
- (2)通知Observers:即将处理Timers;
- (3)通知Observers:即将处理Sources;
- (4)处理Blocks(比如通过CFRunLoopPerformBlock函数添加的Block);
- (5)处理Source0(可能会再次处理Blocks);
- (6)如果存在Source1,就跳转到第8步,处理Source1;
- (7)通知Observers:开始休眠,不再占用CPU资源(等待消息唤醒);
- (8)通知Observers:结束休眠(被某个消息唤醒);
✅处理Source1
✅处理Timer
✅处理GCD Async To Main Queue
(9)处理Blocks;
(10)根据前面的执行结果,决定如何操作:有可能不退出当前的RunLoop,而是回到第2步继续执行;也有可能结束当前RunLoop,切换到其他的模式。如果切换到其他模式的话,则会执行(11)步;
- (11)通知Observers:退出RunLoop。
RunLoop休眠的实现原理
RunLoop休眠(线程阻塞)的实现原理:平时执行应用层面的代码时处于用户态,当在用户态调用mach_msg()函数时会自动转化到内核态,并调用内核态的mach_msg()函数,此时如果没有消息需要处理就让线程休眠,如果有消息需要处理就唤醒线程,回归到用户态调用应用层面的API去处理消息。
RunLoop在实际开发中的应用
- (1)控制线程生命周期(线程保活)
线程保活的应用场景:频繁执行一个任务或者多个串行(非并发)任务,可以采用线程保活的方式。线程保活的方式比传统的“创建线程-销毁线程-再创建线程-再销毁…”更节省CPU资源且更高效。比如AFNetworking中后台网络请求就使用了线程保活这种技术。
- (2)解决NSTimer在滑动时停止工作(失效)的问题
NSTimer在滑动时失效的原因是NSTimer默认是工作在NSDefaultRunLoopMode模式下,而当我们滑动时,RunLoop会退出NSDefaultRunLoopMode模式,并进入UITrackingRunLoopMode模式,所有NSTimer失效。
- (3)监控应用卡顿
- (4)性能优化
RunLoop总结
1.讲讲RunLoop,项目中用过吗?
- 控制线程生命周期(线程保活)
- 解决NSTimer在滑动时停止工作(失效)的问题
2.RunLoop内部实现逻辑是怎样的?
答:参照RunLoop具体的运行逻辑示意图
3.RunLoop和线程的关系是怎样的?
- 每条线程都有唯一的一个与之对应的RunLoop对象。
- RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value。
- 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取该线程时创建与之对应的RunLoop对象。
- RunLoop会在线程结束时销毁。
- 主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop。
4.timer和RunLoop是怎样的关系?
- 从底层数据结构来看,RunLoop的CFRunLoop结构体中存储着 CFMutableSetRef _modes,_modes是一个类似数组的集合,其所储存的数据类型是CFRunLoopModeRef, CFRunLoopModeRef结构体中存放着CFMutableArrayRef _timers。此外,如果timer被设置为 kCFRunLoopCommonModes模式,那么timer也将被存放在 CFRunLoop结构体中的 CFMutableSetRef _commonModeItems数组中。
- 从RunLoop的运行逻辑来讲,timer会通知Observers结束休眠,唤醒线程来处理timer消息。
5.程序中添加每3秒响应一次的NSTimer,当拖动tableView时,timer可能无法响应要怎么解决?
NSTimer在滑动时失效的原因是NSTimer默认是工作在NSDefaultRunLoopMode模式下,而当我们滑动时,RunLoop会退出NSDefaultRunLoopMode模式,并进入UITrackingRunLoopMode模式,所有NSTimer失效。
【注意】[NSTimer scheduledTimerWithTimeInterval: repeats:block:]方法会自动将定时器添加到主线程的NSDefaultRunLoopMode模式下,如果要自定义RunLoop模式的话,可以使用timerWithTimeInterval方法创建定时器对象,并将定时器添加到当前线程的NSRunLoopCommonModes模式下(实际上是将timer定时器添加到了NSRunLoopCommonModes 模式下的CFMutableSetRef _commonModeItems数组中),这样就能解决timer失效的问题。代码如下:
1 | NSTimer *timer = [NSTimer timerWithTimeInterval:self.completionDelay target:self selector:@selector(completionDelayTimerFired) userInfo:nil repeats:YES]; |
6.RunLoop是怎么响应用户操作的,具体流程是什么样的?
当用户点击屏幕时,RunLoop内部的Source1会捕捉到该触屏事件,并将该事件包装成事件队列eventQueue交给Source0中进行处理。
7.说说RunLoop的几种状态
kCFRunLoopEntry = (1UL << 0), //即将进入RunLoop
kCFRunLoopBeforeTimers = (1UL << 1),//即将处理Timer
kCFRunLoopBeforeSources = (1UL << 2),//即将处理Source
kCFRunLoopBeforeWaiting = (1UL << 5),//即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6),//即将从休眠中唤醒
kCFRunLoopExit = (1UL << 7),//即将推出RunLoop
8.RunLoop的mode作用是什么?
RunLoop常见的mode有2种:kCFRunLoopDefaultMode(NSDefaultRunLoopMode)和UITrackingRunLoopMode。kCFRunLoopDefaultMode是默认模式,通常主线程是在这个模式下运行;UITrackingRunLoopMode用于ScrollView追踪触摸滑动,保证界面滑动时不受其他mode影响。
mode的作用是将不同模式下的Source0/Source1/Timer/Observer隔离开来,互不影响,这样就提高了执行效率和滑动流畅性。