从RunLoop源码探索NSTimer的实现原理

先运行一段测试代码

    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回调只会被触发一次。

image.png

总结:

( 前面其实已经贴在文章开头了)
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()使用

参考文献

1.苹果CF开源代码
2.NSTimer苹果官方文档
3.深入理解RunLoop
4.苹果libdispatch开源

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容