❝这里的RunLoop的源码参考了两个地址的源码,一个是GitHub上的https://github.com/apple/swift-corelibs-foundation中的runloop源码,一个是https://opensource.apple.com/source/CF/中的runloop源码,主要是以后者为准,因为后者猜测更接近为iOS上的版本,前者应该为其他平台上的实现版本。
RunLoop
RunLoop是和线程相关的基础架构的一部分。一般来说,一个线程执行完任务之后就会退出,但是这在Cocoa Touch中是行不通的,比如main thread,需要不停地去处理点击事件等等。而Runloop的目的就在于此,它让线程在有任务时工作,在无任务时休息。这种模型一般称为Event Loop,即事件循环,通常逻辑如下:
void CFRunLoopRun(void) {
int result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(),
KCFRunLoopRunLoopDefaultMode, ...);
} while (KCFRunLoopRunStopped != result && KCFRunLoopRunFinished != result);
}
Event Loop是在程序中等待和派发事件或者消息的一种设计模式。当Event Loop形成一个程序的中央控制流时,通常情况下,称之为主循环。Event Loop在很多程序中都有实际的应用,比如Windows程序中的消息循环,MacOS中的事件循环等等。
而在iOS中RunLoop是一个对象,这个对象管理了需要处理的事件,并且提供了一个入口函数来处理。
let runloop = RunLoop.current()
runloop.add(Port.init(), forMode: .common)
runloop.run()
线程在执行了这个函数之后,就会一直处于“接受消息 -> 等待 -> 处理”的循环中,直到循环结束,函数返回。
在iOS中有两个类来管理RunLoop,一个是RunLoop类,一个是CFRunLoopRef。CFRunLoopRef是CoreFoundation框架封装了,提供了面向对象的API,所有这些API都是线程安全的。RunLoop是基于CFRunLoop的封装,提供了面向对象的API,但是这些API并不是线程安全的。
同时要注意RunLoop实例调用的**run()**方法并不是直接调用的该方法,而是封装的以下方法:
extension RunLoop {
public func run() {
while run(mode: .default, before: Date.distantFuture) { }
}
public func run(until limitDate: Date) {
while run(mode: .default, before: limitDate) && limitDate.timeIntervalSinceReferenceDate > CFAbsoluteTimeGetCurrent() { }
}
......
}
RunLoop和线程
iOS中的线程,我们一般是使用Thread以及pthread_t 来管理的。通过开源的源码可以看到,Thread是封装了pthread_t的,而pthread_t是直接包装最底层的mach thread。同时Thread和pthread_t是一一相关的。
在Swift的Runloop中,Runloop对象是对CFRunLoop的封装,而CFRunLoop是基于pthread_t来进行管理的。
以下是RunLoop的部分源码:
typedef pthread_mutex_t CFLock_t;
/// 全局的dictionary:key是pthread_t, value是CFRunLoopRef
static CFMutableDictionaryRef loopsDic = NULL;
// 访问loopsDic的锁
static CFLock_t loopLock = (pthread_mutex_t)PTHREAD_MUTEX_INITIALIZER;
// ** 这个方法只能通过Foundation来调用
// 比如Swift的Runloop封装CFRunloop,调用了_CFRunLoopGet2这个API,这个方法内部调用了_CFRunloopGet0
// 获取一个pthread对应的Runloop
CFRunLoopRef _CFRunloopGet0(pthread_t thread) {
// 1、首先添加互斥锁
pthread_mutex_lock(&loopLock);
//2、第一次进入,初始化全局Dic,并先为主线程创建一个RunLoop
if (!loopDic) {
loopsDic = CFDictionaryCreateMutable();
CFRunloopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_up());
CFDictionarySetValue(loopsDic, pthread_main_thread_up(), mainLoop);
}
//3、直接从全局Dic中获取
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(loopsDic, thread);
//4、如果取不到,那就创建一个
if (!loop) {
loop = __CFRunLoopCreate(thread);
CFDictionarySetValue(loopDic, thread, loop);
}
//5、保证线程安全
// 注册回调关联:当thread被销毁时,这个runloop也会被销毁!
if (pthread_equal(t, thread_self())) {
//TSD:Thread-share data 线程共享数据
//这里当前thread会持有tsdTable,同时tsdtable->data[__CFTSDKeyRunLoop]的值设为loop
_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
if (0 == _CFGetTSD(__CFTSDKeyRunloopCntr)) {
_CFSetTSD(_CFTSDKeyRunLoopCntr, __CFFinalizeRunLoop);
}
}
return loop;
}
线程和RunLoop之间是一一对应的,保存在了一个全局的Dictionary中。线程创建的时候是没有RunLoop的,如果不主动获取,那它一直都不会有。RunLoop的创建是发生在第一次获取时,RunLoop的销毁是发生线程结束时。线程结束时会调用 __CFFinalizeRunLoop 方法,这个方法中会销毁全局的Dictionary中的Key-Value对。
除了全局的Dictionary之外,两者之间还有互相持有的关系,其一是CFRunLoop的结构体中持有pthread_t, 其二是pthread_t的TSD数据中也持有runloop。
// runLoop中持有pthread
struct __CFRunLoop {
...
_CFThreadRef _pthread;
CFMutableSetRef _modes;
...
}
// pthread中持有runloop
static void __CFSDSetSpecific(void *arg) {
pthread_setspecific(_CFTSDIndexKey, arg);
}
static __CFTSDTable *__CFTSDGetTable(const Boolean create) {
...
__CFTSDTable *table = (__CFTSDTable *)__CFTSDGetSpecific();
__CFTSDSetSpecific(table);
...
return table;
}
_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
TSD
什么是pthread_t的TSD数据呢?
TSD的全称为:Thread-Specific Data即线程特有数据。TSD由具体的每一个线程来维护。TSD采用了一键多值的技术,即一个键对应多个不同的值,每个线程访问数据时通过访问该键来得到对应的数据
线程特有数据可以看作是一个二维的数组,key作为行的索引,而线程id作为列的索引。一个线程特有数据的key是一个不透明的**pthread_key_t
**数据类型。在一个进程中的所有线程都可以使用这个key。即使所有的线程使用了一样的key,它们通过这个key访问到的或者修改的线程特有数据也是不同的。
Keys | T1 Thread | T2 Thread | T3 Thread | T4 Thread |
---|---|---|---|---|
K1(__CFTSDKeyRunLoop) | 6 | 56 | 4 | 3 |
K2 | 87 | 21 | 0 | 9 |
K3 | 23 | 12 | 61 | 2 |
K4 | 11 | 76 | 47 | 88 |
以上表为例,线程T2使用的K3对应的数据是12,而线程T3使用的K3对应的数据是61。
对应到我们的代码中上述代码中的最后一行:_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
key就是_CFTSDKeyRunLoop,而value就是当前的loop。也就是说线程都会将当前的loop存储在对应的线程特有数据中。
RunLoopMode
iOSRunLoop一共有几个核心的类,分别是:
CFRunLoop
CFRunLoopMode
CFRunLoopSource
CFRunLoopObserver
CFRunLoopTimer
RunLoopMode定义
以下是源码中的关于RunLoop以及RunLoopMode的部分定义:
struct CFRunLoop {
...
pthread_t _pthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
...
}
struct CFRunLoopMode {
...
CFMutableSetRef _source0;
CFMutableSetRef _source1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
...
}
根据上面的RunLoop以及RunLoopMode的定义,我们可以得出以下的对应关系:一个RunLoop可以包含有多个Mode,一个mode可以包含多个Source0 & Source1 & Observer & Timer。
每次调用RunLoop的主函数的时候,只能指定其中一个Mode,这个Mode就被称问Common Mode。如果需要切换Mode, 只能退出RunLoop重新指定Mode进入。这样做是为了分割开不同组别的Source/Timer/Observer, 让其互不影响。RunLoop一次只在一个Mode下运行。
CFRunLoopSourceRef
Source是RunLoop的数据源(input source)的一个抽象类,Source有两个版本:Source 0和Source 1。
struct __CFRunLoopSource {
CFRuntimeBase _base;
uint32_t _bits;
pthread_mutex_t _lock;
CFIndex _order; /* immutable */
CFMutableBagRef _runLoops;
// 共用体
// 同一时刻,共用体只存放了一个被选择的成员
union {
CFRunLoopSourceContext version0; /* immutable, except invalidation */
CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
} _context;
};
通过version的不同来区分两种不同类型的Source,而这两种source也有着很明显的区别。
Source0
Souece0包含了一个回调(函数指针),它并不能直接唤醒RunLoop,只能去主动去调用方法来唤醒RunLoop。如果我们自己创建来一个Source0,想通过Source0来唤醒RunLoop,那么需要两个步骤:
发送信号:
CFRunLoopSourceSignal(rs)
手动唤醒:
CFRunLoopWakeUp(RunLoop.main.getCFRunLoop())
也就是说我们需要去调用CFRunLoopSourceSignal(source), 来标记这个Source正在等待处理,然后需要手动调用**CFRunLoopWakeUp(runloop)**来唤醒RunLoop,让它来处理这个事件。
Source1
它被RunLoop和kernel管理,通过mach_port来驱动(特指基于port的事件),比如CFMachPort, CFMessagePort,NSSocketPort。Mach Port是很轻量级的方式用于进程之间的通信,它可以被理解为一个通信的channel。比如点触事件,其实就是由BackService直接将IOEvent传递给处理该事件的前台进程。这就涉及到了进程间的通信,使用的就是Source1。
CFRunLoopTimerRef
基于时间的触发器,它和NSTimer是toll-free bridged的,可以混用(它的底层是基于mk_timer的)。它是会被RunLoop Mode所影响的(而GCD的timer是不会被RunLoopMode影响的)。当它被加入RunLoop之后,RunLoop将会注册与之对应的时间点,当时间点到达的时候,Runloop将被唤醒来执行回调,如果这个线程被阻塞了或者此时RunLoop不在这个Mode中,那么这个触发的时间点点并不会执行回调,将等到下一次循环的时间点被触发。
CFRunLoopObserverRef
它是观察者,每个Observer都包含了对应一个回调(函数指针),一个Mode中可以有多个观察者,当RunLoop的状态发生改变时,观察者就可以通过回调接受这个变化。可以观测的时间点有以下几个:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入RunLoop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出RunLoop
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
这些source & timer & observer都统一被称为mode item,一个item可以被同时加入多个mode。但是一个item被重复加入同一个mode时是不会有效果的。如果一个mode中一个item都没有,那么RunLoop就会直接退出,不进入循环。
Common Modes
同时要注意有一个概念叫做Common Mode。一个Mode可以把自己标记为Common(将mode添加到CommonMode中)。
// 当然Mode中需要加observer/Timer/source
let mode = CFRunLoopMode.init(rawValue: "fff" as CFString)
CFRunLoopAddCommonMode(RunLoop.main.getCFRunLoop(), mode)
这样的话,每当RunLoop的内容变化时(切换Mode运行时),RunLoop会自动将commonModeItems中的Source & Observer & Timer同步到具有"Common"标记的所有Mode中。
举个例子:主线程的RunLoop中有两个预先设置的Mode,其一是kCFRunLoopDefaultMode, 其二是UITrackingRunLoopMode。这两个Mode都被标记为Common属性。DefaultMode是默认状态,TrackingMode是追踪ScrollView滑动时的状态。当创建一个Timer时,Timer会得到重复的回调。但是当滑动一个UIScrollView时,RunLoop会将Mode切换都TrackingMode,这个时候Timer的回调就不会生效了。
所以需要这个Timer在两个Mode中都可以得到回调,那么其中一种方式就是将这个Timer分别放入到这两个Mode中。还有一种方式,就是将Timer放到顶层的RunLoop的CommonModeItems中,这其中的items都会被自动更新到所有具备Common属性的Mode中。
Mode的分类
从Apple的官方文档中,我们可知其实系统提供了几个Mode:
kCFRunLoopDefaultMode: App的默认Mode,通常主线程是在这个Mode下运行的。
UITrackingRunLoopMode:界面追踪Mode,用于ScrollView的追踪,保证界面滑动不受影响
UIInitializationRunLoopMode:刚启动App时进入的第一个Mode,启动后不再使用
GSEventReceiveRunLoopMode:接受系统事件的内部Mode,通常用不到
KCFRunLoopCommonModes:占位的Mode
Mode实例
直接将当前线程的RunLoop在控制台输出,可以得到如下的结构:
CFRunLoop {
current mode = KCFRunLoopDefaultMode,
common modes = {
UITrackingRunLoopMode,
KCFRunLoopDefaultMode
}
common mode items = {
//source 0
CFRunLoopSource = { order = -1,{
callout = PurpleEventSignalCallback}} //GraphicsServices
CFRunLoopSource = { order = 0,{
callout = HandleDelegateSource(void*)}} //WebCore
CFRunLoopSource = { order = 0, {
callout = __NSThreadPerformPerform}} //Foundation
CFRunLoopSource = { order = 0, {
callout = FBSSerialQueueRunLoopSourceHandler}} // FrontBoardServices
CFRunLoopSource = { order = -1,{
callout = __eventQueueSourceCallback}} //UIKitCore
CFRunLoopSource = { order = 0,{
callout = WTF::RunLoop::performWork(void*)}} //JavaScriptCore
CFRunLoopSource = { order = -2,{
callout = __eventFetcherSourceCallback}} //UIKitCore
// source 1(mach port)
CFRunLoopSource {order = 0, {port = 17923}}
CFRunLoopSource {order = 0, {port = 12039}}
CFRunLoopSource {order = 0, {port = 16647}}
CFRunLoopSource {order =-1, {
callout = PurpleEventCallback}}
CFRunLoopSource {order = 0, {port = 2407,
callout = _ZL20notify_port_callbackP12__CFMachPortPvlS1_}}
CFRunLoopSource {order = 0, {port = 1c03,
callout = __IOHIDEventSystemClientAvailabilityCallback}}
CFRunLoopSource {order = 0, {port = 1b03,
callout = __IOHIDEventSystemClientQueueCallback}}
CFRunLoopSource {order = 1, {port = 1903,
callout = __IOMIGMachPortPortCallback}}
// observer
CFRunLoopObserver {order = -2147483647, activities = 0x1, // Entry
callout = _wrapRunLoopWithAutoreleasePoolHandler}
CFRunLoopObserver {order = 0, activities = 0x20, // BeforeWaiting
callout = _UIGestureRecognizerUpdateObserver}
CFRunLoopObserver {order = 1999000, activities = 0xa0, // BeforeWaiting | Exit
callout = _beforeCACommitHandler}// UIKitCore
CFRunLoopObserver {order = 1999000, activities = 0xa0, // BeforeWaiting | Exit
callout = _afterCACommitHandler} // UIKitCore
CFRunLoopObserver {order = 2000000, activities = 0xa0, // BeforeWaiting | Exit
callout = CA::Transaction::observer_callback(__CFRunLoopObserver*, unsigned long, void*)}
CFRunLoopObserver {order = -2147483648, activities = 0x46,// BeforeTimer | BeforeSource | AfterWait
callout = __trackRunLoopTimes} // UIKitCore
CFRunLoopObserver {order = 2147483647, activities = 0x20, // BeforeWaiting
callout = __trackRunLoopTimes} // UIKitCore
CFRunLoopObserver {order = 2000000, activities = 0xa0, // BeforeWaiting | Exit
callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv}
CFRunLoopObserver {order = 2147483647, activities = 0xa0, // BeforeWaiting | Exit
callout = _wrapRunLoopWithAutoreleasePoolHandler}
// timer
CFRunLoopTimer {valid = Yes, firing = No, interval = 3.1536e+09, tolerance = 0,
next fire date = 1.17402502e+09 (504911209 @ 12130246606742925),
callout = CA::timer_callback(__CFRunLoopTimer*, void*)} // QuartzCore
CFRunLoopTimer {valid = Yes, firing = No, interval = 3, tolerance = 0,
next fire date = 669113787 (-20.825485 @ 12377085760065),
callout = [FSPagerView.FSPagerView flipNextWithSender:]}
}
modes = {
CFRunLoopMode { //kCFRunLoopDefaultMode
sources0 = { /* same as 'common mode items' */ },
sources1 = { /* same as 'common mode items' */ },
observers = { /* same as 'common mode items' */ },
timers = { /* same as 'common mode items' */ },
},
CFRunLoopMode { //UITrackingRunLoopMode
sources0 = { /* same as 'common mode items' */ },
sources1 = { /* same as 'common mode items' */ },
observers = { /* same as 'common mode items' */ },
timers = { /* same as 'common mode items' */ },
},
CFRunLoopMode { // kCFRunLoopCommonModes
source0 = null,
source1 = null,
timers = null,
observers = null,
},
CFRunLoopMode { // GSEventReceiveRunLoopMode
source0 = {
CFRunLoopSource {order = 1,
callout = PurpleEventSignalCallback}
},
source1 = {},
observers = null,
timers = null,
}
}
}
}
我们可以很明确的看到,一个RunLoop下对应有很多种不同的Mode,而每个Mode中都有对应的source,observer等元素。当然要想看到更多的Mode类型的话,可以到Apple关于RunLoop的官方描述中查看。
RunLoop的结构
接下来我直接PO一段RunLoop的源码:
// GitHub上开源的CoreFoundation,run方法其实还是调用了运行defaultMode的方法
extension RunLoop {
public func run() {
while run(mode: .default, before: Date.distantFuture) { }
}
...
}
// 使用指定的模式来启动RunLoop
int CFRunInMode(modeName, seconds, returnAftersourceHandled) {
return CFRunloopRunSpecific(CFRunLoopGetCurrent(),
modeName,
seconds,
returnAftersourceHandled);
}
int CFRunloopRunSpecific(runloop, modeName, seconds, returnAftersourceHandled) {
// 根据mode名找到对应的mode
CFRunLoopModeRef currentMode = __CFRunLoopMode(r1, modeName, false);
if (Null == currentMode || CFRunLoopModeIsEmpty(currentMode)) {
return;
}
int result = KCFRunLoopRunFinished;
// 1、通知observers:RunLoop即将进入RunLoop (KCFRunLoopEntry)
CFRunLoopDoObservers(rl, currentMode, KCFRunLoopEntry);
result = __CFRunLoopRun(runloop, currentMode, ...);
// 内部函数
int __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled) {
bool didDispatchPortLastTime = true;
in retVal = 0;
do {
// runloopMode中的所有port
__CFPortSet waitSet = rlm->_portSet;
// 2、通知observers:RunLoop即将准备处理timer
CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
// 3、通知observers:RunLoop即将准备处理source(不基于port的即source0)
CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
// -- 执行被加入的block
CFRunLoopDoBlocks(rl, rlm);
// 4、处理Source0:Runloop处理source(不基于port的source)
bool sourceHandledThisLoop = CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
// -- 执行被加入的block
if (sourceHandledThisLoop) {
CFRunLoopDoBlocks(rl, rlm);
}
// 5、如果有Source1处于Ready状态:RunLoop跳到第九步处理Source(基于port的source)
// 这里指定了接收消息的端口是dispatchPort:如Dispatch.main.async{}
if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
bool hasMsg = CFRunLoopServiceMachPort(dispatchPort, msg, &livePort, 0);
if (hasMsg) goto handle_msg;
}
didDispatchPortLastTime = false;
// 6、通知Observers:RunLoop即将进入休眠(kCFRunLoopBeforeWaiting)
CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
// 7、线程进入睡眠:调用CFRunLoopServiceMachPort等待被唤醒,被以下几个唤醒
// * 一个基于port的source事件(·)
// * 一个到点要执行的timer(Timer)
// * RunLoop要超时了(RunLoopTimeOut)
// * RunLoop被显式的唤醒了
CFRunLoopServiceMachPort(waitSet, msg, livePort, TIMEOUT_INFINITY);
// 8、通知Observers:线程被唤醒了
CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
// 9、收到消息,处理消息。
handle_msg;
// * 无port啥也不做
if (Mach_Port_Null == livePort) {
CFRUNLOOP_WAKEUP_FOR_NOTHING();
// * 标记为唤醒
} else if (livePort == rl->_wakeUpPort) {
CFRUNLOOP_WAKEUP_FOR_WAKEUP();
// * 标记为Timer唤醒:Timer/NSTimer唤醒
} else if (livePort == rlm->_timerPort) {
CFRUNLOOP_WAKEUP_FOR_TIMER();
if (!_CFRunLoopDoTimers(rl, rlm, mach_absolute_timer())) {
CFArmNextTimerInMode(rlm, rl);
}
// * 标记为GCD唤醒:由main_queue派发的block
} else if (livPort === dispatchPort) {
CFRUNLOOP_WAKEUP_FOR_DISPATCH();
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
sourceHandledThisLoop = true;
didDispatchPortLastTime = true;
// * 标记为Source1唤醒
} else {
CFRUNLOOP_WAKEUP_FOR_SOURCE();
CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
__CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply)
}
// 执行加入到runloop中的block
CFRunLoopDoBlocks(rl, rlm);
// 判断runloop是否结束
// * 刚刚处理完事件
if (sourceHandledThisLoop && stopAfterHandle) {
retVal = kCFRunLoopRunHandledSource;
// * runloop超时了
} else if (timeout) {
retVal = kCFRunLoopRunTimedOut;
// * 被外部调用者强制停止了
} else if (CFRunLoopIsStopped(rl)) {
retVal = kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
rlm->_stopped = false;
retVal = kCFRunLoopRunStopped;
// * runloop的Mode中source/timer/observer一个都没有了
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
retVal = kCFRunLoopRunFinished;
}
} while (0 == retValue);
}
//10、通知Observers:RunLoop即将离开
CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}
它的内部逻辑是一个while循环,可以一直使线程处于存活的状态,有事情来就处理事情,没有事情来,就处于休息状态。基于上方的流程,我也创建了一个RunLoop流程图。
根据上述的流程,我们可以梳理出相应的回调:
{
/// 1. 通知Observers,即将进入RunLoop
/// 此处有Observer会创建AutoreleasePool: _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
do {
/// 2. 通知 Observers: 即将触发 Timer 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 4. 触发 Source0 (非基于port的) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 6. 通知Observers,即将进入休眠
/// 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
/// 7. sleep to wait msg.
mach_msg() -> mach_msg_trap();
/// 8. 通知Observers,线程被唤醒
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
/// 9. 如果是被Timer唤醒的,回调Timer
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
/// 9. 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
/// 9. 如果如果Runloop是被 Source1 (基于port的) 的事件唤醒了,处理这个事件
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
} while (...);
/// 10. 通知Observers,即将退出RunLoop
/// 此处有Observer释放AutoreleasePool: _objc_autoreleasePoolPop();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}
iOS系统的历史
Mac OS X融合了Mac OS Classic和NextStep的优点:Mac OC Classic的GUI以及NextStep的架构。
全新的Mac OS X在设计与实现上都和NextStep非常接近,诸如Cocoa、Mach、Interface Builder等核心组件都源于NextStep。
iOS最初被称为iPhone OS,它是OS X对应移动平台的分支,本质上iOS就是Mac OS X。而iOS也是iPad OS,tvOS,watchOS这三者的基础。正因为本质上iOS就是Mac OS X,所以iOS拥有和和Mac OS一样的操作系统层次结构以及相同的操作系统核心Dawin。
iOS系统的架构
Apple在关于OS X以及iOS系统架构的文档中,展示了非常简洁的分层,某种意义上,甚至有些过于简单
The User ExperienceLayer(用户UI层)
包括Aqua,Dashboard,Spotlight以及一些特性。在iOS中,用户体验完全取决于SpringBoard,同时, iOS中Spotlight也是支持的。
The Application Frameworks layer(应用框架层)
包括Cocoa,Carbon以及Java。然而在iOS中,只有Cocoa(严格来讲,Cocoa Touch,Cocoa的派生物)
The Core Frameworks(核心框架层)
有时也被称为图形和媒体层(Graphic and Media layer)。包括核心框架,Open GL以及Quick Time。
Darwin(系统核心层)
操作系统核心——kernel以及UNIX shell的环境。
在以上的这些层级中,Darwin是完全开源的,而顶部的其他层级都是闭源的,Apple保持专利。iOS 和 Mac OS整体上是非常像的,但是还是有一些细微的不同。比如iOS使用的是Spring Board而OS X使用的是Aqua,因为前者是针对触屏操作,而后者针对的是鼠标操作。如果深入的看看Darwin,可以得到如下结构:
要明确的是Darwin的核心是XNU内核。它是一个混合内核,将宏内核和微内核两者的特点兼收并蓄: 比如为微内核中提高操作系统模块化程度,以及内存保护和消息传递的机制;还有宏内核在高负荷下表现的高性能。XNU主要是由Mach,BSD,以及IOKit组成的。
❝上面这张图提出了一个问题:在什么时候会发生用户态和内核态的切换? 用户态和内核态的区分是非常明显的,但是应用会频繁去使用内核服务,所以这两种态(用户态和内核态)之间的转换就需要一种高效的 且安全的方式。在XNU内核中用户态和内核态的切换有两种情况: 其一是主动切换。
XNU主要的核心其实是Mach,它作为微内核,只处理操作系统最基础的一些职责,提供了进程和线程的抽象、虚拟内存的管理、任务调度、进程间通信(IPC)这些基本的功能。而XNU暴露给用户的是BSD层,这一层对下在一些底层的功能上使用了Mach,对上,它给应用提供了流行的POSIX API,这也使得OSX系统对于许多其他的UNIX实现是兼容的。
Mach只具备有限的API,它并不是要成为一个五脏俱全的操作系统,它只是提供一些基本的功能,没有它,那么操作系统也无法工作。而一些其他的功能诸如文件管理以及设备访问,都是由它的上一层也就是BSD层来处理的,这一层提供了一些更高层级的抽象,比如The POSIX线程模型(Pthread),文件系统,网络等功能。
Mach
Mach拥有一个很简单的概念:一个最小的核心支持一个面向对象的模型,其中的子系统通过Message相互通信。其他的操作系统都是提供了一个完整的模型,而Mach提供了一个基本的模型,可以在此基础上实现操作系统本身,OS X的XNU是Mach之上的一个特殊实现。
在Mach中,一切都被视为对象。进程(Mach中称为tasks),线程以及虚拟内存都是对象,每一个都有它的属性。但是这个并不是值得大书特书的地方,因为其他的操作系统也可以使用对象来实现。真正让Mach不同的是它选择通过消息传递(Message Passing)来实现对象之间的通信。
所以Mach最基础的概念就是两个端点(Port)中交换的message,这就是Mach的IPC(进程间通信)的核心。
Mach中的消息,定义在<mach/message.h>文件中,简单来说,一个message就是msgh_size大小的blob, 带着一些flags,从一个端口发送到另一个端口。
typedef struct {
mach_msg_header_t header;
mach_msg_body_t body;
} mach_msg_base_t;
// 消息头是必须的,它定义了一个消息所需要的数据
typedef struct {
mach_msg_bites_t msgh_bits;
mach_msg_size_t msgh_size;
mach_port_t msgh_remote_port;
mach_port_t msgh_local_port;
mach_msg_size_t msgh_reserved;
mach_msg_id_t msgh_id;
} mach_msg_header_t;
Mach Message发送和接收消息都使用了同样的API:mach_msg()。这个方法在用户态和内核态都有实现。它通过option参数来决定是收消息,还是发消息。
mach_msg_return_t mach_msg(mach_msg_header_t msg,f
mach_msg_option_t option,
mach_msg_size_t send_size,
mach_msg_size_t reveive_limit,
mach_port_t reveive_name,
mach_msg_timeout_t timeout,
mach_port_t notify,
);
在发送消息或者接收消息的时候,在用户态中Mach message使用了mach_msg() ,它会通过内核的Mach trap机制调用对应的内核方法mach_msg_trap(),。而这个mach_msg_trap()会调用到mach_msg_overwrite_trap(), 这个方法通过MACH_SEND_MSG或者是MACH_RCV_MSG的flag来决定是发送操作,还是接受操作。
具体关于mach_msg_trap()如何工作的,可以看Apple开源的xnu中关于mach的源码(https://opensource.apple.com/source/xnu/xnu-1228/osfmk/ipc/mach_msg.c.auto.html)。同时本文中的大量关于系统和架构中的知识点均参考自《Mac OS X and iOS Internals To the Apples Core》。
RunLoop接受消息
接下来我们回到RunLoop,首先问一个问题:RunLoop中是如何实现被唤醒的呢?
从源码中可知,在RunLoop即将进入休眠状态之后,它会调用**CFRunLoopServiceMachPort()方法,而这个方法内部会调用mach_msg()方法。所以RunLoop的唤醒就是通过mach_msg()**方法来接受port或者port set的消息,被唤醒后接着再处理相应的任务。以下是这两个方法的定义:
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);
mach_msg_return_t mach_msg
(mach_msg_header_t msg,
mach_msg_option_t option,
mach_msg_size_t send_size,
mach_msg_size_t receive_limit,
mach_port_t receive_name,
mach_msg_timeout_t timeout,
mach_port_t notify);
mach_msg方法上述已经提到过了,它既用于发送消息,也用于接受消息。而在Runloop的这个实际应用场景下,它只用于接受消息。以下是对这个方法中的各个参数含义的解释:
msg: 是mach_msg用于发送和接受消息的消息缓冲区
option: Message的options是位值,按位或来结合。应该使用MACH_SEND_MSG和MACH_RCV_MSG中的一种或两种。
send_size: 当发送消息时,指定要发送的message buffer的大小。否则就是零。
receive_limit: 当接受消息时,指定接受的message buffer的大小。否则就是零。
receive_name:当接受消息时,指定了端口或者端口集。消息就是从receive_name指定的端口中接受的。否则就是MACH_PORT_NULL。
timeout:当使用MACH_SEND_TIMEOUT或者MACH_RCV_TIMEOUT选项时,指定放弃前需要等待的时间(单位为毫秒),否则就是MACH_MSG_TIMEOUT_NONE。
notify: 当使用MACH_SEND_CANCEL,MACH_RCV_NOTIFY和MACH_SEND_NOTIFY选项时,指定用于notification的端口。否则就是MACH_PORT_NULL
mach_msg调用用于接受和发送mach消息,它是用相同的缓冲区去来发送和接受消息,也就是msg参数对应的消息缓冲区。
typedef struct {
mach_msg_bites_t msgh_bits;
mach_msg_size_t msgh_size;
mach_port_t msgh_remote_port;
mach_port_t msgh_local_port;
mach_msg_size_t msgh_reserved;
mach_msg_id_t msgh_id;
} mach_msg_header_t;
消息接收
当接受消息的时候,实际上是使来着端口的消息出消息队列。receive_name指定了要从中接受消息的端口或者端口集。
如果指定了端口(port),那么调用者必须拥有该端口的权限,并且该端口不能是端口集的成员。如果没有任何消息,那么调用会被阻塞,根据MACH_RCV_TIMEOUT选项来决定放弃等待的时机。
如果指定了端口集(port set),那么调用者将接收到发送到任何端口成员的消息。端口集没有成员是允许的,并且可以在端口集接收的过程中添加和删除端口。而接收到的消息头中的magh_local_port字段指定消息来着端口集中的哪个端口。
接下来我们再回到RunLoop中的源码调用中,来看这个方法的调用:
// ** 首先是外层
// 1、处理Source1事件的时候,调用了该方法
CFRunLoopServiceMachPort(dispatchPort,
&msg,
sizeof(msg_buffer),
&livePort,
0,
&voucherState,
NULL)
// 2、进入休眠状态的时候,调用了该方法
CFRunLoopServiceMachPort(waitSet,
&msg,
sizeof(msg_buffer),
&livePort,
poll ? 0 : TIMEOUT_INFINITY,
&voucherState,
&voucherCopy);
// ** 然后是里层
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) {
for(;;) {
···
ret = mach_msg(msg,
MACH_RCV_MSG|(voucherState ? MACH_RCV_VOUCHER : 0)|MACH_RCV_LARGE|((TIMEOUT_INFINITY != timeout) ? MACH_RCV_TIMEOUT : 0)| MACH_RCV_TRAILER_TYPE(MACH_MSG_TRAILER_FORMAT_0) | MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_AV),
0,
msg->msgh_size,
port,
timeout,
MACH_PORT_NULL);
···
}
}
端口接收:dispatchPort
也就是说在处理source1事件的时候,需要接受的消息是从dispatchPort端口的消息队列中接受的,而这个端口:dispatchPort = _dispatch_get_main_queue_port_4CF(),所以这里只处理GCD的主队列的事件,同时这里CFRunLoopServiceMachPort的timeout参数为0,这意味着,如果没有收到消息,那它就直接放弃而不会继续等待了,这也符合RunLoop的运行逻辑。
端口集接收:waitSet
而在进入休眠状态时,CFRunLoopServiceMachPort的port参数是waitSet,这个参数会传递到内部的mach_msg()函数的receive_name参数,这表明它是从这个端口集中接受消息的。那么waitSet包括哪些端口呢?
在__CFRunLoopRun函数中有:
...
dispatchPort = _dispatch_get_main_queue_port_4CF();
__CFPortSet waitSet = rlm -> _portSet;
CFPortSetInsert(dispatchPort, waitSet);
...
那么rlm中的_portSet呢?在__CFRunLoopFindMode函数中
···
mach_port_t queuePort = _dispatch_runloop_root_queue_get_port_4CF(rlm->_queue);
__CFPortSetInsert(queuePort, rlm->_portSet);
__CFPortSetInsert(rlm->_timerPort, rlm->_portSet);
__CFPortSetInsert(rl->_wakeUpPort, rlm->_portSet);
···
在CFRunLoopAddSource方法中:
CFPortSetInsert(src_port, rlm->_portSet);// source1
至此,我们可以确定Apple关于RunLoop文档(https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-SW6)中,将RunLoop唤醒的几种事件了:
1、基于Port的source事件
2、timer到时间了
3、runloop要超时了
4、runloop被显式唤醒了
那么RunLoop又是如何判断是由那个Port接受到的消息呢?在CFRunLoopServiceMachPort函数中,当成功接受到消息后,会将livePort赋值为**msg->msgh_local_port
,而msgh_local_port
**就是端口集中接受消息的那个端口,而后RunLoop判断livePort的端口,从而决定处理不同的唤醒事件。
__CFRunLoopRun() {
···
if (MACH_PORT_NULL == livePort) {
···
} else if (livePort == rl->_wakeUpPort) {
···
} else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
···
} else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort){
···
} else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort){
···
} else if (livePort == dispatchPort) {
···
} else {
}
···
}
端口接收的是什么?
上述的描述比较明确的是一个端口接收到到消息是会放在了端口的消息队列中,那么这个消息队列是如何实现的呢?从安卓中的looper中可以看到它们使用了链表来管理这种消息队列的,其实在iOS的xnu(x is not Unix)内核底层也是通过双向链表的方式来关系的消息的,在**mach_msg_overwrite_trap
** 方法中接收消息的时候,最后都会将消息存储到**ipc_msg
**中,而这个ipc_msg
就是一个双向链表的节点, 源码(https://opensource.apple.com/source/xnu/xnu-4570.61.1/osfmk/ipc/ipc_kmsg.h.auto.html)如下:
struct ipc_kmsg {
mach_msg_size_t ikm_size;
struct ipc_kmsg *ikm_next; /* next message on port/discard queue */
struct ipc_kmsg *ikm_prev; /* prev message on port/discard queue */
mach_msg_header_t *ikm_header;
ipc_port_t ikm_prealloc; /* port we were preallocated from */
ipc_port_t ikm_voucher; /* voucher port carried */
mach_msg_priority_t ikm_qos; /* qos of this kmsg */
mach_msg_priority_t ikm_qos_override; /* qos override on this kmsg */
struct ipc_importance_elem *ikm_importance; /* inherited from */
queue_chain_t ikm_inheritance; /* inherited from link */
sync_qos_count_t sync_qos[THREAD_QOS_LAST]; /* sync qos counters for ikm_prealloc port */
sync_qos_count_t special_port_qos; /* special port qos for ikm_prealloc port */
#if MACH_FLIPC
struct mach_node *ikm_node; /* Originating node - needed for ack */
#endif
};
参考
1、mach_ms
(https://web.mit.edu/darwin/src/modules/xnu/osfmk/man/mach_msg.html)
2、Mach Message Call
(https://www.gnu.org/software/hurd/gnumach-doc/Mach-Message-Call.html)
3、深入理RunLoop
(https://blog.ibireme.com/2015/05/18/runloop/)
4、Apple文档《Threading Programming Guide》
(https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-SW6)
5、《Mac OS X and iOS Internals To the Apples Core》一书
6、opensource.apple.com开源代码
(https://opensource.apple.com/source/xnu/xnu1228/osfmk/ipc/mach_msg.c.auto.html)