Y神写的是真的好。这篇文章的大部份内容来自 Y神的深刻理解 RunLoop,再结合官方文档 和其余一些网上的资料再加上本身的一些理解作了一些补充和概括,官方文档也很是值得一看。
php
RunLoop 直接翻译过来就是 运行循环。运行是什么?运行指你的程序运行,循环?额,就是循环。因此运行循环就是指能让你的程序循环不断的运行的一个东西。html
RunLoop 是一个让线程能随时处理事件但并不退出的机制。这种模型一般被称为 Event Loop,实现这种模型的关键点在于:如何管理事件/消息,如何让线程在没有处理消息时休眠以免资源占用、在有消息到来时马上被唤醒。git
因此,RunLoop 实际上就是一个对象,这个对象管理了其须要处理的事件和消息,并提供了一个入口函数来执行上面 Event Loop 的逻辑。线程执行了这个函数后,就会一直处于函数内部 “接受消息->等待->处理” 的循环中,直到这个循环结束(好比传入 quit
的消息),函数返回。github
至于为何要有这个 RunLoop,说一个最直接的,iOS 程序启动后运行在主线程上,而线程通常执行完一段任务就会结束,被 CPU 挂起,若是咱们没有保住主线程的命,那么咱们的 App 一打开就会关闭。因此苹果帮咱们在主线程中默认开启 RunLoop,让 App 可以持续运行。swift
直接列一下二者的关系:网络
RunLoop 与线程是一一对应的。架构
RunLoop 不容许手动建立,只能经过方法去获取,CFRunLoopGetMain()
和 CFRunLoopGetCurrent()
。app
RunLoop 是懒加载的,若是你不去使用它,那么它就不会被建立,主线程中的 RunLoop 是苹果帮咱们默认开启的。框架
RunLoop 的销毁发生在线程结束的时候。iphone
RunLoop 与线程的对应关系保存在一个全局的 Dictionary 中
在 Core Foundation 里面关于 RunLoop 有 5 个类:
每一个 RunLoop 中包含若干个 Mode,每一个 Mode 中包含若干个 Source/Observer/Timer。
每次调用 RunLoop 的主函数,都只能指定一种 Mode,指定的 Mode 称为 CurrentMode
,若是须要切换 Mode,就只能退出当前 Loop,而后从新更新指定一种 Mode 进入。这样是为了分割不一样组的 Source/Observer/Timer,使其互不影响。
CFRunLoopSourceRef 是事件产生的地方。Source 有两个版本:Source0 和 Source1。
CFRunLoopSourceSignal(source)
,将这个 Source 标记为待处理,而后手动调用 CFRunLoopWakeUp(runloop)
来唤醒 RunLoop,让其处理事件。CFRunLoopTimeRef 是基于时间的触发器,它和 NSTimer 和 toll-free bridged 的,能够混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop 会注册对应的时间点,当时间点到时,RunLoop 会被唤醒以执行那个回调。
CFRunLoopObserverRef 是观察者,每一个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者都能经过回调接受到这个变化。能够观测的时间点有如下几个:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出Loop
};
复制代码
上面的 Source/Timer/Observer 被统称为 mode item,一个 item 能够被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。若是一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。
关于具体的结构,你们能够查看 官方文档,里面有一个关于 RunLoop Modes 的列表。
RunLoop 的大体结构:
struct __CFRunLoop {
CFMutableSetRef _commonModes; // Set
CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
CFRunLoopModeRef _currentMode; // Current Runloop Mode
CFMutableSetRef _modes; // Set
...
};
复制代码
RunLoop Mode 的大体结构:
struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
...
};
复制代码
你们能够下载 Core Foundation 的源码来查看详细结构。
系统默认注册了 5 个 Mode:
kCFRunLoopDefaultMode
: App 的默认 Mode,一般主线程是在这个 Mode 下面运行的UITrackingRunLoopMode
: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动UIInitializationRunLoopMode
: 在刚启动 App 时进入的第一个 Mode,启动完后就再也不使用GSEventReceiveRunLoopMode
: 接受系统事件的内部 Mode,一般用不到kCFRunLoopCommonModes
: 这是一个占位的 Mode,没有实际做用能够点击这里查看更多的苹果内部的 Mode,但那些 Mode 在开发中基本不会遇到。
不一样 Mode 之间互不干扰。
咱们经常使用的有两种,kCFRunLoopDefaultMode
和 UITrackingRunLoopMode
,还有一个 kCFRunLoopCommonModes
,不过 kCFRunLoopCommonModes
只是一种伪模式。
关于 Common modes:一个 Mode 能够将本身标记为 “Common” 属性(经过将其 ModeName 添加到 RunLoop 的 “commonModes” 中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems
里的 Source/Observer/Timer 同步到具备 “Common” 标记的全部 Mode 里。
视图滑动时定时器失效的解决方法:
主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultMode
和UITrackingRunLoopMode
。这两个 Mode 都已经被标记为 "Commoc" 属性。Default Mode
是 App 平时所处的状态,UITrackingRunLoopMode
是追踪ScrollView
滑动时的状态(UITextView
的滑动也算)。
当你建立一个 Timer 并加到DefaultMode
时,Timer 会获得重复回调,但此时滑动一个ScrollView
时,RunLoop 会将 mode 切换为UITrackingRunLoopMode
,这时 Timer 就不会被回调,而且也不会影响的到滑动操做,由于不一样 Mode 之间是互不干扰的。
有时你须要一个 Timer,在两个 Mode 中都能获得回调,一种方法就是将这个 Timer 分别加入这两个 Mode。还有一种方式,就是将 Timer 加入到顶层的 RunLoop 的commonModeItems
中,commonModeItems
被 RunLoop 自动更新到全部具备 "Common" 属性的 Mode 里去。
CFRunLoop 对外暴露的管理 Mode 的接口只有下面 2 个:
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);
复制代码
Mode 暴露的管理 mode item 的接口有下面几个:
CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer,CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
复制代码
你只能经过 mode name 来操做内部的 mode,当你传入一个新的 mode name 但 RunLoop 内部没有对应 mode 时,RunLoop 会自动帮你建立对应的 CFRunLoopModeRef
。对于一个 RunLoop 来讲,其内部的 mode 只能增长不能删除。
苹果公开提供的 Mode 有两个:kCFRunLoopDefaultMode(NSDefalutRunLoopMode)
和 UITrackingRunLoopMode
,你能够用这两个 Mode Name 来操做其对应的 Mode。
同时苹果还提供了一个操做 Common 标记的字符串:kCFRunLoopCommonModes(NSRunLoopCommonModes)
,你能够用这个字符串来操做 Common Items,或标记一个 Mode 为 “Common”。使用时注意区分这个字符串和其余 mode name。
因此 RunLoop Mode 是能够自定义建立的。
输入源以异步的方式向线程传递事件。事件的来源取决于输入源的类型,一般是两个类别之一:
基于端口的输入源监听应用程序的 Mach 端口,自定义输入源监听自定义事件源。就 RunLoop 而言,输入源是基于端口的仍是自定义的是可有可无的,两个来源之间的惟一区别就是它们如何发出信号。基于端口的源由内核自动发出信号,可是自定义的源必须从另外一个线程手动发送信号。
当建立输入源(input sources)时,会将其分配给 RunLoop 的一个或多个 Mode。不一样的 Mode 会影响对这些输入源的监听。大部分状况下你在默认模式(kCFRunLoopDefaultMode
)下运行 RunLoop,但你也能够指定自定义的 Mode。若是输入源未处于当前监听的 Mode,则在它生成的任何事件,都将被保留,直到 RunLoop 被指定到正确的 Mode 运行。
Cocoa 和 Core Foundation 提供内置支持,使用与端口相关的对象和函数建立基于端口的输入源。例如,在 Cocoa 中,你根本没必要直接建立输入源。你只须要建立一个端口对象并使用 NSPort
提供的方法将该端口添加到 RunLoop 中,port 对象会自动为你建立和配置所需的输入源。
在 Core Foundation 中,你必须手动建立端口和输入源。也就是使用 CFMachPortRef
、CFMessagePortRef
、CFSocketRef
去建立合适的对象。
自定义输入源的建立和使用的例子你们能够去查一下官方文档。
除了基于端口的源以外,Cocoa 还定义了一个自定义 input source,容许你在任何线程上执行选择器。与基于端口的 source 相似,perform selector
请求在目标线程上被序列化,从而减小在一个线程上运行多个方法时可能发生的许多同步问题。与基于端口的 source 不一样,perform selector
源在执行其 selector
后将其自身从 RunLoop 中移除。
在另外一个线程执行选择器时,目标线程必须开启了 RunLoop。对于你本身建立的线程,意味着要显式启动 RunLoop。因为主线程中默认启动了 RunLoop,因此只要应用程序调用 applicationDidFinishLaunching:
,就能够开始在主线程上发出调用。RunLoop 每次经过循环处理全部排队的 perform selector
调用,而不是在每次循环迭代期间处理一个。
关于在其余线程上执行选择器的方法,能够查看官方文档的表3.2。
其实就是 NSTimer,计时器,一个 NSTimer 注册到 RunLoop 以后,RunLoop 会为其重复的时间点注册好事件。不过 RunLoop 为了节省资源,并不会在很是准确的时间点回调这个 Timer,由于 RunLoop 内部是有一个处理逻辑的,这个咱们放到下面再讲。
定时器是线程通知本身作事情的一种方式。例如,有一个搜索的功能,咱们可使用计时器,设置一个时间,让用户在开始输入以后通过这个时间后开始搜索,这样咱们就可使用户在开始搜索以前输入尽量的搜索字符串。
虽然咱们设置的时间到了计时器就会发出通知让 RunLoop 去作事情,但它并不会真正的实时去处理。与输入源相似,计时器与 RunLoop 的 Mode 相关联。好比你添加定时器到 kCFRunLoopDefaultMode
中,若是此时你的 Mode 是 UITrackingRunLoopMode
,那么这个计时器是不会被触发的。除非你切换 Mode 为 kCFRunLoopDefaultMode
。若是 Timer 设置的时间到了,该执行 Timer 对应的事件了,可是此时 RunLoop 还在忙着处理其余的事情,那么 Timer 会等到 RunLoop 执行完其余事情再执行。若是线程中的 RunLoop 根本没有被启动,那么 Timer 永远不会被触发。
你能够设置计时器只触发一次或者重复触发,当你设置为重复触发的时候,计时器会按照你原来设置的间隔去不断的触发事件,而不是按照实际触发事件的间隔。好比说你在 11:00
的时候,设置了每隔 10 分钟,触发一次计时器事件,也就是 11:10
、11:20
、11:30
...若是因为某些缘由,原本应该在 11:10
分触发的事件,被推迟到了 11:15
才触发,这时虽然你设置的时间间隔是 10 分钟,好像是应该 11:25
才触发下一次事件,可是其实不是的,仍是会在 11:20
分触发下一次时间,而后在 11:30
分触发下下次事件。因此计时器是按照你最开始计划的时间来发出通知的。
RunLoop 在内部会处理 Source 事件、Timer 触发的事件、会休眠、会退出等,在这些特定的时期,系统会经过 Observer 来通知开发者,Observer 关联了 RunLoop 的下列时刻:
与 Timer 相似,Observer 能够一次或重复使用。一次性 Observer 在触发后将其自身从 RunLoop 中移除,你能够在建立 Observer 时指定是运行一次仍是重复运行。
下面是官方文档提到的内部逻辑:
由于 Timer 和 Source 的 Observer 通知是在这些事件实际发生以前传递的,所以通知事件与实际事件的时间可能存在差距。若是这些事件之间的时间关系很重要,你可使用休眠和唤醒休眠通知来帮助你关联实际事件之间的时间。
实际上 RunLoop 其内部就是一个 do-while 循环。当你调用 CFRunLoopRun()
时,线程就会一直停留在这个循环里,直到超时或被手动中止,该函数才会返回。
RunLoop 的核心是基于 mach port 的,其进入休眠时调用的函数是 mach_msg()
。
Mach 自己提供的 API 很是有限,并且苹果也不鼓励使用 Mach 的 API,可是这些 API 很是基础,若是没有这些 API 的话,其余任何工做都没法实施。在 Mach 中,全部的东西都是经过本身的对象实现的,进程、线程和虚拟内存都被称为 “对象”。和其余架构不一样,Mach 的对象间不能直接调用,只能经过消息传递的方式实现对象间的通讯。“消息” 是 Mach 中最基础的概念,消息在两个端口(port)之间传递,这就是 Mach 的 IPC(进程间通讯)的核心。
为了实现消息的发送和接受,mach_msg()
函数其实是调用了一个 Mach 陷阱(trap),即函数 mach_msg_trap()
,陷阱这个概念在 Mach 中等同于系统调用。当你在用户态调用 mach_msg_trap()
时会触发陷阱机制,切换到内核态。内核态中内核实现的 mach_msg()
函数会完成实际的工做。
也就是你在用户态调用了 mach_msg()
函数,会触发 mach trap,进入由系统调用的 mach_msg()
函数中去执行实际的内容。关于用户态和内核态的概念,不知道的朋友能够百度一下。
RunLoop 的核心就是一个 mach_msg()
,RunLoop 调用这个函数去接收消息,若是没有别人发送 port 消息过来,内核会将线程置于等待状态。例如你在模拟器跑起一个 iOS 的 App,而后在 App 静止时点击暂停,你会看到主线程调用栈停留在 mach_msg_trap()
这个地方。
按照官方文档的说法,你惟一须要使用到 RunLoop 的时候是为你的应用程序建立辅助线程(create secondary threads)。
App 的主线程的 RunLoop 是一个相当重要的基础架构,所以,App 框架默认在运行时启动主线程并开启主线程中的 RunLoop。若是是用 Xcode 的模板项目来建立应用程序,那么这些系统都已经帮你作好了,不须要显式调用。
对于辅助线程(secondary threads),要肯定实际状况看是否须要开启 RunLoop,若是须要的话,就自行配置并启动它。在任何状况下,都不该该为一个线程开启 RunLoop。例如,若是使用线程执行某些长时间运行且自定义的任务,则能够避免启动 RunLoop。(我以为这个说的我有点云里雾里,我贴一下原文)
You do not need to start a thread’s run loop in all cases. For example, if you use a thread to perform some long-running and predetermined task, you can probably avoid starting the run loop. Run loops are intended for situations where you want more interactivity with the thread.
在如下的几种状况,须要启动 RunLoop:
performSelector
调用其余线程方法的时候在开发中我还没直接用到 RunLoop 去作过什么东西,能够作一个常驻线程,可是常驻线程这种东西是有问题的,虽然 AFN2.0 曾经用过,可是那是由于当时苹果的网路请求框架有缺陷。另一个用到 runloop 的地方可能就是作自动轮播那里,用到了 common mode,其余的方面就不多使用了。因此仍是具体来看下苹果对 RunLoop 的应用。
App 启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()
。
第一个 Observer 监视的事件是 Entry(即将进入 Loop),其回调内会调用 _objc_autoreleasePoolPush()
建立自动释放池。其 order 是 -2147483647
,优先级最高,保证建立释放池发生在其余全部回调以前。
第二个 Observer 监视了两个事件:BeforeWaiting(准备进入休眠)
时调用 _objc_autoreleasePoolPop()
和 _objc_autoreleasePoolPush()
释放旧的池并建立新池。Exit(即将退出 Loop)
时调用 _objc_autoreleasePoolPop()
来释放自动释放池。这个 Observer 的 order 是 2147483647
,优先级最低,保证其释放池释放发生在其余全部回调以后。
在主线程执行的代码,一般是写在诸如事件回调、Timer 回调内的。这些回调会被 RunLoop 建立好的 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
的变化(建立/销毁/状态改变)时,这个回调都会进行相应处理。
步骤:
UIGestureRecognizer
标记为待处理BeforeWaiting
时,在其函数回调内部会获取全部刚被标记为待处理的 GestureRecognizer
,并执行 GestureRecognizer
的回调当在操做 UI 时,好比改变了 frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay
方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。
苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠)
和 Exit(即将退出Loop)
事件,回调去执行一个很长的函数:_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
。这个函数里会遍历全部待处理的 UIView/CALayer
以执行实际的绘制和调整,并更新 UI 界面。
步骤:
BeforeWaiting
和 Exit
时遍历全部待处理的 UI 以执行实际的绘制和调整,并更新 UI 界面NSTimer
其实就是 CFRunLoopTimeRef
,他们之间是 toll-free bridged
的。一个 NSTimer
注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00,10:10,10:20 这几个时间点。RunLoop 为了节省资源,并不会很是准确的时间点回调这个 Timer。Timer 有个属性叫作 Tolerance(宽容度),标示了当时间点到后,允许有多少最大偏差。
若是某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就好比等公交,若是 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。
CADisplayLink
是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer
并不同,其内部实际是操做了一个 Source)。若是在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer
相似),形成界面卡顿的感受。在快速滑动 TableView
时,即便一帧的卡顿也会让用户有所察觉。Facebook 开源的 AsyncDisplayKit 就是为了解决界面卡顿的问题,其内部也用到了 RunLoop(模仿了 iOS 界面更新的过程)。
当调用 NSObject 的 performSelector:afterDelay:
后,实际上其内部会建立一个 Timer 并添加到当前线程的 RunLoop 中。因此若是当前线程没有 RunLoop,则这个方法会失效。
当调用 dispatch_async(dispatch_get_main_queue(), block)
时,libDispatch
会向主线程的 RunLoop 发送消息,RunLoop 会被唤醒,并从消息中取得这个 block,并在回调 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其余线程仍然是由 libDispatch
处理的。
iOS 关于网络请求的接口自下至上的层次:
CFSocket
CFNetwork ->ASIHttpRequest
NSURLConnection ->AFNetworking
NSURLSession ->AFNetworking2, Alamofire
复制代码
CFSocket
是最底层的接口,只负责 socket 通讯CFNetwork
是基于 CFSocket
等接口的上层封装NSURLConnection
是基于 CFNetwork
的更高层的封装,提供面向对象的接口NSURLSession
是 iOS7 中新增的接口,表面上和 NSURLConnection
并列的,但底层仍然用到了 NSURLConnection
的部分功能(好比 com.apple.NSURLConnectionLoader
线程)下面主要介绍下 NSURLConnection
的工做过程。
一般使用 NSURLConnection
时,你会传入一个 Delegate
,当调用了 [connection start]
后,这个 Delegate
就会不停收到事件回调。实际上,strat
这个函数的内部会获取 CurrentRunLoop,而后在其中的 DefaultMode
添加 4 个 Source0
(即须要手动触发的 Source)。CFMultiplexerSource
是负责各类 Delegate
回调的,CFHTTPCookieStorage
是处理各类 Cookie 的。
当开始网络传输时,咱们能够看到 NSURLConnection
建立了两个新线程:com.apple.NSURLConnectionLoader
和 com.apple.CFSocket.private
。其中 CFSocket
线程是处理底层 socket 链接的。NSURLConnectionLoader
这个线程内部会使用 RunLoop 来接受底层 socket 事件,并经过以前添加的 Source0 通知到上层的 delegate。
NSURLConnectionLoader
中的 RunLoop 经过一些基于 mach port 的 Source 接收来自底层 CFSocket
的通知。当收到通知后,其会在合适的时机向 CFMultiplexerSource
等 Source0 发送通知,同时唤醒 Delegate
线程的 RunLoop 对 Delegate
执行实际的回调。
步骤:
NSURLConnection
建立两个新线程,com.apple.CFSocket.private
处理 socket 链接,com.aoole.NSURLConnectionLoader
内部使用 RunLoop 来接受底层 socket 事件NSURLConnectionLoader
经过 Source1 接收到这个通知NSURLConnectionLoader
在合适的时机向 CFMultiplexerSource
、CFHTTPCookieStorage
等 Source0 发送通知,同时唤醒 Delegate
线程的 RunLoop 让其来处理这些通知CFMultiplexerSource
会在 Delegate
线程的 RunLoop 对 Delegate
执行实际的回调RunLoop 是一个让线程能随时处理事件但并不退出的机制。这种模型一般被称为 Event Loop,实现这种模型的关键点在于:如何管理事件/消息,如何让线程在没有处理消息时休眠以免资源占用、在有消息到来时马上被唤醒。
因此,RunLoop 实际上就是一个对象,这个对象管理了其须要处理的事件和消息,并提供了一个入口函数来执行上面 Event Loop 的逻辑。线程执行了这个函数后,就会一直处于函数内部 “接受消息->等待->处理” 的循环中,直到这个循环结束(好比传入 quit
的消息),函数返回。
说 RunLoop 就绕不开线程,RunLoop 与线程的关系:
Dictionary
中一个 RunLoop 包含若干个 Mode,每一个 Mode 包含若干个 Source/Timer/Observer,Source/Timer/Observer 被统称为 Mode Item。不一样 Mode 之间互不干扰。
在 Core Foundation 里面关于 RunLoop 的 5 个类:
Mode
系统默认注册了 5 个 Mode:
kCFRunLoopDefaultMode
:App 的默认 ModeUITrackingRunLoopMode
:界面跟踪 Mode,用于 ScrollView
追踪触摸滑动UIInitializationRunLoopMode
:在刚启动 App 时进入的第一个 ModeGSEventReceiveRunLoopMode
:接受系统事件的内部 Mode,一般用不到kCFRunLoopCommonModes
:这是一个占位的 Mode,没有实际做用Source
主要分两种类型:
具体的类型:
Timer
基于时间的触发器,提早在 RunLoop 中注册好事件,时间点到达时,RunLoop 将被唤醒以执行事件。受限于 RunLoop 的内部逻辑,计时器并不十分准确。
Observer
观察者,每一个 Observer 都包含了一个回调(函数指针)。
可观测的时间点: