从NSTimer的失效性谈起(一):关于NSTimer和NSRunLoop
一、NSTimer的失效性
在iOS中要设置一个定时器的通常做法是调用如下API:
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
这个API会创建一个NSTimer对象,将其添加到当前runloop的defaultMode中,然后返回该对象,如下所说:
Creates and returns a new NSTimer object and schedules it on the current run loop in the default mode.
由于NSTimer的过期事件需要由NSRunLoop来执行:
而在次线程上我们还需要额外显式地开启次线程所对应的runloop(比较麻烦),所以通常我们都会在主线程直接调用该API来设置并启动一个定时器。
一个NSRunLoop有几种mode,目前我们接触比较多的是NSDefaultRunLoopMode
和UITrackingRunLoopMode
,而NSRunLoopCommonModes
更多类似于一个标志,可以通过如下API将某一种mode归进commonModes:
CF_EXPORT void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef mode);
这样一来,我们就可以通过将定时器添加到NSRunLoopCommonModes
来一次性添加到多个对应的mode中。
通常,我们添加到defaultMode的定时器是可以正常工作的,不过当用户对scrollView进行滑动时定时器就失效了。这是因为此时mainRunLoop从NSDefaultRunLoopMode
退出,而进入到了UITrackingRunLoopMode
,所以添加到前者的定时器不会得到处理。
二、验证mainRunLoop的mode变化
关于NSRunLoop的进一步探究,可以参考:Run Loops,NSRunLoop Internals,CFRunLoop.c,深入理解RunLoop。
我们可以通过RunLoopObserver来观察一个runloop的mode切换:
CFRunLoopObserverRef observerRef = CFRunLoopObserverCreateWithHandler(NULL, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
if (activity == kCFRunLoopExit) {
NSLog(@"runloop mode %@ exit\n", [[NSRunLoop currentRunLoop] currentMode]);
} else if (activity == kCFRunLoopEntry) {
NSLog(@"runloop mode %@ entry\n", [[NSRunLoop currentRunLoop] currentMode]);
}
});
CFRunLoopAddObserver(CFRunLoopGetMain(), observerRef, kCFRunLoopCommonModes);
之前我猜想:对于mainRunLoop来说,一旦进入就不会退出了,除非应用程序结束。经过模拟,可以发现在滑动scrollView的时候会触发kCFRunLoopExit
和kCFRunLoopEntry
事件,断点得到的调用栈如下图:
通过上图调用栈来对应CFRunLoop.c源码可以找到相应方法调用:
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
... // 省略部分代码
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
... // 省略部分代码
return result;
}
可以看到在CFRunLoopRunSpecific
这一层对应的是kCFRunLoopEntry
和kCFRunLoopExit
的回调,而具体事件处理则落在了__CFRunLoopRun
内:
/* rl, rlm are locked on entrance and exit */
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;
... // 省略部分代码
int32_t retVal = 0;
do {
... // 省略部分代码
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
__CFRunLoopDoBlocks(rl, rlm);
... // 省略部分代码
Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
... // 省略部分代码
if (!poll) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
... // 省略部分代码
if (!poll) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
... // 省略部分代码
} while (0 == retVal);
return retVal;
}
可以看到在__CFRunLoopRun
中负责剩余几种状态的回调:kCFRunLoopBeforeTimers
,kCFRunLoopBeforeSources
,kCFRunLoopBeforeWaiting
和kCFRunLoopAfterWaiting
,对应着如下调用栈:
综上可以验证在开始滚动和停止滚动的时候,mainRunLoop确实会进行mode的切换。开始滚动时从NSDefaultRunLoopMode
切换到UITrackingRunLoopMode
(可能是为了让滚动更顺畅?),停止滚动时则反过来。
而不同mode维护着各自的inputSources和timerSources,所以在UITrackingRunLoopMode
下不会处理添加到NSDefaultRunLoopMode
的定时器。