RunLoop
实际上是一个对象,这个对象管理了其需要处理的事件和消息.
macOS/iOS系统中,提供了两个这样的对象:NSRunLoop 和 CFRunLoopRef。
CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的(CFSpinlock/CFSpinUnlock)。
1 | struct __CFRunLoop { |
NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。
CFRunLoopRef 的代码是开源的,你可以在这里 下载到整个 CoreFoundation 的源码来查看。
RunLoop与线程的关系
CFRunLoop 是基于 pthread 来管理的,苹果不允许直接创建RunLoop,可以通过CFRunLoopGetMain()和CFRunLoopGetCurrent()获取。
1 | CFRunLoopRef CFRunLoopGetMain(void) { |
你可以通过pthread_main_thread_np()或[NSThread mainThread]来获取主线程;也可以通过 pthread_self()或[NSThread currentThread]来获取当前线程。
线程和RunLoop之间是一一对应的,其关系是保存在一个全局的Dictionary(pthread_t : CFRunLoopRef)里的同时,也在线程里保存了一份
_pthread_setspecific_direct(CF_TSD_KEY, arg) 可通过本线各的
_CFGetTSD(CFTSDKeyRunLoop) // CFTSDKeyRunLoop 10
获取已保存的RunLoop。
线程刚创建时并没有RunLoop,如果你不主动获取,那它一直都不会有。RunLoop的创建是发生在第一次获取时,RunLoop的销毁是发生在线程结束时。你只能在一个线程的内部获取其RunLoop(主线程除外)。
1 | typedef struct __CFRunLoopSource * CFRunLoopSourceRef; |
CFRunLoopSourceRef
1 | struct __CFRunLoopSource { |
Source有两个版本:Source0 和 Source1。
- Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
- Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。
CFRunLoopObserverRef
1 | struct __CFRunLoopObserver { |
包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。
Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。
CFRunLoopTimerRef
1 | struct __CFRunLoopTimer { |
包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。
1 | truct __CFRunLoopMode { |
苹果提供的 Mode 有五个,其中两个是公开的:kCFRunLoopDefaultMode (NSDefaultRunLoopMode) 和 kCFRunLoopCommonModes (NSRunLoopCommonModes)
RunLoop 的内部逻辑
代码实现
1 | void CFRunLoopRun(void) { /* DOES CALLOUT */ |
OSX和iOS架构
RunLoop的Source0负责处理App内部事件,而Souces1则接收系统发来的Mach Message再转发给Souces0处理。
系统注册了一个基于port 的source ,回调函数为__IOHIDEventSystemClientQueueCallback。通过测试,无论你点击屏幕,甚至是你晃动手机,都是触发这个回调。
经查资料知道这首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收,随后用 mach port 转发给需要的App进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。
_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。
以下是点击Button后的线程堆栈
App 启动后 RunLoop 的状态
1 | <CFRunLoop 0x6000001f9300 [0x10306ec80]>{wakeup port = 0x2203, stopped = false, ignoreWakeUps = false, |
App启动后系统添加了6个Observer,其中2个的回调都是_wrapRunLoopWithAutoreleasePoolHandler。
第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。
回调是_UIGestureRecognizerUpdateObserver的 Observer 在 BeforeWaiting(即将进入休眠) 时执行GestureRecognizer的回调
回调是_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv的 Observer 在BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 时会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面
关于网络
Runloop & CFSocket
1 | static void __CFSocketSendNameRegistryRequest(CFSocketSignature *signature, CFDictionaryRef requestDictionary, __CFSocketNameRegistryResponse *response, CFTimeInterval timeout) { |
CFSocket 是最底层的接口,只负责 socket 通信。CFNetwork 是基于 CFSocket 等接口的上层封装。
RunLoop 的实例
AFNetworking
1 | + (void)networkRequestThreadEntryPoint:(id)__unused object { |