注:以Core Foundation的实现作为参考(版本为CF-855.17)。
1. 相关数据结构
1.1 run loop的基本结构
CFRunLoopRef,是__CFRunLoop的结构体指针:
|
|
作为RunLoop的基本数据结构,CFRunLoopRef内部主要包含了:
- _commonModes:标记为“common”的mode的集合
- _commonModeItems:所有“common”mode中共享的item,即修改此内部的item,就会对所有的“common”的mode内部的item进行更新。
- _currentMode:当前所在的mode模式,切换时要退出重新指定
- _modes:所有支持的mode。
常用API:
|
|
既然引入了mode这一概念,我们就看看mode到底是个啥。
1.2 RunLoop的Mode
RunLoop的mode为内部数据,没有对外公开。类型为CFRunLoopModeRef,即__CFRunLoopMode的结构体指针。看一下简化过的代码结构:
|
|
其中,CFRunLoopModeRef中主要包含了如下几个内容:
- _name:用于识别区分不同的mode实例。切换mode时可以使用。
- _sources0:非基于端口的源的集合(我们在线程中自己添加的一般都是source0)
- _sources1:基于端口的源的集合(通过Mach Port进行线程间通信使用)
- _observers:观察者的数组,用于runloop在不同状态时发送通知的接收对象。
- _timers:定时器数组,包含的就是NSTimer对象。
常用API:
|
|
下面我们分着来看各部分的数据结构。
1.3 RunLoop源
CFRunLoopSourceRef,即__CFRunLoopSource结构体的指针。其基本结构如下:
|
|
其中,联合体中的成员version0和version1即为上面mode中的source0和source1。大概看一下二者的结构体:
|
|
可以看到,二者内容基本相同,都是包含了内存管理、相等性、hash等信息。不同的是source1的结构体(CFRunLoopSourceContext1)包含了mach_port相关函数。
因此,可以将source根据类型(version成员的0和1)分为source0和source1。
常用API:
|
|
1.4 Run Loop的定时器
定时器为CFRunLoopTimerRef类型,也就是__CFRunLoopTimer的结构体指针。
|
|
其中,主要包含了:
- _runLoop:绑定到的run loop
- _interval:执行事件间隔(重复执行的定时器)
- _tolerance:宽容度,也就是指定执行时间的最大延迟时间
- _callout:执行的回调函数指针
- _context:定时器上下文对象,包含内存管理等相关函数
|
|
常用API:
|
|
1.5 Run Loop的观察者
观察者为CFRunLoopObserverRef类型,即__CFRunLoopObserver的结构体指针。
|
|
其中,主要包含:
- _runLoop:对应的run loop
- _activities:用于监听的run loop的指定运行状态。
- _order:优先级
- _callout:收到监听通知的回调函数指针。
- _context:上下文对象
activities,即CFOptionFlags,用于识别run loop在不同执行时刻的运行状态:
|
|
上下文对象为CFRunLoopObserverContext结构体,内部包含了内存管理的相关函数指针:
|
|
回调函数的类型为:
|
|
常用API:
|
|
2. 重点API基本实现
2.1 CFRunLoopGetMain & CFRunLoopGetCurrent
系统隐藏了生成过程,通过获取线程对应的runloop对象,即可完成创建。
|
|
可以看到,真正的实现都在 _CFRunLoopGet0 函数中:
|
|
通过实现可以看到:
- runloop对象整体懒加载,不调用不生成。
- runloop对象保存在全局字典中,key为线程指针。
- 系统第一次调用时,生成全局字典,且自动创建主线程的runloop,插入其中,因此APP可以保持运行。
2.2 CFRunLoopRun
首先看一下run loop的启动过程:
|
|
在以上两个启动run loop的函数中,真正的实现都包含在CFRunLoopRunSpecific中,且此函数会返回run loop的执行状态(由CFRunLoopRunInMode()函数返回)。
|
|
现在,我们看一下运行循环,到底是如何循环的。
|
|
CFRunLoopRunSpecific函数的整体运行,是一个“创建新现场 – 执行loop – 恢复旧现场”的过程。
- 根据指定的modeName名称,获取run loop内部保存的mode对象。将其设置为currentMode,然后创建新的perRunData进行设置。且如果存在进入循环的观察者,发通知进行告知。
其中,_per_run_data指针标记为volatile,即告知编译器要直接从原地址读取值,而不是从寄存器中读取(防止多线程更改时不修改寄存器中的值导致的数据不一致问题)。__CFRunLoopPushPerRunData函数的内部实现为:
|
|
- 将指定run loop mode中的_observers取出,单独放到一个指定数组中进行处理(超出1024个observer后要单独开辟内存)。由于调用callback函数为同步操作,由可能会长时间锁住runloop和mode。故单独处理,防止影响runloop的执行效率。
- 将监听的activity对应的observer插入到数组容器中。activity即上面说过的CFOptionFlags,也就是runloop运行过程中的各种监控时间点。
- 插入到数组容器时要对observer进行保留操作,使用后要对observer进行释放操作,内存管理要时刻谨记。
- 在observer数组中依次取出,通过 CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION 函数对observer的_callout进行函数调用。
|
|
主要就是直接进行函数调用(getpid神马进程相关的,就不动了。。。)。
2.3 __CFRunLoopRun
前面说了,__CFRunLoopRun个才是核心所在,真正的“loop”在这里执行。
由于代码过长,我们分步骤看。
2.3.1 其他(非do-while循环)
|
|
非核心部分主要做了三件事:
- 对runloop和currentMode的运行状态进行判断,防止无畏的运行。
- 根据情况,查看端口是否为GCD的主队列端口(用于后面通过主线程自身端口调用mach_msg函数,执行主队列的异步任务)。
- 根据设置的超时时间,使用GCD添加一个定时器,超时时直接释放runloop。最后结尾时检查移除此定时器。
2.3.2 do-while运行循环
|
|
虽然代码很多(已经大幅度简化),但实际上只是分为三部分:
- 根据运行状态,进行预处理:
- 在不同运行状态,给观察者发通知(准备执行timer、准备执行source)
- 执行block
- 执行配置到mode中的source0,成功后再次检测执行block。实现函数为 CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION
- 准备处理GCD事件(通过mach_msg函数,自身主队列的port执行,存在则直接去处理任务)
- 没有其他事件时,通过mach_msg函数,让线程进入睡眠,同时在接收消息后自动唤醒
- 处理消息部分(线程已被唤醒 或 直接进入处理),根据端口类型,对执行的工作进行区分:
- 执行timer:CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION,其入口函数 __CFRunLoopDoTimers 与 __CFRunLoopDoObservers 实现相似:先将符合要求(刚刚到达或者稍微过时一点的)的timer加入到数组中,防止长时间锁住runloop,然后依次调用 __CFRunLoopDoTimer ,也就是calling_out函数,完成回调。
- 执行GCD主队列的异步任务:CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE
- source1的主动唤醒任务:mach_msg,进入内核态,直接进行线程通信
- 再次检测执行block
- 根据状态设置retVal值,决定运行循环是否继续。如果状态都不满足,仍为0,则继续执行循环,保证线程不退出。