【iOS】NSTimer Block 為什麼不會觸發迴圈引用?!

語言: CN / TW / HK

引子

NSTimer 是 iOS Foundation 框架中一種計時器,在經過一定的時間間隔後觸發,向目標物件傳送指定的訊息。

本文以標題為主線,探究 NSTimer 與 Runloop 之間的關係。

我們先看下面這段程式碼的執行: memory.png 場景: 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 的物件已經由底層 CFRetainCFRelease 方法正確 Retain 和 Release。不存在迴圈引用。