[toc] ## RunLoop是什么?有什么作用? `RunLoop`:翻译过来是运行环路(中式翻译: 跑圈)。我们在创建命令行项目和创建ios项目时,发现命令行项目当最后一行代码执行完后项目就自动退出了,而ios项目确可以一直运行,知道用户手动点击退出按钮。这就是因为ios项目在main函数中自动创建了runLoop,从而可以使项目可以一直响应用户的操作。 ``` int main(int argc, char * argv[]) { @autoreleasepool { //这行代码 会自动创建主线程的RunLoop return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } } ``` 我们可以将这个过程我们可以简化成: ![](https://img.kancloud.cn/3a/ab/3aab8e377e133f62cb08fcdf96c468c9_891x487.png) 我们从这个过程可以看出RunLoop的基本作用: - 保持程序的持续运行 - 处理App中的各种事件(比如触摸事件、定时器事件等) - 节省CPU资源,提高程序性能:该做事时做事,该休息时休息 - ...... 我们平时开发中,涉及到RunLoop的挺多的,比如说定时器、手势识别、网络请求等等. ![](https://img.kancloud.cn/78/df/78dfa2947f327a8ae0ba4280dfdedbfa_750x500.png) ## 一、RunLoop的结构 iOS中有2套API来访问和使用RunLoop: >① Foundation:`NSRunLoop`,它是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API `不是线程安全`的。 >② Core Foundation:`CFRunLoopRef`,它提供了纯 C 函数的 API,所有这些 API都是`线程安全`的。([CFRunLoopRef](https://opensource.apple.com/tarballs/CF/)是开源的) 两者关系: ![](https://img.kancloud.cn/b3/ae/b3ae06021348f6257baa614e28ae11b5_592x416.png) 所以我们获取RunLoop对象也有两种方法: ``` // Foundation [NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象 [NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象 // Core Foundation CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象 CFRunLoopGetMain(); // 获得主线程的RunLoop对象 ``` 因为CFRunLoopRef是开源的,所以我们可以通过它来看一下它的实现结构。来到CFRunLoop.c文件中,找到了RunLoop的结构体定义: ``` //已剔除非必要部分struct __CFRunLoop { pthread_t _pthread; CFMutableSetRef _commonModes; CFMutableSetRef _commonModeItems; CFRunLoopModeRef _currentMode; CFMutableSetRef _modes; }; ``` >这里的Set和数组类似,只不过数组是有序的,而set是无序的,都是用来存放数据的,所以 CFMutableSetRef可以理解成可变数组,也就是说在一个RunLoop对象中,存储着一个线程对象,三个可变数组,一个当前模式。 **那么`CFRunLoopModeRef`又是什么呢**?我们找到了它的定义:   ``` typedef struct __CFRunLoopMode *CFRunLoopModeRef; //剔除了其他无关属性 struct __CFRunLoopMode { CFStringRef _name; CFMutableSetRef _sources0; CFMutableSetRef _sources1; CFMutableArrayRef _observers; CFMutableArrayRef _timers; }; ``` **所以RunLoop的结构是这样的:** ![](https://img.kancloud.cn/fc/9b/fc9b12191aa06c5bb986f318dcb1fe0e_1676x658.png) - `_pthread`就是RunLoop对应的`线程`,每条线程都有唯一的一个与之对应的RunLoop对象。 - `_commonModeItems`和`_commonModes`是用来存放某些特定模式和模式内事件的,接下来会讲到。 - `_currentMode`,RunLoop当前所处的模式,当前模式是从`_modes`里面选择的。 - `_modes`:RunLoop的运行模式,一共有`五种`,但是我们经常用的就两三种: >- `kCFRunLoopDefaultMode`: App的默认运行模式,通常`主线程`是在这个运行模式下运行 >- `UITrackingRunLoopMode`: `跟踪用户交互事件`(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode影响)页面滚动式所处的模式 >- `kCFRunLoopCommonModes`: `伪模式`,不是一种真正的运行模式 >- UIInitializationRunLoopMode:在刚启动App时第进入的第一个Mode,启动完成后就不再使用 >- GSEventReceiveRunLoopMode:接受系统内部事件,通常用不到 我们上面提到的`_commonModeItems`和`_commonModes`就是存放`kCFRunLoopCommonModes`这种模式的数据的。CommonModes其实并不是一种真正的模式,而是指可以在标记为`Common Modes`的模式下运行的`伪模式`。 简单来说目前kCFRunLoopCommonModes就是指: `kCFRunLoopDefaultMode+UITrackingRunLoopMode`。比如,我们经常遇到在tableview添加定时器后,当tableview滚动后timer就不响应了。 >这是因为tableview滚动式处在`UITrackingRunLoopMode`模式下的,而定时器默认是处在`kCFRunLoopDefaultMode`下的,所以当模式切换后,RunLoop就无法响应之前模式的时间了,故而无法响应定时器时间。所以我们的方案是将定时器添加到RunLoop的`kCFRunLoopCommonModes`模式下,这样无论是否滑动tableview都可以响应定时器事件了。 >这里还需要注意的一点是:如果需要`切换 Mode`,只能`退出Loop`,再重新指定一个 Mode 进入。这样做主要是为了`分隔`开不同组的 `Source/Timer/Observer`,让其互不影响。 **接下来,我们再来看一下这个RunLoop中的模式指的是什么?有什么作用?** 我们前面通过源码,看到了`CFRunLoopMode`的结构,里面有`sources0、sources1、timer、observers`,其实这里面就存储着app要处理的种种事情,它们分别负责不同的工作。它们的分工是这样的:(个人认为sources0和sources1其实是一个整体,当事件发生时sources1先去获取这个时间,涉及不到端口或内核或其他线程的事情的话就交给sources0处理,其余的自己处理) - `sources0`:只包含了一个`回调`(函数指针),它并不能主动触发事件,比如点击事件等操作都是通过sources0处理的。 - `sources1`:包含了一个 `mach_port` 和一个`回调`(函数指针),用于通过内核和其他线程相互发送消息,这种 Source 能`主动唤醒` RunLoop 的线程。 - `timer`:是基于`时间`的`触发器`,其包含一个`时间长度`和一个`回调`(函数指针)。当其加入到 RunLoop 时,RunLoop会`注册`对应的时间点,当时间点到时,RunLoop会被`唤醒`以`执行`那个回调。 - `observers`:是`观察者`,当 RunLoop 的`状态`发生变化时,观察者就能通过回调接收到这个变化。 RunLoop的状态有一下几种: ![](https://img.kancloud.cn/fc/74/fc746e215c2dda7797e1f734e5930c48_1262x517.png) **总结 `CFRunLoopMode`的图示:** ![](https://img.kancloud.cn/3b/70/3b7019d4b94582d8dd112466c5f6c59d_954x824.png) >需要注意的一点是:如果Mode里没有任何`Source0/Source1/Timer/Observer`,RunLoop会`立马退出`。 ## 二、RunLoop与线程 关于RunLoop与线程的关系,我们可以总结以下几点: >1. 每条线程都有`唯一`的一个`与之对应`的RunLoop对象 (也就是RunLoop宿主于线程) >2. 线程刚创建时并没有RunLoop对象,`RunLoop会在第一次获取它时创建` >3. `主线程的RunLoop已经自动获取(创建)`,`子线程默认没有开启RunLoop`,子线程没有开启RunLoop的话就跟命令行项目一样,任务执行完就会结束 >4. RunLoop保存在一个`全局的Dictionary里`,`线程作为key`,`RunLoop作为value` >5. RunLoop会在线程结束时`销毁 ` 接下来,我们通过源码来验证: 当我们获取线程的Runloop的时候,发现RunLoop没有获取到话,都会调用`__CFRunLoopGet0`, 并把线程作为参数传递 ![](https://img.kancloud.cn/be/88/be88039a30e46af90e6a99e73f0cc259_1080x434.png) 继续,跳转至__CFRunLoopGet0,如下: ![](https://img.kancloud.cn/ec/bf/ecbff424b5bcab4a6a25e8976cf69815_1080x792.png) >发现,RunLoop与线程的关系是`一对一`的,并且用了个`全局字典`保存了起来,`线程作为key`,`RunLoop作为value`。 我们发现如果线程没有启用RunLoop后会执行完马上销毁: ![](https://img.kancloud.cn/ee/1a/ee1a0718a00d4fd3293610eb0a8fd164_1334x600.png) >添加RunLoop后,发现还是运行完就销毁:这是因为如果`Mode里`没有任何`Source0/Source1/Timer/Observer`,RunLoop会立马退出 ![](https://img.kancloud.cn/f9/f3/f9f362694ce948e4f112b0c62377d917_1286x816.png) 所以我们需要往`Model中`添加一个数据: ![](https://img.kancloud.cn/eb/cf/ebcf0778e7820a1872af3d064e0f4dcf_1600x894.png) 发现确实执行完后,线程阻塞了,一直没有被销毁,这是因为当runloop创建后,如果没有被事件唤醒后它就一直在`休眠`,cpu就不会继续处理事情,所以`阻塞`在这。 ## 三、RunLoop的运行逻辑  我们在了解RunLoop的结构以及与线程的关系后,我们再来看一下RunLoop的运行流程: ![](https://img.kancloud.cn/69/d8/69d8ad58baa25db1b0b3430227b8c9ad_1345x703.png) **接下来,我们通过源码来看一下RunLoop是如何处理这些事件的?** 关于入口的查找,我们可以现在touchesBegan:方法中打个断点,查看程序是怎么执行到这的: ![](https://img.kancloud.cn/84/12/8412d35a6fcdc27906af370074e0e0d6_1778x876.png) ``` //RunLoop入口 SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */ //通知Observers 进入RunLoop __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry); //RunLoop的具体运行 result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode); //通知Observers 退出RunLoop __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit); return result; } //RunLoop的具体运行 static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) { int32_t retVal = 0; do { //通知Observers 即将处理Timers __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers); //通知Observers 即将处理Sources __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources); //处理Block __CFRunLoopDoBlocks(rl, rlm); //处理Sources0 if (__CFRunLoopDoSources0(rl, rlm, stopAfterHandle)) { //处理Block __CFRunLoopDoBlocks(rl, rlm); } Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR); // 如果当前是主线程的runloop,并且主线程有事情需要处理,则跳转至handle_msg处理,即跳过休眠 这条指令网上大部分说法是指判断Sources1中是否有事情处理,个人觉得这个说法不太对,这篇文章中有正面:资料 if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) { goto handle_msg; } //通知Observers 即将休眠 __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting); //开始休眠 __CFRunLoopSetSleeping(rl); //等待别的消息来唤醒当前线程 如果没有消息就会一直在这休眠 阻塞在这 cpu不工作 有消息的话则唤醒执行 __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy); //结束休眠 __CFRunLoopUnsetSleeping(rl); //通知Observers 结束休眠 __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting); //handle_msg handle_msg:; if (被timer唤醒) { //处理Timers __CFRunLoopDoTimers(rl, rlm, mach_absolute_time()) } else if (被gcd唤醒) { //处理gcd __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg); } else {//被sources1唤醒 //处理Sources1 __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) } //处理Block __CFRunLoopDoBlocks(rl, rlm); //处理返回值 if (sourceHandledThisLoop && stopAfterHandle) { retVal = kCFRunLoopRunHandledSource; } else if (timeout_context->termTSR < mach_absolute_time()) { retVal = kCFRunLoopRunTimedOut; } else if (__CFRunLoopIsStopped(rl)) { __CFRunLoopUnsetStopped(rl); retVal = kCFRunLoopRunStopped; } else if (rlm->_stopped) { rlm->_stopped = false; retVal = kCFRunLoopRunStopped; } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) { retVal = kCFRunLoopRunFinished; } } while (0 == retVal); return retVal; } ``` 简化成流程图 则是: ![](https://img.kancloud.cn/14/4a/144a09f3c28d6edd4bfc29792db981bf_1323x679.png) ## 四、RunLoop的应用 - [控制线程生命周期(线程保活)](https://www.cnblogs.com/chglog/p/9585068.html) - [解决NSTimer在滑动时停止工作的问题](https://blog.csdn.net/M316625387/article/details/83270639) - 监控应用卡顿 - 性能优化