先运行一段测试代码
CFAbsoluteTime refTime = CFAbsoluteTimeGetCurrent();
NSLog(@"start time 0.000000");
NSTimer *timer = [NSTimer timerWithTimeInterval:5.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"timer fire %f",CFAbsoluteTimeGetCurrent() - refTime);
}];
timer.tolerance = 0.5;
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"before busy %f", CFAbsoluteTimeGetCurrent() - refTime);
NSInteger j;
for (long i = 0; i< 1000000000; i++) {
j = i*3;
}
NSLog(@"after busy %f", CFAbsoluteTimeGetCurrent() - refTime);
});
代码中生成一个间隔5s,tolerance为0.5s的NSTimer,加入主线程的RunLoop,然后在4s的时候在主线程开始一个耗时的任务,耗时大约2秒多。按照我之前的理解4s+2s也就是6s多之后,已经超过了timer的5s+0.5s的最晚触发时刻,这个时刻点的timer应该不会触发了。然而,实际的运行结果如下:
2017-08-23 21:17:29.632 testTimer[10110:5869174] start time 0.000000
2017-08-23 21:17:34.026 testTimer[10110:5869174] before busy 4.393954
2017-08-23 21:17:36.474 testTimer[10110:5869174] after busy 6.841994
2017-08-23 21:17:36.474 testTimer[10110:5869174] timer fire 6.842287
2017-08-23 21:17:39.782 testTimer[10110:5869174] timer fire 10.150711
2017-08-23 21:17:45.125 testTimer[10110:5869174] timer fire 15.493015
2017-08-23 21:17:49.929 testTimer[10110:5869174] timer fire 20.297691
先不去管为什么dispatch_after 4s 在4.39s才执行,但是运算任务确实6.84s才结束,然而timer本应被阻塞的5s时候的那次触发并未被阻塞,而是直接在RunLoop不忙的时候就触发了。完全与我之前的理解相悖的运行结果。我们知道,CFRunLoopTimerRef与NSTimer是 toll-free bridged的,因此为了知道原因,去看RunLoop源码。
先贴结论,后面慢慢看代码:
1.对于重复的NSTimer,其多次触发的时刻不是一开始算好的,而是timer触发后计算的。但是计算时参考的是上次应当触发的时间_fireTSR,因此计算出的下次触发的时刻不会有误差。
2.设置了tolerance的NSTimer,对于iOS和MacOS系统,实质上会采用GCD timer的形式注册到内核中,GCD timer触发后,再由RunLoop处理其回调逻辑。对于没有设置tolerance的timer,则是用mk_timer的形式注册。
3.RunLoopMode中timer的排序是按照_fireTSR,也就是应当触发的时间排序的。而且,出于对于保证timer严格有序的考虑,保证时间考前的tolerance较大的timer不会影响后面的timer,系统在给GCD timer 传dummy字段时候会保证_fireTSR+dummy小于后面timer的最晚触发时间。
4.RunLoop层在timer触发后进行回调的时候,不会对tolerance进行验证。也就是说,因为RunLoop忙导致的timer触发时刻超出了tolerance的情况下,timer并不会取消,而不执行回调。
5.对于RunLoop忙时很长(或者timeInteval很短)的情况,会导致本该在这段时间内触发的几次回调中,只触发一次,也就是说,这种情况下还是会损失回调的次数。
6.对于RunLoop比较忙的情况,timer的回调时刻有可能不准,且不会受到tolerance的任何限制。tolerance的作用不是决定timer是否触发的标准,而是一个传递给系统的数值,帮助系统合理的规划GCD Timer的mach-port触发时机。设置了tolerance,一定会损失一定的时间精确度,但是可以显著的降低耗电。
一些延伸:
1.用NSTimer去计次可不可信?不太可信。对于timeInteval长的时候基本可信,但是,在timeInteval很短的时候,是有可能导致RunLoop忙时超过1~2个timeInteval,从而丢失某次回调。
2.用NSTimer获取的时间间隔准不准?不准,如果想获取可靠时间,请配合CFAbsoluteTimeGetCurrent()使用
代码阅读开始:
首先来看Timer的结构体的定义,在后面有可能用得到:
typedef struct CF_BRIDGED_MUTABLE_TYPE(NSTimer) __CFRunLoopTimer * CFRunLoopTimerRef;
struct __CFRunLoopTimer {
CFRuntimeBase _base;
uint16_t _bits;
pthread_mutex_t _lock;
CFRunLoopRef _runLoop;
CFMutableSetRef _rlModes;
CFAbsoluteTime _nextFireDate;
CFTimeInterval _interval; /* immutable */
CFTimeInterval _tolerance; /* mutable */
uint64_t _fireTSR; /* TSR units */
CFIndex _order; /* immutable */
CFRunLoopTimerCallBack _callout; /* immutable */
CFRunLoopTimerContext _context; /* immutable, except invalidation */
};
可以看出CFRunLoopTimerRef与NSTimer是 toll-free bridged的,上面的结构体就是一个Timer的结构体定义,其中_interval、_tolerace与NSTimer的timeInterval、tolerance是对应的,_runLoop标示了Timer所在的RunLoop,_rlModes是这个Timer所在的RunLoopModes,_fireTSR这是一个时间,他的单位是一种与内核有关的时间计数单位,可以和TimeInterval之间转化。_callout是这个Timer的回调函数指针。
添加Timer
下面从RunLoop中添加Timer的入口函数
void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef rlt, CFStringRef modeName)
开始看,为了简便,后面的代码中会略掉所有的锁操作和部分运行条件检查与异常处理
void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef rlt, CFStringRef modeName) {
CHECK_FOR_FORK();
if (modeName == kCFRunLoopCommonModes) {
//如果入参modeName是kCFRunLoopCommonModes,则需要加到runloop中所有的Common Mode里面
CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
if (NULL == rl->_commonModeItems) {
rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
}
CFSetAddValue(rl->_commonModeItems, rlt);
if (NULL != set) {
CFTypeRef context[2] = {rl, rlt};
/* add new item to all common-modes */
//通过对每个common mode调用__CFRunLoopAddItemToCommonModes函数,
//这个函数里面会再次调用CFRunLoopAddTimer函数,完成针对单个mode的添加timer动作
CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
CFRelease(set);
}
} else {
//针对单个mode添加timer
CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, true);
if (NULL != rlm) {
if (NULL == rlm->_timers) {
CFArrayCallBacks cb = kCFTypeArrayCallBacks;
cb.equal = NULL;
//初始化一个array,由于CallBacks的equal为NULL,因此数组内采用指针做相等性比较
rlm->_timers = CFArrayCreateMutable(kCFAllocatorSystemDefault, 0, &cb);
}
}
if (NULL != rlm && !CFSetContainsValue(rlt->_rlModes, rlm->_name)) {
if (NULL == rlt->_runLoop) {
//设置timer中的_runLoop字段
rlt->_runLoop = rl;
} else if (rl != rlt->_runLoop) {
return;
}
//设置timer中的_rlModes字段,添加这个mode
CFSetAddValue(rlt->_rlModes, rlm->_name);
//重新排列这个mode中的各个timer
__CFRepositionTimerInMode(rlm, rlt, false);
}
}
}
除了必要的锁和初始化操作,这段代码主要干了两个事情:1,对于添加到CommonModes的定时器,通过对每个commonMode递归调用本函数,逐次的添加到每个commonMode中。2.对于指定了mode的定时器,设置必要的字段,然后调用__CFRepositionTimerInMode
函数,重新排列这个mode中的所有timer触发时刻。下面看一看__CFRepositionTimerInMode
函数
static void __CFRepositionTimerInMode(CFRunLoopModeRef rlm, CFRunLoopTimerRef rlt, Boolean isInArray) {
if (!rlt) return;
CFMutableArrayRef timerArray = rlm->_timers;
if (!timerArray) return;
Boolean found = false;
// If we know in advance that the timer is not in the array (just being added now) then we can skip this search
if (isInArray) {
CFIndex idx = CFArrayGetFirstIndexOfValue(timerArray, CFRangeMake(0, CFArrayGetCount(timerArray)), rlt);
if (kCFNotFound != idx) {
CFRetain(rlt);
CFArrayRemoveValueAtIndex(timerArray, idx);
found = true;
}
}
if (!found && isInArray) return;
CFIndex newIdx = __CFRunLoopInsertionIndexInTimerArray(timerArray, rlt);
CFArrayInsertValueAtIndex(timerArray, newIdx, rlt);
__CFArmNextTimerInMode(rlm, rlt->_runLoop);
if (isInArray) CFRelease(rlt);
}
__CFRepositionTimerInMode
这个函数比较简单,就是先调用__CFRunLoopInsertionIndexInTimerArray
函数,这个函数就是根据timer的_fireTSR时间字段,利用二分查找的算法,将timer插入到已按照时间排列好的timerArray(rlm_timers)中,这个rlm_timers的array是按照fireTSR的升序排列的。然后再调用__CFArmNextTimerInMode
函数.
__CFArmNextTimerInMode
函数的作用是根据mode中的最前面的那个timer的触发时间,将其通过dispatch_source_set_runloop_timer或者mk_timer的方式注册。CFLite的源码中涉及部分宏定义,我通过符号断点的方式测试得知iOS系统同时支持dispatch_source_set_runloop_timer和mk_timer,因此下面的代码中隐藏了其他条件下的处理。
static void __CFArmNextTimerInMode(CFRunLoopModeRef rlm, CFRunLoopRef rl) {
uint64_t nextHardDeadline = UINT64_MAX;
uint64_t nextSoftDeadline = UINT64_MAX;
if (rlm->_timers) {
// Look at the list of timers. We will calculate two TSR values; the next soft and next hard deadline.
// The next soft deadline is the first time we can fire any timer. This is the fire date of the first timer in our sorted list of timers.
// The next hard deadline is the last time at which we can fire the timer before we've moved out of the allowable tolerance of the timers in our list.
for (CFIndex idx = 0, cnt = CFArrayGetCount(rlm->_timers); idx < cnt; idx++) {
CFRunLoopTimerRef t = (CFRunLoopTimerRef)CFArrayGetValueAtIndex(rlm->_timers , idx);
// discount timers currently firing
if (__CFRunLoopTimerIsFiring(t)) continue;
int32_t err = CHECKINT_NO_ERROR;
//SoftDeadline是理应触发的时间
uint64_t oneTimerSoftDeadline = t->_fireTSR;
//HardDeadline是理应触发的时间加上tolerance
uint64_t oneTimerHardDeadline = check_uint64_add(t->_fireTSR, __CFTimeIntervalToTSR(t->_tolerance), &err);
if (err != CHECKINT_NO_ERROR) oneTimerHardDeadline = UINT64_MAX;
// We can stop searching if the soft deadline for this timer exceeds the current hard deadline. Otherwise, later timers with lower tolerance could still have earlier hard deadlines.
//通过这几行代码对deadline进行修正,保证前边的长tolerance的timer不会影响后面的timer的触发
if (oneTimerSoftDeadline > nextHardDeadline) {
break;
}
if (oneTimerSoftDeadline < nextSoftDeadline) {
nextSoftDeadline = oneTimerSoftDeadline;
}
if (oneTimerHardDeadline < nextHardDeadline) {
nextHardDeadline = oneTimerHardDeadline;
}
}
if (nextSoftDeadline < UINT64_MAX && (nextHardDeadline != rlm->_timerHardDeadline || nextSoftDeadline != rlm->_timerSoftDeadline)) {
if (CFRUNLOOP_NEXT_TIMER_ARMED_ENABLED()) {
CFRUNLOOP_NEXT_TIMER_ARMED((unsigned long)(nextSoftDeadline - mach_absolute_time()));
}
// We're going to hand off the range of allowable timer fire date to dispatch and let it fire when appropriate for the system.
uint64_t leeway = __CFTSRToNanoseconds(nextHardDeadline - nextSoftDeadline);
dispatch_time_t deadline = __CFTSRToDispatchTime(nextSoftDeadline);
if (leeway > 0) {
// Only use the dispatch timer if we have any leeway
// <rdar://problem/14447675>
//对于有leeway的情况(有tolerance的情况),只采用_dispatch_source_set_runloop_timer_4CF的方法
// Cancel the mk timer
if (rlm->_mkTimerArmed && rlm->_timerPort) {
AbsoluteTime dummy;
mk_timer_cancel(rlm->_timerPort, &dummy);
rlm->_mkTimerArmed = false;
}
// Arm the dispatch timer
_dispatch_source_set_runloop_timer_4CF(rlm->_timerSource, deadline, DISPATCH_TIME_FOREVER, leeway);
rlm->_dispatchTimerArmed = true;
} else {
// 对于leeway为0的情况(无tolerance的情况),采用mk_timer的方式
// Cancel the dispatch timer
if (rlm->_dispatchTimerArmed) {
// Cancel the dispatch timer
_dispatch_source_set_runloop_timer_4CF(rlm->_timerSource, DISPATCH_TIME_FOREVER, DISPATCH_TIME_FOREVER, 888);
rlm->_dispatchTimerArmed = false;
}
// Arm the mk timer
if (rlm->_timerPort) {
mk_timer_arm(rlm->_timerPort, __CFUInt64ToAbsoluteTime(nextSoftDeadline));
rlm->_mkTimerArmed = true;
}
}
} else if (nextSoftDeadline == UINT64_MAX) {
// Disarm the timers - there is no timer scheduled
//移除timer
if (rlm->_mkTimerArmed && rlm->_timerPort) {
AbsoluteTime dummy;
mk_timer_cancel(rlm->_timerPort, &dummy);
rlm->_mkTimerArmed = false;
}
if (rlm->_dispatchTimerArmed) {
_dispatch_source_set_runloop_timer_4CF(rlm->_timerSource, DISPATCH_TIME_FOREVER, DISPATCH_TIME_FOREVER, 333);
rlm->_dispatchTimerArmed = false;
}
}
}
//设置RunLoopMode的_timerHardDeadline和_timerSoftDeadline字段
rlm->_timerHardDeadline = nextHardDeadline;
rlm->_timerSoftDeadline = nextSoftDeadline;
}
这个函数根据RunLoopMode中Timer的时间点和tolerance,计算出timer触发的SoftDeadline(应该触发的时间点)和HardDeadline(最晚的时间点),若二者相同(没有tolerance的情况下),则调用底层xnu的mk_timer注册一个mach-port事件(具体代码在https://opensource.apple.com/source/xnu/xnu-3789.51.2/osfmk/kern/mk_timer.c),若不同(有tolerance),则调用_dispatch_source_set_runloop_timer_4CF
函数,通过查阅libdispatch的源码可知这个函数就是dispatch_source_set_timer
,因此,对于有tolerance的NSTimer,其最终注册成了一个GCD Timer,只不过最终定时器fire的时候,会再通过RunLoop那一层,调用RunLoopTimer中保存的回调。
timer触发
下面来看timer触发的逻辑。
RunLoop在处理完各种事件后,调用__CFRunLoopServiceMachPort
函数等待接收mach port消息并进入休眠。值得注意的是,支持用GCD timer来实现runloop timer的情况下,runloop源码在这里又添加了一个循环,在GCD的callback设置了RunLoopMode的timerFired字段后跳出循环。这一段的目的暂时没有看太懂。
RunLoop被唤醒后,调用了__CFRunLoopDoTimers
函数,这个函数取出所有_fireTSR(应该触发的时间)小于当前系统时刻的RunLoopTimer,对其分别调用__CFRunLoopDoTimer
函数。__CFRunLoopDoTimer
函数主要干了两个事情,1:对timer中存的callout进行调用:__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(rlt->_callout, rlt, context_info);
。2:根据timer中的间隔interval信息,和当前这次fire的理论触发时刻_fireTSR,计算得到下一个应该触发的时刻_fireTSR,下一个应该触发的时刻_fireTSR必须晚于系统当前时刻,将_fireTSR设置到timer结构中,然后调用__CFRepositionTimerInMode
函数,重新排列这个mode中的所有timer触发时刻。
这一部分的注释代码如下:
// rl and rlm are locked on entry and exit
//该函数被调用时候limitTSR传入的是mach_absolute_time(),也就是当前时刻
static Boolean __CFRunLoopDoTimers(CFRunLoopRef rl, CFRunLoopModeRef rlm, uint64_t limitTSR) { /* DOES CALLOUT */
Boolean timerHandled = false;
CFMutableArrayRef timers = NULL;
for (CFIndex idx = 0, cnt = rlm->_timers ? CFArrayGetCount(rlm->_timers) : 0; idx < cnt; idx++) {
CFRunLoopTimerRef rlt = (CFRunLoopTimerRef)CFArrayGetValueAtIndex(rlm->_timers, idx);
if (__CFIsValid(rlt) && !__CFRunLoopTimerIsFiring(rlt)) {
if (rlt->_fireTSR <= limitTSR) {
//对于应当触发的时间_fireTSR早于当前时刻的timer,统统加入到timers数组中,等待挨个对它们调用__CFRunLoopDoTimer
if (!timers) timers = CFArrayCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeArrayCallBacks);
CFArrayAppendValue(timers, rlt);
}
}
}
for (CFIndex idx = 0, cnt = timers ? CFArrayGetCount(timers) : 0; idx < cnt; idx++) {
//对数组中的timer依次调用__CFRunLoopDoTimer进行处理
CFRunLoopTimerRef rlt = (CFRunLoopTimerRef)CFArrayGetValueAtIndex(timers, idx);
Boolean did = __CFRunLoopDoTimer(rl, rlm, rlt);
timerHandled = timerHandled || did;
}
if (timers) CFRelease(timers);
return timerHandled;
}
// mode and rl are locked on entry and exit
static Boolean __CFRunLoopDoTimer(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopTimerRef rlt) { /* DOES CALLOUT */
Boolean timerHandled = false;
uint64_t oldFireTSR = 0;
/* Fire a timer */
if (__CFIsValid(rlt) && rlt->_fireTSR <= mach_absolute_time() && !__CFRunLoopTimerIsFiring(rlt) && rlt->_runLoop == rl) {
void *context_info = NULL;
void (*context_release)(const void *) = NULL;
if (rlt->_context.retain) {
context_info = (void *)rlt->_context.retain(rlt->_context.info);
context_release = rlt->_context.release;
} else {
context_info = rlt->_context.info;
}
Boolean doInvalidate = (0.0 == rlt->_interval);
//设置正在fire的标志位
__CFRunLoopTimerSetFiring(rlt);
// Just in case the next timer has exactly the same deadlines as this one, we reset these values so that the arm next timer code can correctly find the next timer in the list and arm the underlying timer.
rlm->_timerSoftDeadline = UINT64_MAX;
rlm->_timerHardDeadline = UINT64_MAX;
oldFireTSR = rlt->_fireTSR;
__CFArmNextTimerInMode(rlm, rl);
//调用timer的回调
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(rlt->_callout, rlt, context_info);
if (doInvalidate) {
//根据timer是否设置了间隔inteval,决定是否需要移除timer
CFRunLoopTimerInvalidate(rlt); /* DOES CALLOUT */
}
if (context_release) {
context_release(context_info);
}
timerHandled = true;
__CFRunLoopTimerUnsetFiring(rlt);
}
if (__CFIsValid(rlt) && timerHandled) {
if (oldFireTSR < rlt->_fireTSR) {
//这种异常情况何时出现我没想明白
__CFArmNextTimerInMode(rlm, rl);
} else {
uint64_t nextFireTSR = 0LL;
uint64_t intervalTSR = 0LL;
//先是集中interval的异常情况
if (rlt->_interval <= 0.0) {
} else if (TIMER_INTERVAL_LIMIT < rlt->_interval) {
intervalTSR = __CFTimeIntervalToTSR(TIMER_INTERVAL_LIMIT);
} else {
intervalTSR = __CFTimeIntervalToTSR(rlt->_interval);
}
if (LLONG_MAX - intervalTSR <= oldFireTSR) {
nextFireTSR = LLONG_MAX;
} else {
//正常情况
uint64_t currentTSR = mach_absolute_time();
nextFireTSR = oldFireTSR;
//对于本次的应当触发时间,增加interval的倍数,直到增加后的时间大于当前时间
while (nextFireTSR <= currentTSR) {
nextFireTSR += intervalTSR;
}
}
CFRunLoopRef rlt_rl = rlt->_runLoop;
if (rlt_rl) {
CFIndex cnt = CFSetGetCount(rlt->_rlModes);
STACK_BUFFER_DECL(CFTypeRef, modes, cnt);
CFSetGetValues(rlt->_rlModes, (const void **)modes);
for (CFIndex idx = 0; idx < cnt; idx++) {
CFStringRef name = (CFStringRef)modes[idx];
modes[idx] = (CFTypeRef)__CFRunLoopFindMode(rlt_rl, name, false);
}
//将前面计算好的下次触发时间记录到timer结构体中
rlt->_fireTSR = nextFireTSR;
rlt->_nextFireDate = CFAbsoluteTimeGetCurrent() + __CFTimeIntervalUntilTSR(nextFireTSR);
for (CFIndex idx = 0; idx < cnt; idx++) {
CFRunLoopModeRef rlm = (CFRunLoopModeRef)modes[idx];
if (rlm) {
//对mode中的timer重新排序
__CFRepositionTimerInMode(rlm, rlt, true);
}
}
} else {
rlt->_fireTSR = nextFireTSR;
rlt->_nextFireDate = CFAbsoluteTimeGetCurrent() + __CFTimeIntervalUntilTSR(nextFireTSR);
}
}
}
return timerHandled;
}
可以看出。对于重复的NSTimer,其多次触发的时刻不是一开始算好的,而是timer触发后计算的。但是计算时参考的是上次应当触发的时间_fireTSR,因此计算出的下次触发的时刻不会有误差。这保证了timer不会出现误差叠加。
回到本文已开始提出的问题,为什么RunLoop忙的时候,RunLoopTimer的触发并没有被阻塞掉。显然,只要阻塞结束后__CFRunLoopDoTimers依然被调用,就不会影响timer的回调,这一段逻辑不会去校验timer的回调点是否超出了tolerance。换句话说,a:对于有tolerance的timer的情况,只要仍然能收到GCD timer的mach-port消息,这次timer的回调就会触发,只不过回调触发的时间变晚了不少。b:对于没有tolerance的timer,同样,只要能收到mk_timer发出的mach-port时间,就仍然会触发这次timer的回调。通过符号断点验证了上述想法。
考虑到前面说的,对于有tolerance的Timer,系统实际上注册了GCD-timer,因此猜想,GCD timer的回调时机也一定不会被runLoop阻塞掉,同时,其回调时机也不一定在dummy这个参数范围內。
一小段代码验证下:
CFAbsoluteTime refTime = CFAbsoluteTimeGetCurrent();
self.testTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_set_timer(self.testTimer, DISPATCH_TIME_NOW, 5.0 * NSEC_PER_SEC, 0.5 * NSEC_PER_SEC);
dispatch_source_set_event_handler(self.testTimer, ^{
NSLog(@"fire %f", CFAbsoluteTimeGetCurrent() - refTime);
});
dispatch_resume(self.testTimer);
NSLog(@"start");
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"before busy %f", CFAbsoluteTimeGetCurrent() - refTime);
long j;
for (long i = 0; i< 1000000000; i++) {
j = i*3;
}
NSLog(@"after busy %f", CFAbsoluteTimeGetCurrent() - refTime);
});
结果
2017-08-23 21:49:50.401 testTimer[10438:5896739] start
2017-08-23 21:49:50.402 testTimer[10438:5896739] fire 0.000974
2017-08-23 21:49:54.793 testTimer[10438:5896739] before busy 4.392407
2017-08-23 21:49:57.285 testTimer[10438:5896739] after busy 6.884340
2017-08-23 21:49:57.285 testTimer[10438:5896739] fire 6.884606
2017-08-23 21:50:00.646 testTimer[10438:5896739] fire 10.245657
2017-08-23 21:50:05.893 testTimer[10438:5896739] fire 15.492485
2017-08-23 21:50:10.792 testTimer[10438:5896739] fire 20.391427
从运行结果可见,GCD timer也没有被RunLoop阻塞掉,其回调触发时刻也确实超出了dummy的范围。验证了前边我们的猜想。
回到NSTimer,在能收到mach-port消息的情况下,Timer的回调就会触发,这是不是意味着NSTimer能确保不丢帧,不会缺少任何一次调用呢?答案也是否定的。上述机制保证了回调不会被阻塞掉,但是如果RunLoop忙的时间过长,以至于收到mach-port消息时,已经过了下次的理论触发点,则系统在__CFRunLoopDoTimer
逻辑中计算_fireTSR
的时候,会找到晚于当前时刻的那个理应触发点,作为_fireTSR
。就是下面这一小段代码:while (nextFireTSR <= currentTSR) {nextFireTSR += intervalTSR;}
。因此,如果RunLoop的忙的时间很长,长度达到了好多个timeInteval,则忙的这段时间内的timer回调只会被触发一次。
总结:
( 前面其实已经贴在文章开头了)
1.对于重复的NSTimer,其多次触发的时刻不是一开始算好的,而是timer触发后计算的。但是计算时参考的是上次应当触发的时间_fireTSR,因此计算出的下次触发的时刻不会有误差。
2.设置了tolerance的NSTimer,对于iOS和MacOS系统,实质上会采用GCD timer的形式注册到内核中,GCD timer触发后,再由RunLoop处理其回调逻辑。对于没有设置tolerance的timer,则是用mk_timer的形式注册。
3.RunLoopMode中timer的排序是按照_fireTSR,也就是应当触发的时间排序的。而且,出于对于保证timer严格有序的考虑,保证时间考前的tolerance较大的timer不会影响后面的timer,系统在给GCD timer 传dummy字段时候会保证_fireTSR+dummy小于后面timer的最晚触发时间。
4.RunLoop层在timer触发后进行回调的时候,不会对tolerance进行验证。也就是说,因为RunLoop忙导致的timer触发时刻超出了tolerance的情况下,timer并不会取消,而不执行回调。
5.对于RunLoop忙时很长(或者timeInteval很短)的情况,会导致本该在这段时间内触发的几次回调中,只触发一次,也就是说,这种情况下还是会损失回调的次数。
6.对于RunLoop比较忙的情况,timer的回调时刻有可能不准,且不会受到tolerance的任何限制。tolerance的作用不是决定timer是否触发的标准,而是一个传递给系统的数值,帮助系统合理的规划GCD Timer的mach-port触发时机。设置了tolerance,一定会损失一定的时间精确度,但是可以显著的降低耗电。
一些延伸:
1.用NSTimer去计次可不可信?不太可信。对于timeInteval长的时候基本可信,但是,在timeInteval很短的时候,是有可能导致RunLoop忙时超过1~2个timeInteval,从而丢失某次回调。
2.用NSTimer获取的时间间隔准不准?不准,如果想获取可靠时间,请配合CFAbsoluteTimeGetCurrent()使用