跟多数开发者一样,我也曾经迷惑于runloop,最初只了解可以通过runloop一些监听事件的通知来做一些事情,优化性能。关于runloop源码的基础知识,本文不做论述,可以参考众神的文章:
ibireme:《深入理解RunLoop》
sunyawang:《RunLoop系列之源码分析》
xiaoxiaobukuang:《RunLoop》
本文主要内容:
- 指出广泛传播runloop文章中错误
- 通过代码论证错误
- 通过demo论证错误
runloop解读文章中的错误
本人也看着众神的文章才对runloop有了比较深入了解,最近自己终于利用零零星星的时间把runloop源码也看了一遍,才发现好多人都误解了runloop!!就拿下面这张好多文章中都提及的图片和流程来说:
摘自《深入理解RunLoop》这是runloop运行流程图,但其实这个图里面有两个错误,请看下面标注图:
错误标注图-
第一个错误 “source0(port)” 应该是作者笔误,图中错误将source1 (基于port)写成source0;
-
第二个错误 "5. 如果有source1,跳到第9步" 从图和作者的代码注释中都能看出是理解有错误,这里也正是本文重点描述的内容
先说结论,再逐步验证:
这里其实判断的是 主线程是否有需要处理的事件,如果没有则调到第9步,这里跟source1没有关系!
所以应该改成“5. 如果当前是主线程的runloop,并且主线程有事儿,跳到第9步”
源码论证
我们直接上源码(版本CF-1151.16)分析一下,直接看这句话对应的代码(有精简):
if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime)
{
msg = (mach_msg_header_t *)msg_buffer;
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL))
{
goto handle_msg;
}
}
可以看出跳转到第9步(goto handle_msg)的逻辑是判断__CFRunLoopServiceMachPort函数的返回值是否为真,而这个if对应的就是上文描述“如果有source1”,那么这句话是这个意思吗? 起初我也是这么认为的,直到我看到了后面下一段第7步“休眠”的代码:
// 第七步,进入循环开始不断的读取端口信息,如果端口有唤醒信息则唤醒当前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);
这里面出现了上面的一样的__CFRunLoopServiceMachPort方法, 单拎出来比对下,
__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy)
比较后发现,参数中第一个参数和倒数第三个参数不同。我们通过__CFRunLoopServiceMachPort的源码来分析下,其中重点关注:
- livePort的赋值用于函数外部使用;
- __CFRunLoopServiceMachPort方法中mach_msg的参数MACH_RCV_MSG表示在接收消息;
- __CFRunLoopServiceMachPort参数timeout对于二者入参分别是0和TIMEOUT_INFINITY,分别表示查询到立刻返回和一直等待有消息再返回;
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)
{
Boolean originalBuffer = true;
kern_return_t ret = KERN_SUCCESS;
for (;;)
{ /* In that sleep of death what nightmares may come ... */
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);
// Take care of all voucher-related work right after mach_msg.
// If we don't release the previous voucher we're going to leak it.
voucher_mach_msg_revert(*voucherState);
// Someone will be responsible for calling voucher_mach_msg_revert. This call makes the received voucher the current one.
*voucherState = voucher_mach_msg_adopt(msg);
if (voucherCopy)
{
if (*voucherState != VOUCHER_MACH_MSG_STATE_UNCHANGED)
{
*voucherCopy = voucher_copy();
}
else
{
*voucherCopy = NULL;
}
}
CFRUNLOOP_WAKEUP(ret);
if (MACH_MSG_SUCCESS == ret)
{
*livePort = msg ? msg->msgh_local_port : MACH_PORT_NULL;
return true;
}
if (MACH_RCV_TIMED_OUT == ret)
{
if (!originalBuffer) free(msg);
*buffer = NULL;
*livePort = MACH_PORT_NULL;
return false;
}
if (MACH_RCV_TOO_LARGE != ret) break;
buffer_size = round_msg(msg->msgh_size + MAX_TRAILER_SIZE);
if (originalBuffer) *buffer = NULL;
originalBuffer = false;
*buffer = realloc(*buffer, buffer_size);
}
HALT;
return false;
}
从代码中我们可以大概看出,休眠时调用这个方法的作用就是监听判断waitSet中所有port,如果这些port中有一个出现消息,就唤醒了跳出休眠,并且将唤醒的port赋值给livePort。对于上面的mach_msg,我们在程序运行时打断点一定经常遇到,如下图,当runloop处于休眠时,就是下面的状态,也就是上面代码中mach_msg的timeout入参为TIMEOUT_INFINITY时阻塞式等待的情况:
阻塞等待消息堆栈下面的代码也验证了livePort用来判断是哪种激励将休眠唤醒,通过livePort来判断是进行哪种处理:
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;
......
}
通过上面对__CFRunLoopServiceMachPort的源码分析:我们基本确定了,第5步对应的代码
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL))
{
goto handle_msg;
}
其实__CFRunLoopServiceMachPort在等的是dispatchPort这个端口的消息,而这个端口是什么呢? 我们顺着源码向前找:
mach_port_name_t dispatchPort = MACH_PORT_NULL;
Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ)));
if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name))
dispatchPort = _dispatch_get_main_queue_port_4CF();
我们重点看if判断中的 (CFRunLoopGetMain() == rl),其中rl表示当前的runloop,查看CFRunLoopGetMain()源码可知返回的是主线程的runloop,所以这里判断就是当前runloop是否是主线程的runloop,这时我们再回到下面跳转到handle_msg那段代码:
if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime)
{
msg = (mach_msg_header_t *)msg_buffer;
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL))
{
goto handle_msg;
}
}
我们可以看到判断是否跳转之前先判断dispatchPort有没有消息,而再之前的条件必须满足MACH_PORT_NULL != dispatchPort,也就是前面必须对dispatchPort有所赋值,才会进行下面的判断和跳转逻辑。所以这里可以小总结一下重要的结论:
- 只有当前运行的runloop是主线程的runloop时,才会对dispatchPort赋值;
- 如果dispatchPort没有赋值,则不会进行是否“goto handle_msg”的逻辑判断;
- dispatchPort赋予的值是主线程队列对应的port;
- 如果当前运行的runloop不是主线程的runloop,那么原图中的第5步就不会存在,也就是多子线程图中不存在第5步;
综上,终于来到我们理论的总结:原图中第5步的应该由"5. 如果有source1,调到第9步"改成“5. 如果当前是主线程的runloop,并且主线程有事儿,跳到第9步”。 所以最终整体流程应该是:
1. 通知observer run loop被触发
2. 如果有timers事件的话,通知observer
3. 如果有source0要处理的话,通知observer
4. 触发所有的准备完毕的source0
5. 如果当前是主线程的runloop,并且主线程有事儿,跳到第9步
6. 通知Observer runloop将进入sleep状态
7. mach进入sleep和监听状态
8. 通知observer,runloop被woke up
9. 如果runloop是被唤醒,CFRUNLOOP_WAKEUP_FOR_WAKEUP
10. 如果用户定义的timer被触发,处理event并重启RunLoop
11. 如果dispatchPort,处理主线程
12. 如果一个source1被触发,__CFRunLoopDoSource1
13. 继续循环或通知observer runloop将要exited。
demo论证
最后我们再用demo来佐证一下,demo中我会首先则监听主线程的runloop,然后再在子线程监听子线程的runloop,打印监听的事件。
先看下demo中的主要代码:
// 添加主线程runloop监听者
[self addMainObserver];
// 添加子线程runloop监听者
[self addOtherObserver];
// 此处使用sleep是为了避免使用timer造成runloop的timer事件的干扰。
sleep(3);
dispatch_async(dispatch_get_main_queue(), ^{
CGFloat randomAlpha = (arc4random() % 100)*0.01;
[self.view setBackgroundColor:[UIColor colorWithWhite:0.5 alpha:randomAlpha]];
});
...
...
// 添加子线程runloop监听者
- (void)addOtherObserver
{
[NSThread detachNewThreadWithBlock:^{
_timer = [NSTimer scheduledTimerWithTimeInterval:3 repeats:NO block:^(NSTimer * _Nonnull timer)
{
NSLog(@"###cmm子线程###timer时间到");
}];
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"###cmm子线程###进入kCFRunLoopEntry");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"###cmm子线程###即将处理Timer事件");
break;
case kCFRunLoopBeforeSources:
NSLog(@"###cmm子线程###即将处理Source事件");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"###cmm子线程###即将休眠");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"###cmm子线程###被唤醒");
break;
case kCFRunLoopExit:
NSLog(@"###cmm子线程###退出RunLoop");
break;
default:
break;
}
});
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
CFRunLoopRun();
}];
}
// 添加主线程runloop监听者
- (void)addMainObserver
{
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"###cmm###进入kCFRunLoopEntry");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"###cmm###即将处理Timer事件");
break;
case kCFRunLoopBeforeSources:
NSLog(@"###cmm###即将处理Source事件");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"###cmm###即将休眠");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"###cmm###被唤醒");
break;
case kCFRunLoopExit:
NSLog(@"###cmm###退出RunLoop");
break;
default:
break;
}
});
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
_timer1 = [NSTimer scheduledTimerWithTimeInterval:3 repeats:NO block:^(NSTimer * _Nonnull timer) {
NSLog(@"###cmm###timer时间到");
}];
}
结合刚才整理的runloop的整体流程分析一下预期的打印结果应该是:
- 主线程中,如果有事儿需要处理, “即将处理timer事件”-->"即将处理source事件"-->下一个循环的"即将处理timer事件"-->"即将处理source事件",这里没有经过“即将休眠”,就是因为主线程有事儿,进入“goto handle_msg”,直接跳过休眠阶段。
- 子线程在主线程runloop处理事儿的时候,并没有打印结果变化,说明并没有触发这个goto条件。
demo跑起来~~~
我们在主线程的代码中打断点,查看堆栈和日志如下图:
可以发现,如我们所料:主线程的runloop在即将处理source事件后,直接跳到了 “__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__” ,也就是跳过了休眠,直接到了handle_msg对应的 else if (livePort == dispatchPort) 分支。另外我们可以在日志中发现此时子线程的runloop已经启动,并处于休眠状态。
然后我们注意下下图:
如图中箭头处,在我们程序跳过断点继续执行后,并没有子线程的相关打印,说明此时子线程的runloop并不会管主线程那部分代码。
网友评论