前言
这一篇文章主要在于 Run Loop 源码的阅读,内容有点长,需要一些基础。
Event Loop
Run Loop 是一个 iOS 开发里的基础概念,它并非独有的机制,很多系统和框架都有类似的实现,Run Loop 是 Event Loop (事件循环)机制的在 iOS 平台的一种实现。
查阅 wikipedia 有关 Event Loop 的描述:
在计算机科学里, Event Loop / Run Loop 是一个用于等待和发送消息/事件的程序结构,在程序中等待和派发一系列事件或者消息。它通过向“事件提供者”发出请求来工作,通常会阻塞请求,直到事件到达,然后调用相应的事件处理程序。
Event Driven
说到 Event Loop ,其实还应该了解到 Event-Driven (事件驱动)。
Event-Driven 的出现,在于解决图形界面和用户交互的问题:
通常 GUI 程序的交互事件执行是由用户来控制的,无法预测它发生的节点,对应这样的情况,需要采用 Event-Driven 的编程方法。
Event-Driven 的实现原理,基本就是使用 Event Loop 完成。Event-Driven 程序的执行,可以概括成:
启动 ——> 事件循环(即等待事件发生并处理之)。
在 GUI 的设计场景下,一般写代码会是下面的思维:
用户输入 -> 事件响应 -> 代码运行 -> 刷新页面状态
Event
我们一直在说 Event Loop 和 Event-Driven 。那什么是 Event (事件) 呢?
在 Event-Driven 中,可以把一切行为都抽象为 Event 。例如: IO 操作完成,用户点击按钮,一个图片加载完成,文本框的文字改变等等情况,都可以看作是一个 Event 。
Event Handler
当 Event 被放到 Event Loop 里进行处理的时候,会调用预先注册过的代码对 Event 做处理。这就叫 Event Handler 。
Event Handler 其实就是对 Event 的响应,可以叫做事件回调,事件处理,事件响应,都是一样的概念。
这里需要注意的是,一个 Event 并不一定有 Event Handler .
Event Loop 解决了什么问题
一般来说,操作分为同步和异步。
同步操作,是一个接一个的处理。等前一个处理完,再执行下一个。那么在一些耗时任务上,比如有很多 I/O 操作 或者 网络请求 的任务,线程就会有长时间在等待任务结果。
异步操作,是不用等待执行结果的,可以直接在这期间执行另外的任务。等到任务结果出来之后,再进行处理。
实际上 Event Loop 就是实现异步的一种方法。
对于有回调的 Event,线程不用一直等待任务的结果出来再去执行下一个。而是等 Event 被加入到 Event Loop 时,再去执行。如果一个 Event 也没有,那线程就会休眠,避免浪费资源。
如果没有 Event Loop 来实现异步操作,那我们的程序会很容易出现卡顿。
扩展 :
JavaScript 在单线程条件下运行,可以完成异步操作,也是基于 Event Loop 机制。
建议可以参考 JavaScript异步编程 的内容来理解,更以帮助我们触类旁通,学习到通用的知识。
Run Loop 实现
网上目前有关 Run Loop 的文章, 10 篇里面可能有 8 篇都是重复了 深入理解RunLoop 中的代码。
然而这都是经过作者大量简化过的版本,隐藏了大量的细节。
其实从细节里面,我们一样可以学习到很多东西,不妨尝试去阅读一下。
我们知道 CFRunLoopRef 的代码是开源的,可以查看源代码来看它的实现,我选择的版本是 CF-1153.18 中的 CFRunLoop.c 。
获取 Run Loop
由于苹果不允许我们直接创建 RunLoop,只提供 2 个获取操作的函数:
- CFRunLoopGetMain :
1 | CFRunLoopRef CFRunLoopGetMain(void) { |
- CFRunLoopGetCurrent :
1 | CFRunLoopRef CFRunLoopGetCurrent(void) { |
CHECK_FOR_FORK()
在两个函数里,都有使用了 CHECK_FOR_FORK() 。
它应该是属于多进程情况下的一个断言。
Threading Programming Guide 中,有这么一段话:
Warning: When launching separate processes using the fork function, you must always follow a call to fork with a call to exec or a similar function. Applications that depend on the Core Foundation, Cocoa, or Core Data frameworks (either explicitly or implicitly) must make a subsequent call to an exec function or those frameworks may behave improperly.
也就是说,当通过 fork 启动一个新进程的时候,你必须要接着调用一个 exec 或类似的函数。而依赖于 Core Founadtion / Cocoa / Core Data 框架的应用,必须调用 exec 函数,否则这些框架也许不能正确的工作。
所以为了保证安全,使用 CHECK_FOR_FORK 进行检查。
FORK
这里简单提一下 fork 。
在 UNIX 中,用 fork 来创建子进程,调用 fork( ) 的进程被称为父进程,新进程是子进程,并且几乎是父进程的完全复制(变量、文件句柄、共享内存消息等相同,但 process id 不同)。
因为子进程和父进程基本是一样的,要想让子进程去执行其他不同的程序,子进程就需要调用 exec ,把自身替换为新的进程,其中process id不变,但原来进程的代码段、堆栈段、数据段被新的内容取代,来执行新的程序。
这样 fork 和 exec 就成为一种组合。
而在 iOS 这样的类 UNIX 系统里,基本上也都要通过 fork 的形式来创建新的进程。
假如没有执行完 exec ,那么执行的代码段等内容,还是父进程里的,出现问题可以说百分之百。这就是 CHECK_FOR_FORK 检查的目的。
Thread-specific data
为了帮助理解,还需要先说 Thread-specific data (TSD),它可以叫作 线程私有数据 , 这个概念来自于 unix 之中。
它是存储和查询与某个线程相关数据的一种机制:
进程内的所有线程,共享进程的数据空间,因此全局变量为所有线程所共有。
而有时线程也需要保存自己的私有数据,这时可以创建线程私有数据(Thread-specific Data)TSD来解决。
在线程内部,私有数据可以被各个函数访问,但对其他线程是屏蔽的。例如我们常见的变量errno,它返回标准的出错信息。它显然不能是一个局部变量,几乎每个函数都应该可以调用它;但它又不能是一个全局变量。
在 Pthreads 里,把它叫做 Thread-local storage (线程私有存储) , 有以下几个相关的操作函数:
1 | - pthread_key_create(): 分配用于标识进程中线程特定数据的pthread_key_t类型的键 |
在苹果的平台上,基本就是利用上面操作实现 TSD 。
RunLoop 实际上就属于 TSD 的里存储的一种数据。所以我们讲, RunLoop 和线程是一一对应的。而 RunLoop 会在线程销毁时,跟着一起清理,也是由于线程私有数据的机制。
TSD 对应存储的 key 有相关的析构函数,线程退出时,析构函数函数就会按照操作系统,实现定义的顺序被调用。所以在 CFSetTSD 会有一个析构函数的参数位置。
CFGetTSD / CFSetTSD
关于 CFGetTSD / CFSetTSD , 在 ForFoundationOnly.h 找到定义:
1 | // ---- Thread-specific data -------------------------------------------- |
TSD
也就是 thread specific data
的缩写了。
按照注释,说明 CFGetTSD
的作用是 – 从预先赋值的位置,得到 TSD
。
上面也说明了 CFSetTSD
的作用 – 在预先位置设置 TSD
。 这个数据不可以是随机的值,并保证你使用的位置有唯一性。如果需要释放这个数据,就传入析构函数;如果不需要释放,则传入NULL。和 pthread TSD 不同的是,这一个析构函数是每一个线程都有的。
在上面的CFRunLoopGetCurrent
里,是这么使用 _CFGetTSD 的:1
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
这里的 slot 值,一般对应的,都应该是类似 __CFTSDKeyRunLoop 的枚举类型的关键字。
CFTSDTable
在 CFPlatform.c 找到 CFGetTSD / CFSetTSD 具体的实现,发现两者其实都依靠了 CFTSDTable 类型的一个 table 实现。
CFTSDTable 是一个保存 TSD 数据的结构体:
1 | // Data structure to hold TSD data, cleanup functions for each |
它拥有两个数组: data 存储私有数据, destructors 存储释放函数 . 还有一个 destructorCount ,它顾名思义就是 destructors 数组的数量。
CFGetTSD 主要是取了 table ,获取 table 的 data 数组,按 slot 索引取值。
CFSetTSD 的作用,就是根据 CFTSDTable 的结构,分别是往 table 里设置 data 数组 slot 位置的值,以及 destructors 数组 slot 位置的值:
1 | void *oldVal = (void *)table->data[slot]; |
CFRunLoopGet0
无论是 CFRunLoopGetMain 还是 CFRunLoopGetCurrent ,两者调用了 CFRunLoopGet0 :
1 | static CFMutableDictionaryRef __CFRunLoops = NULL; |
这里对主要的流程做一下解释:
- 第一次进入,无论 t 为主线程或者子线程。因为 __CFRunLoops 为 null ,所以会创建一个 mainLoop.
- 根据传递进来的 t ,创建对应的 loop 。t 作为 key,loop 作为 value ,存储到 __CFRunLoops 里。如果已经有了对应 loop 存在,则不创建。
- 判断 t 是否为当前线程。如果是当前线程,就会利用 CFSetTSD 在 CFTSDKeyRunLoop/CFTSDKeyRunLoopCntr 的位置做设置。
注意:
在了解完 CFSetTSD 的作用, CFTSDKeyRunLoop 设置的意思就很清楚: 在 CFTSDKeyRunLoop 位置,存储 loop , 但不对 loop 设置析构函数。
直接对于 loop 的设置,其实这里已经完成了。
网络文章大部分,直接就说在 CFTSDKeyRunLoopCntr 设置了清理 loop 的回调。
对于为什么可以释放 loop ,却避而不谈。
大家想过没有 :
在 CFTSDKeyRunLoopCntr 位置,给出的参数是
PTHREAD_DESTRUCTOR_ITERATIONS - 1
PTHREAD_DESTRUCTOR_ITERATIONS 表示的,是线程退出时,操作系统实现试图销毁线程私有数据的最大次数。试图销毁次数,和 CFFinalizeRunLoop 这个析构函数,是怎么关联起来的?又是怎么被调用的?
在前面,说过了 CFTSDTable ,实际上在 CFTSDGetTable() 里面,就做了相关 TSD 的设置:
1 | // Get or initialize a thread local storage. It is created on demand. |
通过 CF_TSD_KEY ,指定了对应的析构函数 CFTSDFinalize 。
而 CFTSDFinalize 的代码如下:
1 | static void __CFTSDFinalize(void *arg) { |
我们可以看到,table 会循环遍历 data 和 destructors 的数据,并且把 old 变量作为 destructors 里函数的参数。
这就是线程退出时,会调用到 Run Loop 销毁函数的原因。
同时也由于 table 是从 0 开始遍历,所以会根据枚举值的大小,来决定销毁调用的顺序的。
我们可以在 CFInternal.h 中找到相关枚举定义:
1 | // Foundation uses 20-40 |
注释里有一句 autorelease pool stuff must be higher than run loop constants
的说明,这一点就其实关系到 Run Loop 和 autorelease pool 释放的顺序了。
OSAtomicCompareAndSwapPtrBarrier
1 | /*! @abstract Compare and swap for <code>int</code> values. |
这一个函数,它首先对 oldValue , theValue 进行比较.
如果两个值相等,就执行 theValue = newValue,并返回 YES.
如果两个值不等,返回 NO .
值得注意的是,这个函数引入了 barrier,它的操作是原子的。
这是一个典型的 CAS 操作,无独有偶,在 RAC 中的一个特点也是使用 Atomic Operations ,完成线程同步。
它在CFRunLoopGet0
的作用是 : 比较 CFRunLoops 是否为 null 。 如果为 null (第一次创建)了,就把 dict 赋值给 CFRunLoops 。如果不为 null,就释放掉 dict 。
memory barrier
这里再稍微提一下 barrier , 上面说它保证了原子操作。
memory barrier 在维基的定义是:
内存屏障(英语:Memory barrier),也称内存栅栏,内存栅障,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作。
大多数现代计算机为了提高性能而采取乱序执行,这使得内存屏障成为必须。
而对于 Objective C 的实现来说,几乎所有的加锁操作最后都会设置 memory barrier ,官方文档的解释:
Note: Most types of locks also incorporate a memory barrier to ensure that any preceding load and store instructions are completed before entering the critical section.
为了防止编译器对我们的代码做优化,改变我们代码的指令顺序,可以采用 barrier 设置对我们的代码顺序做保证。
CFRunLoopCreate
讲完 Run Loop 怎么获取,再看 Run Loop 怎么创建。
对于 CFRunLoopCreate :
1 | static CFRunLoopRef __CFRunLoopCreate(pthread_t t) { |
大体说一下创建的流程:
- 通过 CFRuntimeCreateInstance ,创建一个 CFRunLoopRef 实例。
- 对 loop 做初始化设置,比如唤醒端口,commonModes 等的设置。
注意:
CFRunLoopPushPerRunData 会在创建时做一些初始化设置,
__CFPortAllocate() 会设置唤醒的端口,
CFRunLoopSetIgnoreWakeUps 调用的原因时,目前处于唤醒状态,对它的消息做忽略。
HALT 命令可以停止系统运行,假如 wakeUpPort 为 CFPORT_NULL
CFRuntimeCreateInstance
真正创建得到 CFRunLoopRef 类型的 loop ,调用的是 CFRuntimeCreateInstance
来创建的。
它是一个用来创建 CF 实例类型的函数:
1 | CF_EXPORT CFTypeRef _CFRuntimeCreateInstance(CFAllocatorRef allocator, CFTypeID typeID, CFIndex extraBytes, unsigned char *category); |
更具体的解释可以查看 CFRuntime.h 对它的定义。
CFRunLoopCreate 给它传入了一个默认的分配器 kCFAllocatorSystemDefault ,一个 CFRunLoopGetTypeID() ,一个 size 。
CFRunLoopGetTypeID() 的操作如下:
1 | CFTypeID CFRunLoopGetTypeID(void) { |
它在里面注册了 CFRunLoopClass 和 CFRunLoopModeClass 的类型,并用返回值,给对应的 typeID 赋值。作为单例,只运行一次。
size 的计算为 :
1 | uint32_t size = sizeof(struct __CFRunLoop) - sizeof(CFRuntimeBase); |
size 是一个 CFRunLoop 类型本身的大小,减掉 CFRuntimeBase 类型的大小得到的结果。
为什么要减去一个 CFRuntimeBase 的类型大小?
查看 CFRuntime.c 对源码,发现里面会把减掉的 sizeof(CFRuntimeBase) 再给加回来:
1 | CFIndex size = sizeof(CFRuntimeBase) + extraBytes + (usesSystemDefaultAllocator ? 0 : sizeof(CFAllocatorRef)); |
CFRunLoopFindMode
1 | static CFRunLoopModeRef __CFRunLoopFindMode(CFRunLoopRef rl, CFStringRef modeName, Boolean create) |
CFRunLoopFindMode 是一个用来查找 mode 的函数,同时也可以来创建 mode 。
它其中有利用两个宏,来对 timer 的种类进行判断.查阅了一下定义:
1 | #if DEPLOYMENT_TARGET_MACOSX |
也就是说,在 MACOSX 下,同时还会有使用 dispatch timer 来做定时器。而 MK_TIMER 是两个平台下都有的。
函数的大体逻辑是先判断有无,有就返回. 没有的话,就根据 create 的值决定是否新创建一个 mode .
在 CFRunLoopCreate 里面,调用的代码是
1 | CFRunLoopFindMode(loop, kCFRunLoopDefaultMode, true) |
它会新创建一个 mode 返回。
运行 Run Loop
启动 Run Loop 有 2 个函数,一个是 CFRunLoopRun
, 一个是 CFRunLoopRunInMode
:
- DefaultMode 启动
1 |
|
注:1.0e10,这个表示1.0乘以10的10次方,这个参数主要是规定RunLoop的时间,传这个时间,表示线程常驻。
主线程的RunLoop调用函数,就是使用了 CFRunLoopRun
- 指定 Mode 启动,允许设置RunLoop超时时间
1 |
|
查看上面两个启动 Run Loop 运行的函数实现,发现都使用了 CFRunLoopRunSpecific
.
CFRunLoopRunSpecific
1 | SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */ |
1.通过 runloop 的 modeName 查找当前 mode。因为 CFRunLoopFindMode 的 create 参数为 false , 如果没找到,直接为 null ,不会创建新的 mode.
2.如果当前 mode 为空,函数结束,返回 CFRunLoopRunFinished .
这里比较奇怪的是
Boolean did = false
直接写死了 did 的值,后面又是return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished
. 怀疑 did 的值,应该还有一段代码是决定kCFRunLoopRunHandledSource
的结果,被苹果隐藏了没有开源出来。
3.如果当前 mode 存在,做一些赋值操作 .
4.向观察者发送 kCFRunLoopEntry 的消息,即将进入 RunLoop .
5.进入 CFRunLoopRun 函数,在这里做一系列观察和操作。
6.向观察者发送 kCFRunLoopExit 的消息,即将退出 RunLoop .
关于 CFRunLoopRun 在运行的时候,有 4 个枚举值表示它的状态:
1 | /* Reasons for CFRunLoopRunInMode() to Return */ |
CFRunLoopDoObservers
CFRunLoopDoObservers 的官方文档说明如下:
A CFRunLoopObserver provides a general means to receive callbacks at different points within a running run loop. In contrast to sources, which fire when an asynchronous event occurs, and timers, which fire when a particular time passes, observers fire at special locations within the execution of the run loop, such as before sources are processed or before the run loop goes to sleep, waiting for an event to occur. Observers can be either one-time events or repeated every time through the run loop’s loop.
Each run loop observer can be registered in only one run loop at a time, although it can be added to multiple run loop modes within that run loop.
一个 CFRunLoopObserver 提供了一个通用的方法,在不同的时机去接受运行中的 runloop 的回调。与在一个异步事件发生时触发的源,和在特定时间之后触发的定时器相比,在 run loop 执行的过程中, 观察者会在特定的位置发送信号,例如 sources 执行之前活着 run loop 将要休眠之前,等待事件的发生. 观察者可以是一次性的,或者在通过每次 run loop 的循环里重复。
每个 run loop 观察者只能在 run loop 中注册一次,尽管它可以添加到该 run loop 内的多个 run loop mode 中。
CFRunLoopRun
这里其实有两个 CFRunLoopRun 函数,一个是暴露给我们在外面使用的,不带参数的:
1 | CF_EXPORT void CFRunLoopRun(void); |
现在要说的,是这一个:
1 | static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) __attribute__((noinline)); |
因为函数比较长,所以分段来进行讲解.
1.runloop 状态判断 / GCD 队列的端口设置:
1 | //获取开始时间 |
- HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY
1 | #define HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY 0 |
这里对于它的定义是 0 ,写死了。我猜测应该还是有一个函数去做判断的。
目前只能从字面意思猜测,代表是否只分发 dispatch 消息的
- _dispatch_get_main_queue_port_4CF
1 | #define _dispatch_get_main_queue_port_4CF _dispatch_get_main_queue_handle_4CF |
它是 dispatch_get_main_queue_handle_4CF 的宏,存在 libdispatch 中,里面对它的实现为:
1 | dispatch_runloop_handle_t |
返回的是主线程 runloop 所关联的的端口。
2.MACOSX 下,声明一个 mode 的队列通信端口(在 MACOSX 环境中):
1 |
|
3.根据超时 seconds 的时长,做对应操作。
1 | dispatch_source_t timeout_timer = NULL; |
4.进入 do - while 循环,直到 reVal 不为 0 。以下代码为更好理解,删去 windows 相关:
1 | // 设置判断是否为最后一次 dispatch 的端口通信的变量 |
不透明的对象封装状态由 voucher_mach_msg_adopt() 改变,它代表一种 mach_msg 通信时的状态。
- HANDLE:
1 | DISPATCH_EXPORT HANDLE _dispatch_get_main_queue_handle_4CF(void); |
返回作为主队列相关联的 run loop 。
- memset :作用是在一段内存块中填充某个给定的值,它是对较大的结构体或数组进行清零操作的一种最快方法。
5.释放 timerout_timer 定时器相关
1 | if (timeout_timer) {//如果存在,取消并释放 |
CFRunLoopServiceMachPort
这个函数是让线程休眠的关键,它在里面做了和 mach port 相关的操作。
1 | static Boolean __CFRunLoopServiceMachPort(mach_port_name_t port, mach_msg_header_t **buffer, size_t buffer_size, mach_port_t *livePort, mach_msg_timeout_t timeout, voucher_mach_msg_state_t *voucherState, voucher_t *voucherCopy) { |
这里有查询 CFRUNLOOP_SLEEP() 和 CFRUNLOOP_POLL() 等函数,都是 do { } while (0)
这样的宏,没有真正实现代码,所以无法再看到具体的情况。
总结
这一次学习的过程,最大的感触,就是对于知识的相通性。
例如对于 TSD 线程私有数据的理解,搜寻很多跟 iOS 有关资料都找不到说明,最后是在 unix 相关的文章才看到解释。还有 Event Loop 的机制在其它平台等实现。
比较遗憾的是,有一些地方,苹果并没有给出具体的代码实现或者明确的解释。