【iOS】NSTimer Block 為什麼不會觸發迴圈引用?!
引子
NSTimer 是 iOS Foundation 框架中一種計時器,在經過一定的時間間隔後觸發,向目標物件傳送指定的訊息。
本文以標題為主線,探究 NSTimer 與 Runloop 之間的關係。
我們先看下面這段程式碼的執行:
場景:
ViewController --Present-> SecondViewController。其中 Manager 例項會持有 Block。
SecondViewController 中有兩個點選按鈕,
test1
按鈕和 test2
按鈕分別排程方法 -didTapTest1:
與 -didTapTest2:
流程: 1. 點選 xx 按鈕; 2. 等待數秒,列印 block 中的內容; 3. 關閉 SecondViewController 控制器;
點選 test1 按鈕執行流程,列印:
2023-01-25 16:59:08.712191+0800 BlockMemoryLeaks[27573:1465591] Manager:
點選 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 處理的。
objectivec
typedef struct CF_BRIDGED_MUTABLE_TYPE(NSTimer) __CFRunLoopTimer * CFRunLoopTimerRef;
即便是 NSTimer 是中間層,如果底層 Timer 持有了 Block,還是存在迴圈引用。接著閱讀 Runloop 程式碼來找到標題問題的答案。
RunLoop 的 Mode
CFRunloop 與 CFRunloop Mode 的大致結構如下:
c++
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 下。
c++
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
方法
c++
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
函式排序:
c++
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
```c++
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:
c++
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 的資料結構,自始至終 Timer 都在被 Runloop 管理。NSTimer 物件僅是 Foundation 到 CoreFoundation 銜接的物件,通過 NSTimer 可以匹配操作到 CFRunLoopTimerRef 物件,而 CoreFoundation 的物件已經由底層 CFRetain
和 CFRelease
方法正確 Retain 和 Release。不存在迴圈引用。