引子
NSTimer 是 iOS Foundation 框架中一种计时器,在经过必定的时间距离后触发,向方针目标发送指定的消息。
本文以标题为主线,探究 NSTimer 与 Runloop 之间的关系。
咱们先看下面这段代码的运行:
场景:
ViewController –Present-> SecondViewController。其间 Manager 实例会持有 Block。
SecondViewController 中有两个点击按钮,test1
按钮和 test2
按钮分别调度办法 -didTapTest1:
与 -didTapTest2:
流程:
- 点击 xx 按钮;
- 等候数秒,打印 block 中的内容;
- 关闭 SecondViewController 控制器;
点击 test1 按钮履行流程,打印:
2023-01-25 16:59:08.712191+0800 BlockMemoryLeaks[27573:1465591] Manager: <Manager: 0x6000002e40c0>
点击 test2 按钮履行流程,打印:
2023-01-25 17:01:47.305332+0800 BlockMemoryLeaks[27622:1467958] Timer: <__NSCFTimer: 0x6000000400c0> 2023-01-25 17:01:48.305246+0800 BlockMemoryLeaks[27622:1467958] Timer: <__NSCFTimer: 0x6000000400c0> 2023-01-25 17:01:49.141697+0800 BlockMemoryLeaks[27622:1467958] SecondViewController dealloc
test1 Manager 和预期相同,Manager -> Block,Block -> self,self -> Manager
,造成了循环引证。
而 test2 NSTimer 尽管被 self
持有,这个 Block 也捕获了 self,但这并没有触发循环引证。
NSTimer 与 Runloop
在苹果关于 NSTimer 文档中有描述,NSTimer 和 CFRunLoopTimerRef 是 toll-free bridged 的:
NSTimer is toll-free bridged with its Core Foundation counterpart, CFRunLoopTimerRef.
NSTimer 是中间桥接层,意味着其实定时器运作是交给 Runloop 处理的。
typedef struct CF_BRIDGED_MUTABLE_TYPE(NSTimer) __CFRunLoopTimer * CFRunLoopTimerRef;
即便是 NSTimer 是中间层,假如底层 Timer 持有了 Block,仍是存在循环引证。接着阅览 Runloop 代码来找到标题问题的答案。
RunLoop 的 Mode
CFRunloop 与 CFRunloop Mode 的大致结构如下:
struct __CFRunLoopMode {
CFStringRef _name;
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
// ...
};
struct __CFRunLoop {
CFRuntimeBase _base;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
// ...
};
一个 CFRunLoop 中包括若干个 CFRunLoopMode,CFRunLoopTimer 则被注册在 mode 下。
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 */
};
CFRunLoopTimer 包括一个时间长度和一个回调,标记了它地点的 runloop mode。
从增加 Timer 开始
先看 CFRunLoopAddTimer
办法
void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef rlt, CFStringRef modeName) {
CHECK_FOR_FORK();
if (__CFRunLoopIsDeallocating(rl)) return;
if (!__CFIsValid(rlt) || (NULL != rlt->_runLoop && rlt->_runLoop != rl)) return;
__CFRunLoopLock(rl);
if (modeName == kCFRunLoopCommonModes) {
/* Mode 是 CommonModes */
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};
/* 将 Timer 加入到一切 Common Mode 中 */
CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
CFRelease(set);
}
} else {
/* Mode 是指定 Mode */
CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, true);
if (NULL != rlm) {
if (NULL == rlm->_timers) {
CFArrayCallBacks cb = kCFTypeArrayCallBacks;
cb.equal = NULL;
rlm->_timers = CFArrayCreateMutable(kCFAllocatorSystemDefault, 0, &cb);
}
}
if (NULL != rlm && !CFSetContainsValue(rlt->_rlModes, rlm->_name)) {
__CFRunLoopTimerLock(rlt);
if (NULL == rlt->_runLoop) {
// 标记 Timer 对应的 Runloop
rlt->_runLoop = rl;
} else if (rl != rlt->_runLoop) {
__CFRunLoopTimerUnlock(rlt);
__CFRunLoopModeUnlock(rlm);
__CFRunLoopUnlock(rl);
return;
}
// 标记 Timer 对应的 Runloop Mode
CFSetAddValue(rlt->_rlModes, rlm->_name);
__CFRunLoopTimerUnlock(rlt);
__CFRunLoopTimerFireTSRLock();
/* 从头排序指定 Mode 中的各个 Timer */
__CFRepositionTimerInMode(rlm, rlt, false);
__CFRunLoopTimerFireTSRUnlock();
if (!_CFExecutableLinkedOnOrAfter(CFSystemVersionLion)) {
if (rl != CFRunLoopGetCurrent()) CFRunLoopWakeUp(rl);
}
}
if (NULL != rlm) {
__CFRunLoopModeUnlock(rlm);
}
}
__CFRunLoopUnlock(rl);
}
CFRunLoopAddTimer(_:_:_:)
将 Timer 增加到 Runloop 的指定 Mode 下。假如被增加的是 commonModes 则遍历一切 commonMode 调用 CFRunLoopAddTimer(_:_:_:)
办法。然后调用 __CFRepositionTimerInMode
函数排序:
static void __CFRepositionTimerInMode(CFRunLoopModeRef rlm, CFRunLoopTimerRef rlt, Boolean isInArray) {
if (!rlt) return;
// 拿到 Mode 下一切 timer
CFMutableArrayRef timerArray = rlm->_timers;
if (!timerArray) return;
Boolean found = false;
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
办法以触发时间 _fireTSR
从小到大排序 rlm->timers Mode 的 timers 数组。
排序完成后调用 __CFArmNextTimerInMode
从头注册最早应该被触发的 timer
static void __CFArmNextTimerInMode(CFRunLoopModeRef rlm, CFRunLoopRef rl) {
uint64_t nextHardDeadline = UINT64_MAX;
uint64_t nextSoftDeadline = UINT64_MAX;
if (rlm->_timers) {
// 修正 tolerance 值,保证时间最近的 timer + tolerance 大于其他 Timer 时,不影响其他 Timer 触发
for (CFIndex idx = 0, cnt = CFArrayGetCount(rlm->_timers); idx < cnt; idx++) {
CFRunLoopTimerRef t = (CFRunLoopTimerRef)CFArrayGetValueAtIndex(rlm->_timers , idx);
if (__CFRunLoopTimerIsFiring(t)) continue;
int32_t err = CHECKINT_NO_ERROR;
uint64_t oneTimerSoftDeadline = t->_fireTSR;
uint64_t oneTimerHardDeadline = check_uint64_add(t->_fireTSR, __CFTimeIntervalToTSR(t->_tolerance), &err);
if (err != CHECKINT_NO_ERROR) oneTimerHardDeadline = UINT64_MAX;
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()));
}
#if USE_DISPATCH_SOURCE_FOR_TIMERS
uint64_t leeway = __CFTSRToNanoseconds(nextHardDeadline - nextSoftDeadline);
dispatch_time_t deadline = __CFTSRToDispatchTime(nextSoftDeadline);
#if USE_MK_TIMER_TOO
if (leeway > 0) {
// 有 tolerance 选用 _dispatch_source_set_runloop_timer_4CF 办法注册定时器
if (rlm->_mkTimerArmed && rlm->_timerPort) {
AbsoluteTime dummy;
mk_timer_cancel(rlm->_timerPort, &dummy);
rlm->_mkTimerArmed = false;
}
_dispatch_source_set_runloop_timer_4CF(rlm->_timerSource, deadline, DISPATCH_TIME_FOREVER, leeway);
rlm->_dispatchTimerArmed = true;
} else {
// 没有 tolerance 选用 RunloopMode 的 mk_timer 办法注册 mach-port 事情
if (rlm->_dispatchTimerArmed) {
_dispatch_source_set_runloop_timer_4CF(rlm->_timerSource, DISPATCH_TIME_FOREVER, DISPATCH_TIME_FOREVER, 888);
rlm->_dispatchTimerArmed = false;
}
if (rlm->_timerPort) {
mk_timer_arm(rlm->_timerPort, __CFUInt64ToAbsoluteTime(nextSoftDeadline));
rlm->_mkTimerArmed = true;
}
}
#else
_dispatch_source_set_runloop_timer_4CF(rlm->_timerSource, deadline, DISPATCH_TIME_FOREVER, leeway);
#endif
#else
if (rlm->_timerPort) {
mk_timer_arm(rlm->_timerPort, __CFUInt64ToAbsoluteTime(nextSoftDeadline));
}
#endif
} else if (nextSoftDeadline == UINT64_MAX) {
// 假如没有定时器安排,则免除定时器,将 _mkTimerArmed 值置为 false
if (rlm->_mkTimerArmed && rlm->_timerPort) {
AbsoluteTime dummy;
mk_timer_cancel(rlm->_timerPort, &dummy);
rlm->_mkTimerArmed = false;
}
#if USE_DISPATCH_SOURCE_FOR_TIMERS
if (rlm->_dispatchTimerArmed) {
_dispatch_source_set_runloop_timer_4CF(rlm->_timerSource, DISPATCH_TIME_FOREVER, DISPATCH_TIME_FOREVER, 333);
rlm->_dispatchTimerArmed = false;
}
#endif
}
}
// 设置 Runloop Mode 的两个截止时间字段 Deadline
rlm->_timerHardDeadline = nextHardDeadline;
rlm->_timerSoftDeadline = nextSoftDeadline;
}
这个办法简略来说便是注册下一个需求触发的 Timer 事情到 Runloop 中。其间有配置 tolerance 的 Timer 会被注册为一个 GCD Timer,未配置 tolerance 的 Timer 截止时间会被注册一个 mach-port 事情,设置到 Runloop Mode 中。
触发 TimerCallBack
等到 Runloop 被 timer mach-port 唤醒时,调用 __CFRunLoopDoTimers
函数,挑选 _fireTSR
早于当时时间的 timers:
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 早于当时时间的 timers
if (!timers) timers = CFArrayCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeArrayCallBacks);
CFArrayAppendValue(timers, rlt);
}
}
}
for (CFIndex idx = 0, cnt = timers ? CFArrayGetCount(timers) : 0; idx < cnt; idx++) {
CFRunLoopTimerRef rlt = (CFRunLoopTimerRef)CFArrayGetValueAtIndex(timers, idx);
// 挑选后的 timer 顺次调用 __CFRunLoopDoTimer
Boolean did = __CFRunLoopDoTimer(rl, rlm, rlt);
timerHandled = timerHandled || did;
}
if (timers) CFRelease(timers);
return timerHandled;
}
接下来 timers 顺次调用 __CFRunLoopDoTimer
,这个办法中会调用 timer 的任务 rlt->_callout
,从头排序 timer,注册下一个 timerPort。
定论
从增加 Timer 到触发 Timer,剖析了 Runloop Mode 的数据结构,自始至终 CFRunLoopTimer 都在被 Runloop 办理。NSTimer 目标仅是 Foundation 到 CoreFoundation 联接的目标,经过 NSTimer 能够匹配操作到 CFRunLoopTimerRef 目标,而 CoreFoundation 的目标已经由底层 CFRetain
和 CFRelease
办法正确 Retain 和 Release。不存在循环引证。