不知道大家有没有跟我一样的困惑?看过很多关于RunLoop的博客,本来觉得已经理解了runloop的运行原理,但是一写代码就发现运行结果和自己预想的不一致。我认为有两个原因:第一是没有去认真看runloop的源码,第二是iOS封装的NSRunLoop的三个接口,run:/runUntilDate:/runMode:beforeDate:隐藏了一些细节,迷惑了大家。
关于RunLoop的运行原理,有很多博客写的都很详细,我这里就不在重复了。我这篇文章从爬坑的角度来探究一下RunLoop. 大家都知道,NSRunLoop是对CFRunLoopRef的面向对象的封装。那么到底封装了什么呢?
- CFRunLoopRef源码探究
/* Reasons for CFRunLoopRunInMode() to Return */
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);
}
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
CHECK_FOR_FORK();
if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
__CFRunLoopLock(rl);
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
Boolean did = false;
if (currentMode) __CFRunLoopModeUnlock(currentMode);
__CFRunLoopUnlock(rl);
return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
}
volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);
CFRunLoopModeRef previousMode = rl->_currentMode;
rl->_currentMode = currentMode;
int32_t result = kCFRunLoopRunFinished;
if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
__CFRunLoopModeUnlock(currentMode);
__CFRunLoopPopPerRunData(rl, previousPerRun);
rl->_currentMode = previousMode;
__CFRunLoopUnlock(rl);
return result;
}
上面是CFRunLoop最常用过的两个函数,这两个函数最后都是调用CFRunLoopRunSpecific(),CFRunLoopRunSpecific()函数返回的是runloop结束的原因,这个原因就是下面的枚举,
enum {
kCFRunLoopRunFinished = 1, //runloop执行完成,通常是source都被移除了
kCFRunLoopRunStopped = 2, //runloop被人为停止了
kCFRunLoopRunTimedOut = 3, //runloop超时了
kCFRunLoopRunHandledSource = 4 //runloop处理完source handle返回了
};
可以就看到一次runloop结束会又四种原因,kCFRunLoopRunFinished通常是runloop被释放,或者source都被移除了;kCFRunLoopRunStopped是调用了CFRunLoopStop(); kCFRunLoopRunTimedOut是超过了设置的seconds,kCFRunLoopRunHandledSource取决于returnAfterSourceHandled参数,如果returnAfterSourceHandled=true,那么runloop在被source触发并执行完了source 回调的函数后就会结束(如果是timer source且是repeats的,那么只有timer被invalidate才会算结束),这时结束原因就是kCFRunLoopRunHandledSource。
理解了这些,我们再来看NSRunLoop就简单多了!!
- [NSRunLoop run] 是不是就是CFRunLoopRun()?
[NSRunLoop run] 这个函数大家都很熟悉了,意思就是让loop跑起来,说更详细点,就是在NSDefaultRunLoopMode下,超时时间是[NSDate distantFuture]的条件下运行runloop; 那么好像和CFRunLoopRun()是相同的操作, 我们来看看CFRunLoopRun()的源码
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
从源代码可以看到CFRunLoopRun()是在NSDefaultRunLoopMode下,超时时间是1.0e10, 而且returnAfterSourceHandled=false 的条件下运行runloop. 看似和[NSRunLoop run]是一样的。
但是在写代码过程中我们发现,用[NSRunLoop run]运行起来的runloop永远不会停止(除非我们移除了所有的timer source和input source),即使调用了CFRunLoopStop()也不会停止,而CFRunLoopRun()的runloop在调用了CFRunLoopStop()后会立马停止。根据observer看到runloop虽然不会退出,但是每次都会重新起一个runloop,说明returnAfterSourceHandled=true这说明[NSRunLoop run]并不是直接调用的CFRunLoopRun(),我们可以猜想,他的实现代码应该是这样的:
-(void) run(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, true);
CHECK_FOR_FORK();
} while (kCFRunLoopRunFinished != result);
}
也就是去掉了kCFRunLoopRunStopped != result这个条件。即使我们CFRunLoopStop()当前runloop,do-while循环不会退出,又会重新开始一个runloop。只有当result==kCFRunLoopRunFinished的时候才会退出,从源码中能看到,只有source为空了,或者runloop被dealloc的时候result才会等于kCFRunLoopRunFinished。那么可以得知,要想结束这种runloop只有移除所有的input source和timer source。这就和我们的实验结果是一致的了,那么[NSRunLoop run]就理解清楚了。同理[NSRunLoop runUntilDate]就是在run的基础上可以设置超时时间外,其他都是一样的。