跟多数开发者一样,我也曾经迷惑于runloop,最初只了解可以通过runloop一些监听事件的通知来做一些事情,优化性能。关于runloop源码的基础知识,本文不做论述,可以参考众神的文章:
ibireme:《深入理解RunLoop》
sunyawang:《RunLoop系列之源码分析》
xiaoxiaobukuang:《RunLoop》
本文将对以上科普性文章之外的一些源码内容进行解读,便于日后自己和大家查阅。
p.s. 本文中代码部分均有删减,仅供参考。
runloop超时
在runloop源码的核心方法__CFRunLoopRun中,在进入核心的 do while循环之前,先使用 dispatch启动了一个计时器:
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
uint64_t startTSR = mach_absolute_time();
......
......
dispatch_source_t timeout_timer = NULL;
struct __timeout_context *timeout_context = (struct __timeout_context *)malloc(sizeof(*timeout_context));
if (seconds <= 0.0) { // instant timeout
seconds = 0.0;
timeout_context->termTSR = 0ULL;
} else if (seconds <= TIMER_INTERVAL_LIMIT) {
dispatch_queue_t queue = pthread_main_np() ? __CFDispatchQueueGetGenericMatchingMain() : __CFDispatchQueueGetGenericBackground();
timeout_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_retain(timeout_timer);
timeout_context->ds = timeout_timer;
timeout_context->rl = (CFRunLoopRef)CFRetain(rl);
timeout_context->termTSR = startTSR + __CFTimeIntervalToTSR(seconds);
dispatch_set_context(timeout_timer, timeout_context); // source gets ownership of context
dispatch_source_set_event_handler_f(timeout_timer, __CFRunLoopTimeout);
dispatch_source_set_cancel_handler_f(timeout_timer, __CFRunLoopTimeoutCancel);
uint64_t ns_at = (uint64_t)((__CFTSRToTimeInterval(startTSR) + seconds) * 1000000000ULL);
dispatch_source_set_timer(timeout_timer, dispatch_time(1, ns_at), DISPATCH_TIME_FOREVER, 1000ULL);
dispatch_resume(timeout_timer);
} else { // infinite timeout
seconds = 9999999999.0;
timeout_context->termTSR = UINT64_MAX;
}
计时器的超时时间从哪里来?
这里的计时器用来做什么?
带着这两个问题我们继续看源码:
首先可以看出超时时间是根据 __CFRunLoopRun 的入参seconds计算的,而__CFRunLoopRun入参从哪里来,顺着源码可以找到 CFRunLoopRunSpecific, 再向上可以找到两处调用:
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
CHECK_FOR_FORK();
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
同时,我们可以在头文件中看到这两个方法:
CF_EXPORT void CFRunLoopRun(void);
CF_EXPORT SInt32 CFRunLoopRunInMode(CFStringRef mode, CFTimeInterval seconds, Boolean returnAfterSourceHandled);
从而可以知道,runloop开放了两种启动runloop的方式,
- 一种是默认启动方式,此时不配置seconds,即未设置超时;
- 一种是自定义启动方式,可以配置seconds的超时时间,以及其他mode等参数;
第一个问题解答完,我们再看第二个问题。
我们知道dispatch 计时器达到超时时间,会调用dispatch_source_set_event_handler_f 中配置的回调函数(也可以用dispatch_source_set_event_handler配置block),这里的回调函数是__CFRunLoopTimeout,源码如下:
static void __CFRunLoopTimeout(void *arg) {
struct __timeout_context *context = (struct __timeout_context *)arg;
context->termTSR = 0ULL;
CFRUNLOOP_WAKEUP_FOR_TIMEOUT();// 没啥X用
CFRunLoopWakeUp(context->rl);
// The interval is DISPATCH_TIME_FOREVER, so this won't fire again
}
void CFRunLoopWakeUp(CFRunLoopRef rl) {
......
ret = __CFSendTrivialMachMessage(rl->_wakeUpPort, 0, MACH_SEND_TIMEOUT, 0);
if (ret != MACH_MSG_SUCCESS && ret != MACH_SEND_TIMED_OUT) CRASH("*** Unable to send message to wake up port. (%d) ***", ret);
......
}
static uint32_t __CFSendTrivialMachMessage(mach_port_t port, uint32_t msg_id, CFOptionFlags options, uint32_t timeout) {
kern_return_t result;
mach_msg_header_t header;
......
result = mach_msg(&header, MACH_SEND_MSG|options, header.msgh_size, 0, MACH_PORT_NULL, timeout, MACH_PORT_NULL);
if (result == MACH_SEND_TIMED_OUT) mach_msg_destroy(&header);
return result;
}
从上面源码可以看出超时到达时,主要做的就是通过 __CFSendTrivialMachMessage再调用mach_msg发送消息,mach_msg参数已经配置了“发送模式”、“超时时间”、唤醒的端口为“rl->_wakeUpPort”,基于文章《runloop你理解对了吗》, 我们可以知道,runloop在休眠时,接收到mach发来的消息,会判断port,决定作何判断和处理:
if (MACH_PORT_NULL == livePort)
{
CFRUNLOOP_WAKEUP_FOR_NOTHING();
}
else if (livePort == rl->_wakeUpPort)
{
CFRUNLOOP_WAKEUP_FOR_WAKEUP();
}
else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort)
{
// 处理timer
}
else if (livePort == dispatchPort)
{
......
// 处理主线程队列中事件
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
......
}
else
{
......
// 处理Source1
sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
......
}
也就是我们通过dispatch 定时器的可以将超时的消息通过mach ,唤醒runloop,然后执行else if (livePort == rl->_wakeUpPort)分支来处理超时(目前源码中CFRUNLOOP_WAKEUP_FOR_WAKEUP 只是一个空的宏定义,未做任何处理)。所以第二个问题答案就是这个计时器是用来在指定时间后唤醒runloop的。
mach_msg入参
mach_msg源码不知在何方,以下只是根据源码猜测,供参考。
我们从源码中摘录几个调用的地方:
mach_msg_header_t *msg = (mach_msg_header_t *)*buffer;
msg->msgh_bits = 0;
msg->msgh_local_port = port;
msg->msgh_remote_port = MACH_PORT_NULL;
msg->msgh_size = buffer_size;
msg->msgh_id = 0;
if (TIMEOUT_INFINITY == timeout) { CFRUNLOOP_SLEEP(); } else { CFRUNLOOP_POLL(); }
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);
mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL);
mach_msg_header_t header;
header.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0);
header.msgh_size = sizeof(mach_msg_header_t);
header.msgh_remote_port = port;
header.msgh_local_port = MACH_PORT_NULL;
header.msgh_id = msg_id;
result = mach_msg(&header, MACH_SEND_MSG|options, header.msgh_size, 0, MACH_PORT_NULL, timeout, MACH_PORT_NULL);
从上面三个调用可以猜测:
- 第一个参数就发送消息内容msg,msg的结构体里定义了消息的收发两方的port及其他内容;
- 第二个参数属于消息发送或接收的类型,通过宏定义已经定义好类型;
- 第三个参数应该是用于接收者申请额外存储空间暂存消息,便于自己处理,不对源消息的空间有耦合;
- 倒数第二个参数表示等待时间,如果是0表示发送或接收后立刻返回,如果是TIMEOUT_INFINITY,就阻塞地等待有消息,直到参数1对应port有消息才返回,继续执行后面的代码;
mach port
在runloop中多次提到port,比如基于port的source1就是就是休眠时候的唤醒源之一,比如休眠时监听消息 __CFRunLoopServiceMachPort也是通过port。
那么port是啥?Mach消息是在端口(Port)之间进行传递。一个端口只能有一个接收者,而可以同时有多个发送者。向一个端口发送消息,实际上是将消息放在一个消息队列中,直到消息能被接收者处理。
从源码中可见port类型是__CFPort /mach_port_name_t /mach_port_t,而mach_port_name_t就是无符号整数,也就是端口的索引值。源码中涉及到的port有几种类型:
// 这个port就对应NSTimer;
mach_port_t _timerPort;
// 这个port对应主线程
mach_port_name_t dispatchPort = MACH_PORT_NULL;
dispatchPort = _dispatch_get_main_queue_port_4CF();
// 这个port唤醒runloop
if (livePort == rl->_wakeUpPort)
还记得__CFRunLoopRun方法中休眠时监听的port集合吗?
// 第七步,进入循环开始不断的读取端口信息,如果端口有唤醒信息则唤醒当前runLoop
__CFPortSet waitSet = rlm->_portSet;
...
...
if (kCFUseCollectableAllocator)
{
memset(msg_buffer, 0, sizeof(msg_buffer));
}
// waitSet 为所有需要监听的port集合, TIMEOUT_INFINITY表示一直等待
msg = (mach_msg_header_t *)msg_buffer;
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
而这里的waitSet就是__CFPortSet ,也就是port的集合,那么__CFPortSet是什么类型呢?这个集合涉及哪些操作呢?
typedef mach_port_t __CFPortSet;
...
...
CF_INLINE kern_return_t __CFPortSetInsert(__CFPort port, __CFPortSet portSet) {
if (MACH_PORT_NULL == port) {
return -1;
}
return mach_port_insert_member(mach_task_self(), port, portSet);
}
也就说__CFPortSet的类型也是mach_port_t,即无符号整数。那么__CFPortSetInsert操作猜测应该就是按bit位来操作,不同bit位表示不同的port类型。__CFRunLoopServiceMachPort的参数入参__CFPort类型自然也可以传入waitSet,在其内部遍历各个bit位来监听各个port的消息。
另外,runloop休眠阶段的轮询的port集合是如何确定的呢?通过源码发现,正是__CFRunLoopFindMode方法中将各个port插入到waitSet中的:
static CFRunLoopModeRef __CFRunLoopFindMode(CFRunLoopRef rl, CFStringRef modeName, Boolean create)
{
...
...
kern_return_t ret = KERN_SUCCESS;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
rlm->_timerFired = false;
rlm->_queue = _dispatch_runloop_root_queue_create_4CF("Run Loop Mode Queue", 0);
mach_port_t queuePort = _dispatch_runloop_root_queue_get_port_4CF(rlm->_queue);
if (queuePort == MACH_PORT_NULL) CRASH("*** Unable to create run loop mode queue port. (%d) ***", -1);
rlm->_timerSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, rlm->_queue);
__block Boolean *timerFiredPointer = &(rlm->_timerFired);
dispatch_source_set_event_handler(rlm->_timerSource, ^{
*timerFiredPointer = true;
});
// Set timer to far out there. The unique leeway makes this timer easy to spot in debug output.
_dispatch_source_set_runloop_timer_4CF(rlm->_timerSource, DISPATCH_TIME_FOREVER, DISPATCH_TIME_FOREVER, 321);
dispatch_resume(rlm->_timerSource);
ret = __CFPortSetInsert(queuePort, rlm->_portSet);
if (KERN_SUCCESS != ret) CRASH("*** Unable to insert timer port into port set. (%d) ***", ret);
#endif
#if USE_MK_TIMER_TOO
rlm->_timerPort = mk_timer_create();
ret = __CFPortSetInsert(rlm->_timerPort, rlm->_portSet);
if (KERN_SUCCESS != ret) CRASH("*** Unable to insert timer port into port set. (%d) ***", ret);
#endif
ret = __CFPortSetInsert(rl->_wakeUpPort, rlm->_portSet);
if (KERN_SUCCESS != ret) CRASH("*** Unable to insert wake up port into port set. (%d) ***", ret);
CFSetAddValue(rl->_modes, rlm);
CFRelease(rlm);
__CFRunLoopModeLock(rlm); /* return mode locked */
return rlm;
}
从上面的三个__CFPortSetInsert可以发现分别插入了queuePort、_timerPort、_wakeUpPort;另外在CFRunLoopAddSource方法中还将source1的port插入其中:
......
__CFPort src_port = rls->_context.version1.getPort(rls->_context.version1.info);
if (CFPORT_NULL != src_port)
{
CFDictionarySetValue(rlm->_portToV1SourceMap, (const void *)(uintptr_t)src_port, rls);
__CFPortSetInsert(src_port, rlm->_portSet);
}
......
再加上__CFRunLoopRun方法中加入的dispatchPort ,至此,waitSet中已经包含了可以唤醒runloop的所有port。