在学习完内存管理与多线程的知识后,我又将目光瞄向了 Run Loop,不过受限于现阶段的能力,我在查阅了大量资料后,对于 Run Loop 的理解仍然很是浅显,因此本文绝大多数的内容,是参照网上大牛们的文章进行总结的。固然啦,我也但愿在不久的未来,对于 Run Loop 能有更多本身的观点与总结。html
首先看如下代码:前端
1 |
int main(int argc, char * argv[]) { |
不知道刚接触 iOS 开发的同窗有没有过这样的疑惑:咱们都知道 main
函数是程序的入口,可为什么当 main
函数执行完毕后,程序没有退出呢?而能在没有事情作的时候维持应用的运行的呢?ios
若是你是个好奇的宝宝,那么必定会去搜寻答案,没错,其实这背后便隐藏了今天的主角 Run Loop。segmentfault
如下来自苹果官方文档的介绍:安全
Run loops are part of the fundamental infrastructure associated with threads. A run loop is an event processing loop that you use to schedule work and coordinate the receipt of incoming events. The purpose of a run loop is to keep your thread busy when there is work to do and put your thread to sleep when there is none.网络
Run loop management is not entirely automatic. You must still design your thread’s code to start the run loop at appropriate times and respond to incoming events. Both Cocoa and Core Foundation provide run loop objects to help you configure and manage your thread’s run loop. Your application does not need to create these objects explicitly; each thread, including the application’s main thread, has an associated run loop object. Only secondary threads need to run their run loop explicitly, however. The app frameworks automatically set up and run the run loop on the main thread as part of the application startup process.多线程
通常来说,一个线程一次只能执行一个任务,执行完成后线程就会退出。若是咱们须要一个机制,让线程能随时处理事件但并不退出,那么就得让它循环。架构
因此,Run Loop 实际上就是一个对象,这个对象管理了其须要处理的事件和消息,并提供了一个入口函数来执行任务。线程执行了这个函数后,就会一直处于这个函数内部 “接受消息->等待->处理” 的循环中,直到这个循环结束(好比传入 quit 的消息),函数返回。app
因此,上面代码中 UIApplicationMain()
方法在这里不只完成了初始化咱们的程序并设置程序 Delegate 的任务,并且随之开启了主线程的 Run Loop,开始接受处理事件。这样咱们的应用就能够在无人操做的时候休息,须要让它干活的时候又能立马响应。框架
直接看图更容易理解:
在 OS X/iOS 系统中,提供了两个这样的对象:
• CFRunLoopRef:是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,全部这些 API 都是线程安全的。
• NSRunLoop:是基于 CFRunLoopRef 的封装,提供了面向对象的 API,可是这些 API 不是线程安全的。
首先来看一张关系图:
苹果不容许直接建立 Run Loop,它只提供了两个自动获取的函数:CFRunLoopGetMain()
和 CFRunLoopGetCurrent()
,这两个函数内部的逻辑大概是下面这样:
1 |
/// 全局的 Dictionary,key 是 pthread_t, value 是 CFRunLoopRef |
从上面的代码能够看出,线程和 Run Loop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚建立时并无 Run Loop,若是你不主动获取,那它一直都不会有。Run Loop 的建立是发生在第一次获取时,Run Loop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 Run Loop(主线程除外)。
在 CoreFoundation 里面关于 RunLoop 有 5 个类:
• CFRunLoopRef
• CFRunLoopModeRef
• CFRunLoopSourceRef
• CFRunLoopTimerRef
• CFRunLoopObserverRef
其中 CFRunLoopModeRef 类并无对外暴露,只是经过 CFRunLoopRef 的接口进行了封装。他们的关系以下:
对于上图的理解:一个 Run Loop 包含若干个 Mode,每一个 Mode 又包含若干个 Source/Timer/Observer。每次调用 Run Loop 的主函数时,只能指定其中一个 Mode,这个 Mode 被称做 CurrentMode。若是须要切换 Mode,只能退出 Loop,再从新指定一个 Mode 进入。这样作主要是为了分隔开不一样组的 Source/Timer/Observer,让其互不影响。
CFRunLoopSourceRef: 是事件产生的地方。Source 有两个版本:Source0 和 Source1:
• Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你须要先调用 CFRunLoopSourceSignal(source)
,将这个 Source 标记为待处理,而后手动调用 CFRunLoopWakeUp(runloop)
来唤醒 Run Loop,让其处理这个事件。
• Source1 包含了一个 mach_port 和一个回调(函数指针),被用于经过内核和其余线程相互发送消息。这种 Source 能主动唤醒 Run Loop 的线程,其原理在下面会讲到。
CFRunLoopTimerRef: 是基于时间的触发器,它和 NSTimer 是 Toll-Free Bridging 的,能够混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 Run Loop 时,Run Loop 会注册对应的时间点,当时间点到时,Run Loop 会被唤醒以执行那个回调。
CFRunLoopObserverRef: 是观察者,每一个 Observer 都包含了一个回调(函数指针),当 Run Loop 的状态发生变化时,观察者就能经过回调接受到这个变化。
Run Loop 对象处理的事件源分为两种:Input sources 和 Timer sources:
• Input sources:用分发异步事件,一般是用于其余线程或程序的消息,好比:performSelector:onThread:...
• Timer sources:用分发同步事件,一般这些事件发生在特定时间或者重复的时间间隔上,好比:[NSTimer scheduledTimerWithTimeInterval:target:selector:...]
上面图中展现了 Run Loop 的概念结构及各类事件源。其中 Input sources 分发异步事件给相应的处理程序而且调用 runUntilDate:
方法(这个方法会在该线程关联的 NSRunLoop 对象上被调用)来退出其 Run Loop。Timer sources 分发事件到相应的处理程序,但不会引发 Run Loop 退出。
Input sources 有两个不一样的种类: Port-Based Sources 和 Custom Input Sources。Run Loop 自己并不关心 Input sources 是哪种类型。系统会实现两种不一样的 Input sources 供咱们使用。这两种不一样类型的 Input sources 的区别在于:Port-Based Sources 由内核自动发送,Custom Input Sources 须要从其余线程手动发送。
Custom Input Sources
咱们可使用 Core Foundation 里面的 CFRunLoopSourceRef 类型相关的函数来建立 Custom Input Sources。
Port-Based Sources
经过内置的端口相关的对象和函数,配置基于端口的 Input sources。(好比在主线程建立子线程时传入一个 NSPort 对象,主线程和子线程就能够进行通信。NSPort 对象会负责本身建立和配置 Input sources。)
Timer sources 在预设的时间点同步的传递消息,Timer 是线程通知本身作某件事的一种方式。
Foundation 中 NSTimer Class 提供了相关方法来设置 Timer sources。须要注意的是除了 scheduledTimerWithTimeInterval
开头的方法建立的 Timer 都须要手动添加到当前 Run Loop 中。(scheduledTimerWithTimeInterval
建立的 Timer 会自动以 Default Mode 加载到当前 Run Loop中。)
Timer 在选择使用一次后,在执行完成时,会从 Run Loop 中移除。选择循环时,会一直保存在当前 Run Loop 中,直到调用 invalidated 方法。
Run Loop Mode 是指要被监听的事件源(包括 Input sources 和 Timer sources)的集合 + 要被通知的 run-loop observers 的集合。每一次运行本身的 Run Loop 时,都须要显示或者隐示的指定其运行于哪种 Mode。在设置 Run Loop Mode 后,你的 Run Loop 会自动过滤和其余 Mode 相关的事件源,而只监视和当前设置 Mode 相关的源(通知相关的观察者)。大多数时候,Run Loop 都是运行在系统定义的默认模式上。
首先咱们能够看一下 App 启动后 Run Loop 的状态:
1 |
CFRunLoop { |
咱们能够看到,系统默认注册了 5 个 Mode:
下图列出了 Cocoa 和 Core Foundation 中定义的一些 Modes:
CFRunLoopMode 和 CFRunLoop 的结构大体以下:
1 |
struct __CFRunLoopMode { |
这里有个概念叫 “CommonModes”:一个 Mode 能够将本身标记为 “Common” 属性(经过将其 Mode Name 添加到 RunLoop 的 “commonModes” 中)。每当 Run Loop 的内容发生变化时,Run Loop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具备 “Common” 标记的全部 Mode 里。
应用场景举例:主线程的 Run Loop 里有两个预置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。这两个 Mode 都已经被标记为 “Common” 属性。DefaultMode 是 App 平时所处的状态,TrackingRunLoopMode 是追踪 ScrollView 滑动时的状态。当你建立一个 Timer 并加到 DefaultMode 时,Timer 会获得重复回调,但此时滑动一个 TableView 时,Run Loop 会将 mode 切换为 TrackingRunLoopMode,这时 Timer 就不会被回调,而且也不会影响到滑动操做。
有时你须要一个 Timer,在两个 Mode 中都能获得回调,一种办法就是将这个 Timer 分别加入这两个 Mode。还有一种方式,就是将 Timer 加入到顶层的 Run Loop 的 “commonModeItems” 中。”commonModeItems” 被 Run Loop 自动更新到全部具备 “Common” 属性的 Mode 里去。
你只能经过 Mode Name 来操做内部的 Mode,当你传入一个新的 Mode Name 但 Run Loop 内部没有对应 Mode 时,Run Loop会自动帮你建立对应的 CFRunLoopModeRef。对于一个 Run Loop 来讲,其内部的 Mode 只能增长不能删除。
苹果公开提供的 Mode 有两个:kCFRunLoopDefaultMode (NSDefaultRunLoopMode) 和 UITrackingRunLoopMode,你能够用这两个 Mode Name 来操做其对应的 Mode。
同时苹果还提供了一个操做 Common 标记的字符串:kCFRunLoopCommonModes (NSRunLoopCommonModes),你能够用这个字符串来操做 Common Items,或标记一个 Mode 为 “Common”。使用时注意区分这个字符串和其余 Mode Name。
对比上面说的事件源——它们是在特定的同步事件或异步事件发生时被触发,Run Loop Observers 就不同了,它是在 Run Loop 执行本身的代码到某一个指定位置时被触发。咱们能够用 Run Loop Observers 来跟踪到这些事件:
与 Timer 相似,Run Loop Observers 也能够只观察一次或者反复观察。只观察一次的话,就在 fire 后把本身从 Run Loop 中给移除掉就好了
当你为一个须要长时间运行的线程配置 Run Loop 时,最好是能添加至少一个 Input source 到 Run Loop 中,这比用 Timer source 更好,Timer 要么一次,触发完了,就会结束,而以后 Run Loop 也就结束了,要么循环,这样就会致使周期性地唤醒线程,这其实是一种轮询的形式。与之相反,Input source会一直等待对应的事件发生,而在事件发生前它能让线程先休眠。
Run Loop 本质是一个处理事件源的循环。咱们对 Run Loop 的运行时具备控制权,若是当前没有时间发生,Run Loop 会让当前线程进入睡眠模式,来减轻 CPU 压力。若是有事件发生,Run Loop 就处理事件并通知相关的 Observer。具体的顺序以下:
因为与 Timer source 和 Input source 相关的 observer 通知是在事件发生前发出去的,因此这些通知和真实的事件发生时间之间是存在必定的延时的。若是你须要精确的时间控制,而这个延时对你来讲很致命的话,你可使用休眠通知和唤醒通知来校队事件实际发生时间。
因为 timer 和其余一些周期性的事件是在你运行其对应的 Run Loop 的时候被分发的,因此当绕过这个 Loop 的时候,这些事件的分发也会被干扰到。一个典型的例子就是当你实现一个鼠标事件追踪的例程时,你进入到一个循环里不断地向应用请求事件,因为你直接抓取这些事件而不是正常地由应用向你的例程分发,这时那些活动的timer也会没法触发,除非你的鼠标事件追踪例程退出并将控制器交给应用。
能够经过 Run Loop 对象来显式地唤醒 Run Loop。其余事件也能够唤醒 Run Loop,好比:添加一个其余的非基于端口的 Input source 能够唤醒 Run Loop 当即处理这个 Input source,而不是等到其余事件发生才处理。
使用 Core Foundation 中的方法一般是线程安全的,能够被任意线程调用。若是修改了 Run Loop 的配置而后须要执行某些操做,咱们最好是在 Run Loop 所在的线程中执行这些操做。
使用 Foundation 中的 NSRunLoop 类来修改本身的 Run Loop,咱们必须在 Run Loop 的所在线程中完成这些操做。在其余线程中给 Run Loop 添加事件源或者 Timer 会致使程序崩溃。
1 |
/// 用DefaultMode启动 |
能够看到,实际上 Run Loop 就是这样一个函数,其内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动中止,该函数才会返回。
从上面代码能够看到,Run Loop 的核心是基于 mach port 的,其进入休眠时调用的函数是 mach_msg()。为了解释这个逻辑,下面稍微介绍一下 OS X/iOS 的系统架构。
苹果官方将整个系统大体划分为上述 4 个层次:
• 应用层包括用户能接触到的图形应用,例如 Spotlight、Aqua、SpringBoard 等。
• 应用框架层即开发人员接触到的 Cocoa 等框架。
• 核心框架层包括各类核心框架、OpenGL 等内容。
• Darwin 即操做系统的核心,包括系统内核、驱动、Shell 等内容,这一层是开源的,其全部源码均可以在 opensource.apple.com 里找到。
咱们在深刻看一下 Darwin 这个核心的架构:
其中,在硬件层上面的三个组成部分:Mach、BSD、IOKit (还包括一些上面没标注的内容),共同组成了 XNU 内核。
XNU 内核的内环被称做 Mach,其做为一个微内核,仅提供了诸如处理器调度、IPC (进程间通讯)等很是少许的基础服务。
BSD 层能够看做围绕 Mach 层的一个外环,其提供了诸如进程管理、文件系统和网络等功能。
IOKit 层是为设备驱动提供了一个面向对象(C++)的一个框架。
Mach 自己提供的 API 很是有限,并且苹果也不鼓励使用 Mach 的 API,可是这些 API 很是基础,若是没有这些 API 的话,其余任何工做都没法实施。在 Mach 中,全部的东西都是经过本身的对象实现的,进程、线程和虚拟内存都被称为”对象”。和其余架构不一样, Mach 的对象间不能直接调用,只能经过消息传递的方式实现对象间的通讯。”消息”是 Mach 中最基础的概念,消息在两个端口 (port) 之间传递,这就是 Mach 的 IPC (进程间通讯) 的核心。
一条 Mach 消息实际上就是一个二进制数据包 (BLOB),其头部定义了当前端口 local_port 和目标端口 remote_port,发送和接受消息是经过同一个 API 进行的。
为了实现消息的发送和接收,mach_msg() 函数其实是调用了一个 Mach 陷阱 (trap),即函数mach_msg_trap(),陷阱这个概念在 Mach 中等同于系统调用。当你在用户态调用 mach_msg_trap() 时会触发陷阱机制,切换到内核态;内核态中内核实现的 mach_msg() 函数会完成实际的工做,以下图:
这些概念能够参考维基百科: System_call、Trap_(computing))。
Run Loop 的核心就是一个 mach_msg()
,Run Loop 调用这个函数去接收消息,若是没有别人发送 port 消息过来,内核会将线程置于等待状态。例如你在模拟器里跑起一个 iOS 的 App,而后在 App 静止时点击暂停,你会看到主线程调用栈是停留在 mach_msg_trap()
这个地方。
关于具体的如何利用 mach port 发送信息,能够看看 NSHipster 这一篇文章,或者这里的中文翻译 。
关于Mach的历史能够看看这篇颇有趣的文章:Mac OS X 背后的故事(三)Mach 之父 Avie Tevanian。
在主线程执行的代码,一般是写在诸如事件回调、Timer 回调内的。这些回调会被 Run Loop 建立好的 AutoreleasePool 环绕着,因此不会出现内存泄漏,开发者也没必要显示建立 Pool 了。
苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。
当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。这个过程的详细状况能够参考这里。SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给须要的 App 进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。
_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。一般事件好比 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。
当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。
苹果注册了一个 Observer 监测 BeforeWaiting (Loop 即将进入休眠) 事件,这个 Observer 的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取全部刚被标记为待处理的 GestureRecognizer,并执行 GestureRecognizer 的回调。
当有 UIGestureRecognizer 的变化(建立/销毁/状态改变)时,这个回调都会进行相应处理。
当在操做 UI 时,好比改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。
苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历全部待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。
这个函数内部的调用栈大概是这样的:
1 |
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv() |
NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 Run Loop 后,Run Loop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。Run Loop 为了节省资源,并不会在很是准确的时间点回调这个 Timer。Timer 有个属性叫作 Tolerance (宽容度),标示了当时间点到后,允许有多少最大偏差。
当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会建立一个 Timer 并添加到当前线程的 Run Loop 中。因此若是当前线程没有 Run Loop,则这个方法会失效。
当调用 performSelector:onThread: 时,实际上其会建立一个 Timer 加到对应的线程去,一样的,若是对应线程没有 Run Loop 该方法也会失效。
GCD 提供的某些接口也用到了 Run Loop, 例如 dispatch_async()。
当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其余线程仍然是由 libDispatch 处理的。
一般使用 NSURLConnection 时,你会传入一个 Delegate,当调用了 [connection start] 后,这个 Delegate 就会不停收到事件回调。实际上,start 这个函数的内部会会获取 CurrentRunLoop,而后在其中的 DefaultMode 添加了4个 Source0 (即须要手动触发的Source)。CFMultiplexerSource 是负责各类 Delegate 回调的,CFHTTPCookieStorage 是处理各类 Cookie 的。
NSURLConnectionLoader 中的 Run Loop 经过一些基于 mach port 的 Source 接收来自底层 CFSocket 的通知。当收到通知后,其会在合适的时机向 CFMultiplexerSource 等 Source0 发送通知,同时唤醒 Delegate 线程的 Run Loop 来让其处理这些通知。CFMultiplexerSource 会在 Delegate 线程的 Run Loop 对 Delegate 执行实际的回调。