2021-08-15

preface

This article explores the core logic of RunLoop. The core logic of RunLoop is the response logic to Sources, Observers, and Timers. Before the introduction of CFRunLoopRun in Chapter 5, the issue of event trigger time is not considered for the time being, and only the event processing process after entering the response process is considered. CFRunLoopRun is actually an organization of the DoBlocks, DoObservers, DoSources, and DoTimers operations of the previous four chapters.

A, DoBlocks

RunLoop processes the blocks list sequentially. Consider that the RunLoop may mount new blocks during the execution of the list. The strategy for a RunLoop is to scoop out the blocks list (with a local variable pointer to the head of the old list) as it enters the blocks process, and then clear the blocks list pointer of the RunLoop. When dealing with old lists, if a block is executed, it is removed from the list. After blocks processing is complete, new blocks mounted during processing are spliced to the end of the old list.

The processing process can be referred to the following figure. Green is the old block node that does not need to be executed, red is the old block node that needs to be executed, and yellow is the newly mounted blocks node during blocks processing.

static Boolean __CFRunLoopDoBlocks(CFRunLoopRef rl, CFRunLoopModeRef rlm) { // Call with rl and rlm locked
    if(! rl->_blocks_head)return false;
    if(! rlm || ! rlm->_name)return false;
    Boolean did = false;
    struct _block_item *head = rl->_blocks_head;
    struct _block_item *tail = rl->_blocks_tail;
    // NOTE:Note that rL blocks are NULL after the list is pulled out, which means that in the next block processing,
    // Block lists can also continuously mount elements
    rl->_blocks_head = NULL;
    rl->_blocks_tail = NULL;
    CFSetRef commonModes = rl->_commonModes;
    CFStringRef curMode = rlm->_name;
    __CFRunLoopModeUnlock(rlm);
    __CFRunLoopUnlock(rl);
    struct _block_item *prev = NULL;
    struct _block_item *item = head;
    // Core operation 1: iterate over all nodes in the blocks linked list
    while (item) {
        struct _block_item *curr = item;
        item = item->_next;
	Boolean doit = false;
        // Core operation 2: blocks can be of Mode type or set type. The criteria are:
        // If the current Mode is equal to _mode of the block, or _mode is one of the CommonModes
	if (CFStringGetTypeID() == CFGetTypeID(curr->_mode)) {
	    doit = CFEqual(curr->_mode, curMode) || (CFEqual(curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(commonModes, curMode));
        } else {
	    doit = CFSetContainsValue((CFSetRef)curr->_mode, curMode) || (CFSetContainsValue((CFSetRef)curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(commonModes, curMode));
	}
	if(! doit) prev = curr;if (doit) {
            // Core operation 3: Execute block and remove node from list
	    if (prev) prev->_next = item;
	    if (curr == head) head = item;
	    if (curr == tail) tail = prev;
	    void (^block)(void) = curr->_block;
            CFRelease(curr->_mode);
            free(curr);
	    if (doit) {
                __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
	        did = true;
	    }
            Block_release(block); // do this before relocking to prevent deadlocks where some yahoo wants to run the run loop reentrantly from their dealloc
	}
    }
    __CFRunLoopLock(rl);
    __CFRunLoopModeLock(rlm);
    // NOTE: If there is still an unexecuted block after the old block list is completed, rL the new block list
    // Attach a new block to the end of the old blocks list and update the rL blocks
    // Change the linked list to old blocks. In other words blocks
    if (head) {
	tail->_next = rl->_blocks_head;
	rl->_blocks_head = head;
        if(! rl->_blocks_tail) rl->_blocks_tail = tail; }return did;
}

static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__() __attribute__((noinline));
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(void (^block)(void)) {
    if (block) {
        block();
    }
    asm __volatile__(""); // thwart tail-call optimization
}
Copy the code

Second, the DoObservers

Dealing with Observers is relatively simple: they are astute of all that they need to deal with under the target RunLoopMode, iterate over and trigger a callback for Observers. The criteria for determining the need for treatment are:

  • The state of the RunLoopactivityOn due;
  • Observer is currently valid (Observer is invalid after Invalidate);
  • The Observer is not currently being processed;

Note that when you need to remove an Observer, you need to iterate through the commonModeItems of the RunLoop to remove the items corresponding to the target Observer and through all modes of the RunLoop. To remove all references to the target Observer.

NOTE: The Observer reference is removed first and stored in the collectedObservers, so the Observer of the RunLoop is removed during the for iteration without raising an exception.

static void __CFRunLoopDoObservers() __attribute__((noinline));
static void __CFRunLoopDoObservers(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopActivity activity) {	/* DOES CALLOUT */
    CHECK_FOR_FORK();

    CFIndex cnt = rlm->_observers ? CFArrayGetCount(rlm->_observers) : 0;
    if (cnt < 1) return;

    // Core operation 1: Observers are astute of all that they need to deal with, and populate the buffer array collectedObservers
    STACK_BUFFER_DECL(CFRunLoopObserverRef, buffer, (cnt <= 1024)? cnt :1);
    CFRunLoopObserverRef *collectedObservers = (cnt <= 1024)? buffer : (CFRunLoopObserverRef *)malloc(cnt * sizeof(CFRunLoopObserverRef));
    CFIndex obs_cnt = 0;
    for (CFIndex idx = 0; idx < cnt; idx++) {
        CFRunLoopObserverRef rlo = (CFRunLoopObserverRef)CFArrayGetValueAtIndex(rlm->_observers, idx);
        if (0! = (rlo->_activities & activity) && __CFIsValid(rlo) && ! __CFRunLoopObserverIsFiring(rlo)) { collectedObservers[obs_cnt++] = (CFRunLoopObserverRef)CFRetain(rlo); } } __CFRunLoopModeUnlock(rlm); __CFRunLoopUnlock(rl);// Core operation 2: Iterate over the buffer array collectedObservers and trigger an Observer callback and Invalidate if necessary
    for (CFIndex idx = 0; idx < obs_cnt; idx++) {
        CFRunLoopObserverRef rlo = collectedObservers[idx];
        __CFRunLoopObserverLock(rlo);
        if(__CFIsValid(rlo)) { Boolean doInvalidate = ! __CFRunLoopObserverRepeats(rlo); __CFRunLoopObserverSetFiring(rlo); __CFRunLoopObserverUnlock(rlo); __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(rlo->_callout, rlo, activity, rlo->_context.info);if (doInvalidate) {
                CFRunLoopObserverInvalidate(rlo);
            }
            __CFRunLoopObserverUnsetFiring(rlo);
        } else {
            __CFRunLoopObserverUnlock(rlo);
        }
        CFRelease(rlo);
    }
    __CFRunLoopLock(rl);
    __CFRunLoopModeLock(rlm);

    if(collectedObservers ! = buffer)free(collectedObservers);
}

static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__() __attribute__((noinline));
static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(CFRunLoopObserverCallBack func, CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    if (func) {
        func(observer, activity, info);
    }
    asm __volatile__(""); // thwart tail-call optimization
}

void CFRunLoopObserverInvalidate(CFRunLoopObserverRef rlo) {    /* DOES CALLOUT */
    CHECK_FOR_FORK();
    __CFGenericValidateType(rlo, CFRunLoopObserverGetTypeID());
    __CFRunLoopObserverLock(rlo);
    CFRetain(rlo);
    if (__CFIsValid(rlo)) {
        CFRunLoopRef rl = rlo->_runLoop;
        void *info = rlo->_context.info;
        rlo->_context.info = NULL;
        __CFUnsetValid(rlo);
        if (NULL! = rl) { CFRetain(rl); __CFRunLoopObserverUnlock(rlo); __CFRunLoopLock(rl);// Core operation 1: Remove the target Observer from all modes
            CFArrayRef array = CFRunLoopCopyAllModes(rl);
            for (CFIndex idx = CFArrayGetCount(array); idx--;) {
                CFStringRef modeName = (CFStringRef)CFArrayGetValueAtIndex(array, idx);
                CFRunLoopRemoveObserver(rl, rlo, modeName);
            }
            // Core operation 2: Remove the commonModeItem corresponding to the target Observer in commonModes
            CFRunLoopRemoveObserver(rl, rlo, kCFRunLoopCommonModes);
            __CFRunLoopUnlock(rl);
            CFRelease(array);
            CFRelease(rl);
            __CFRunLoopObserverLock(rlo);
        }
        if (NULL! = rlo->_context.release) { rlo->_context.release(info);/* CALLOUT */
        }
    }
    __CFRunLoopObserverUnlock(rlo);
    CFRelease(rlo);
}

void CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef rlo, CFStringRef modeName) {
    CHECK_FOR_FORK();
    CFRunLoopModeRef rlm;
    __CFRunLoopLock(rl);
    if (modeName == kCFRunLoopCommonModes) {
        // Core operation 1: Remove the commonModeItem corresponding to the target Observer in commonModes
	if (NULL! = rl->_commonModeItems && CFSetContainsValue(rl->_commonModeItems, rlo)) { CFSetRefset = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
	    CFSetRemoveValue(rl->_commonModeItems, rlo);
	    if (NULL! =set) {
		CFTypeRef context[2] = {rl, rlo};
		CFSetApplyFunction(set, (__CFRunLoopRemoveItemFromCommonModes), (void *)context);
		CFRelease(set); }}}else {
        // Core action 2: Remove target observers in observers of mode
	rlm = __CFRunLoopFindMode(rl, modeName, false);
	if (NULL! = rlm &&NULL! = rlm->_observers) { CFRetain(rlo); CFIndex idx = CFArrayGetFirstIndexOfValue(rlm->_observers, CFRangeMake(0, CFArrayGetCount(rlm->_observers)), rlo);
            if(kCFNotFound ! = idx) { CFArrayRemoveValueAtIndex(rlm->_observers, idx); __CFRunLoopObserverCancel(rlo, rl, rlm); } CFRelease(rlo); }if (NULL! = rlm) { __CFRunLoopModeUnlock(rlm); } } __CFRunLoopUnlock(rl); }Copy the code

NOTE: Observer and Sources share a set of significant bit management methods, namely __CFIsValid, __CFSetValid, __CFUnsetValid methods above. The implementation of the above three methods is based on the _base member, and Observer and Sources have the same type of _base so it makes sense.

Third, DoSources

For Sources, Source0 and Source1 are logically independent.

3.1 DoSource0

The handler for Sources0 is as follows. The first step is to remove all references to Sources0 from RLM and save them to sources. CFSetApplyFunction is done. The output parameter sources may be NULL or CFRunLoopSource or CFSet (a collection of cFRunloopsources), Therefore, the sources type is obtained according to CFGetTypeID(sources) == CFRunLoopSourceGetTypeID(). Source = CFSet (Source = CFSet); elseSource = CFSet (Source = CFSet); stopAfterHandle (stopAfterHandle);

static Boolean __CFRunLoopDoSources0(CFRunLoopRef rl, CFRunLoopModeRef rlm, Boolean stopAfterHandle) __attribute__((noinline));
static Boolean __CFRunLoopDoSources0(CFRunLoopRef rl, CFRunLoopModeRef rlm, Boolean stopAfterHandle) {	/* DOES CALLOUT */
    CHECK_FOR_FORK();
    CFTypeRef sources = NULL;
    Boolean sourceHandled = false;

    // Core operation 1: Extract all references to Source0 from Mode and save them to the sources local variable
    if (NULL! = rlm->_sources0 &&0 < CFSetGetCount(rlm->_sources0)) {
	CFSetApplyFunction(rlm->_sources0, (__CFRunLoopCollectSources0), &sources);
    }
    if (NULL! = sources) { __CFRunLoopModeUnlock(rlm); __CFRunLoopUnlock(rl);if (CFGetTypeID(sources) == CFRunLoopSourceGetTypeID()) {
	    CFRunLoopSourceRef rls = (CFRunLoopSourceRef)sources;
	    __CFRunLoopSourceLock(rls);
            If source0 receives signal and source0 is valid, then the source0 callback (if-else) is triggered.
            if (__CFRunLoopSourceIsSignaled(rls)) {
	        __CFRunLoopSourceUnsetSignaled(rls);
	        if (__CFIsValid(rls)) {
	            __CFRunLoopSourceUnlock(rls);
                    __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(rls->_context.version0.perform, rls->_context.version0.info);
	            CHECK_FOR_FORK();
	            sourceHandled = true;
	        } else{ __CFRunLoopSourceUnlock(rls); }}else{ __CFRunLoopSourceUnlock(rls); }}else {
	    CFIndex cnt = CFArrayGetCount((CFArrayRef)sources);
	    CFArraySortValues((CFMutableArrayRef)sources, CFRangeMake(0, cnt), (__CFRunLoopSourceComparator), NULL);
	    for (CFIndex idx = 0; idx < cnt; idx++) {
		CFRunLoopSourceRef rls = (CFRunLoopSourceRef)CFArrayGetValueAtIndex((CFArrayRef)sources, idx);
		__CFRunLoopSourceLock(rls);
                
                // NOTE: The same code as "core operation 2" is omitted here.if (stopAfterHandle && sourceHandled) {
		    break;
		}
	    }
	}
	CFRelease(sources);
	__CFRunLoopLock(rl);
	__CFRunLoopModeLock(rlm);
    }
    return sourceHandled;
}

static void __CFRunLoopCollectSources0(const void *value, void *context) {
    CFRunLoopSourceRef rls = (CFRunLoopSourceRef)value;
    CFTypeRef *sources = (CFTypeRef *)context;
    if (0 == rls->_context.version0.version && __CFIsValid(rls) && __CFRunLoopSourceIsSignaled(rls)) {
	if (NULL == *sources) {
	    *sources = CFRetain(rls);
	} else if (CFGetTypeID(*sources) == CFRunLoopSourceGetTypeID()) {
	    CFTypeRef oldrls = *sources;
	    *sources = CFArrayCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeArrayCallBacks);
	    CFArrayAppendValue((CFMutableArrayRef)*sources, oldrls);
	    CFArrayAppendValue((CFMutableArrayRef)*sources, rls);
	    CFRelease(oldrls);
	} else{ CFArrayAppendValue((CFMutableArrayRef)*sources, rls); }}}static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__() __attribute__((noinline));
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(void (*perform)(void *), void *info) {
    if (perform) {
        perform(info);
    }
    asm __volatile__(""); // thwart tail-call optimization
}
Copy the code

3.2 DoSource1

The handler for Sources1 is as follows, omitting the precompilation judgment for the platform type. From the code, when DoSource1 is called, the specific CFRunLoopSource object needs to be specified (DoSource0 only needs to be specified to Mode), and as long as CFRunLoopSource is valid, the callback is triggered immediately. Signaled on the CFRunLoopSource, DoSource1 asserts that the released version must be 1 and have been signaled.

NOTE: DoSource1 just looks simple, with a lot of judgment and processing logic put into the RunLoopRun procedure.

static Boolean __CFRunLoopDoSource1() __attribute__((noinline));
static Boolean __CFRunLoopDoSource1(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopSourceRef rls, mach_msg_header_t *msg, CFIndex size, mach_msg_header_t **reply) {	/* DOES CALLOUT */
    CHECK_FOR_FORK();
    Boolean sourceHandled = false;

    CFRetain(rls);
    __CFRunLoopModeUnlock(rlm);
    __CFRunLoopUnlock(rl);
    __CFRunLoopSourceLock(rls);
    if (__CFIsValid(rls)) {
	__CFRunLoopSourceUnsetSignaled(rls);
	__CFRunLoopSourceUnlock(rls);
        __CFRunLoopDebugInfoForRunLoopSource(rls);
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(rls->_context.version1.perform, msg, size, reply, rls->_context.version1.info);
	CHECK_FOR_FORK();
	sourceHandled = true;
    } else {
        if (_LogCFRunLoop) { CFLog(kCFLogLevelDebug, CFSTR("%p (%s) __CFRunLoopDoSource1 rls %p is invalid"), CFRunLoopGetCurrent(), *_CFGetProgname(), rls); }
	__CFRunLoopSourceUnlock(rls);
    }
    CFRelease(rls);
    __CFRunLoopLock(rl);
    __CFRunLoopModeLock(rlm);
    return sourceHandled;
}

static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__() __attribute__((noinline));
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(
        void *(*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info),
        mach_msg_header_t *msg, CFIndex size, mach_msg_header_t **reply, void *info) {
    if (perform) {
        *reply = perform(msg, size, kCFAllocatorSystemDefault, info);
    }
    asm __volatile__(""); // thwart tail-call optimization
}
Copy the code

Four, DoTimers

The previous DoBlocks, DoObservers, and DoSources seem simple, but the DoTimers processing logic becomes a bit more hardcore. Prepare some basics before you get down to business.

4.1 the Timer to build

The Timer build logic is a little more dry. Where now1 is the current standard timestamp returned by CFAbsoluteTimeGetCurrent, now2 is the current CPU clock timestamp returned by mach_absolute_time. The fireData pass parameter is converted to a CPU clock timestamp using the formula now2 + __CFTimeIntervalToTSR(Firedate-now1). So xxxFireDate in the code is benchmarked to the standard timestamp, while xxxTSR is benchmarked to the CPU clock timestamp. In particular, CFRunLoopTimer’s _nextFireDate and _fireTSR represent the same point in time (when the timer will fire next), but with different benchmarks.

Typically, _fireTSR is larger than mach_Absolute_time () during Timer construction, meaning that the Timer is usually specified to fire at some point in the future. Therefore, it is basically possible to predict the triggering criteria of Timer as _fireTSR <= mach_Absolute_time ().

CFRunLoopTimerRef CFRunLoopTimerCreate(CFAllocatorRef allocator, CFAbsoluteTime fireDate, CFTimeInterval interval, CFOptionFlags flags, CFIndex order, CFRunLoopTimerCallBack callout, CFRunLoopTimerContext *context) {
    CHECK_FOR_FORK();
    if (isnan(interval)) {
        CRSetCrashLogMessage("NaN was used as an interval for a CFRunLoopTimer");
        HALT;
    }
    CFRunLoopTimerRef memory;
    UInt32 size;
    size = sizeof(struct __CFRunLoopTimer) - sizeof(CFRuntimeBase);
    memory = (CFRunLoopTimerRef)_CFRuntimeCreateInstance(allocator, CFRunLoopTimerGetTypeID(), size, NULL);
    if (NULL == memory) {
	return NULL;
    }
    __CFSetValid(memory);
    __CFRunLoopTimerUnsetFiring(memory);
    __CFRunLoopLockInit(&memory->_lock);
    memory->_runLoop = NULL;
    memory->_rlModes = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
    memory->_order = order;
    if (interval < 0.0) interval = 0.0;
    memory->_interval = interval;
    memory->_tolerance = 0.0;
    if (TIMER_DATE_LIMIT < fireDate) fireDate = TIMER_DATE_LIMIT;
    memory->_nextFireDate = fireDate;
    memory->_fireTSR = 0ULL;
    uint64_t now2 = mach_absolute_time();
    CFAbsoluteTime now1 = CFAbsoluteTimeGetCurrent();
    if (fireDate < now1) {
	memory->_fireTSR = now2;
    } else if (TIMER_INTERVAL_LIMIT < fireDate - now1) {
	memory->_fireTSR = now2 + __CFTimeIntervalToTSR(TIMER_INTERVAL_LIMIT);
    } else {
	memory->_fireTSR = now2 + __CFTimeIntervalToTSR(fireDate - now1);
    }
    memory->_callout = callout;
    if (NULL! = context) {if (context->retain) {
	    memory->_context.info = (void *)context->retain(context->info);
	} else {
	    memory->_context.info = context->info;
	}
	memory->_context.retain = context->retain;
	memory->_context.release = context->release;
	memory->_context.copyDescription = context->copyDescription;
    } else {
	memory->_context.info = 0;
	memory->_context.retain = 0;
	memory->_context.release = 0;
	memory->_context.copyDescription = 0;
    }
    return memory;
}
Copy the code

4.2 the Timer processing

The initial routine is the same. Firstly, retrieve Timers that need to be processed in the target RunLoopMode. The criteria for determining whether to process Timers are as follows:

  • The Timer is effective;
  • The Timer is not in the processing state;
  • The next triggering time of Timer is less than or equal to the input parameterlimitTSR(usually themach_absolute_time());

Then, iterating over Timers calls the __CFRunLoopDoTimer function to handle timed tasks.

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);
        
        // Core logic 1: Timer trigger criteria
        if(__CFIsValid(rlt) && ! __CFRunLoopTimerIsFiring(rlt)) {if (rlt->_fireTSR <= limitTSR) {
                if(! timers) timers = CFArrayCreateMutable(kCFAllocatorSystemDefault,0, &kCFTypeArrayCallBacks); CFArrayAppendValue(timers, rlt); }}}// Core logic 2: Iterate over the retrieved Timers that need to be triggered one by one
    for (CFIndex idx = 0, cnt = timers ? CFArrayGetCount(timers) : 0; idx < cnt; idx++) {
        CFRunLoopTimerRef rlt = (CFRunLoopTimerRef)CFArrayGetValueAtIndex(timers, idx);
        Boolean did = __CFRunLoopDoTimer(rl, rlm, rlt);
        timerHandled = timerHandled || did;
    }
    if (timers) CFRelease(timers);
    return timerHandled;
}
Copy the code

Let’s get down to business and see how the RunLoop triggers and manages the Timer at run time. The code is long, and the core operations are annotated in the code. There are two operations to note:

  • ARM mk_timer: On iOS RunLoop runs through the XNU kernelmk_timerImplement Timer event wake up (via a Mach port) and use it simultaneously on MAC platformsdispatch_timerandmk_timer;
  • Timer sort: when a Timer is in a RunLoop_fireDateWhenever a change occurs, you need to adjust the Mode_timersArray arrangement. Alignment is to ensure_timersAlways in accordance with the_fireDateAscending order.

Why is it necessary to ensure that the Timer is in ascending order? This is because Timers themselves have implicit priority – triggering time points. As you can see from __CFRunLoopDoTimers, Timers in Mode are simply iterated from beginning to end. Imagine if the Timer is disordered, when several Timers callback events are delayed for some reason (such as switching Mode, long operation blocking, etc.), The execution order of __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ is interrupted, whereas the _timers are kept in ascending order by _fireDate.

The following figure should help you understand why _timers need to be sorted in ascending order of _fireDate. Note that Timer2, Timer3, and Timer4 in red are delayed and stacked together because Timer1 callbacks are too time-consuming. In the figure, the execution order is correct because _timers are arranged in ascending order Timer2 >> Timer3 >> Timer4. If there is no ascending order, for example, _timers (4, 1, 2, 3), the cumulative callback order is Timer4 >> Timer2 >> Timer3, and the relative order between scheduled tasks is disturbed.

NOTE: This article does not cover the dispatch_timer implementation for Mac OS.

// for conservative arithmetic safety, such that (TIMER_DATE_LIMIT + TIMER_INTERVAL_LIMIT + kCFAbsoluteTimeIntervalSince1970) * 10^9 < 2^63
#define TIMER_DATE_LIMIT	4039289856.0
#define TIMER_INTERVAL_LIMIT	504911232.0

static Boolean __CFRunLoopDoTimer(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopTimerRef rlt) {	/* DOES CALLOUT */
    Boolean timerHandled = false;
    uint64_t oldFireTSR = 0;

    CFRetain(rlt);
    __CFRunLoopTimerLock(rlt);

    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;
        }
        // Core operation 1: If doInvalidate is set to true, invalidate is executed when the Timer is triggered
        // interval is 0, which means that doInvalidate is set to true if triggered only once
        Boolean doInvalidate = (0.0 == rlt->_interval);
        // NOTE:Key node (Timer trigger starts)
	__CFRunLoopTimerSetFiring(rlt);
        
        rlm->_timerSoftDeadline = UINT64_MAX;
        rlm->_timerHardDeadline = UINT64_MAX;
        __CFRunLoopTimerUnlock(rlt);
	__CFRunLoopTimerFireTSRLock();
	oldFireTSR = rlt->_fireTSR;
	__CFRunLoopTimerFireTSRUnlock();

        // Core operation 2: schedule the next Timer trigger event (via XNU kernel mk_timer via mach_port)
        __CFArmNextTimerInMode(rlm, rl);

	__CFRunLoopModeUnlock(rlm);
	__CFRunLoopUnlock(rl);
        // Core operation 3: trigger Timer's callback function
	__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(rlt->_callout, rlt, context_info);
	CHECK_FOR_FORK();
        // Core operation 4: Invalidate timer if necessary
        if (doInvalidate) {
            CFRunLoopTimerInvalidate(rlt);      /* DOES CALLOUT */
        }
        if (context_release) {
            context_release(context_info);
        }
	__CFRunLoopLock(rl);
	__CFRunLoopModeLock(rlm);
        __CFRunLoopTimerLock(rlt);
	timerHandled = true;
        // NOTE:Key node (End of Timer trigger)
	__CFRunLoopTimerUnsetFiring(rlt);
    }
    // Core operation 5: If the Timer is processed and the Timer is still not invalidated, the Timer has the interval
    // The next trigger time needs to be calculated
    if (__CFIsValid(rlt) && timerHandled) {
        if (oldFireTSR < rlt->_fireTSR) {
            // NOTE:In order to support in the Timer callback call CFRunLoopTimerSetNextFireDate manually modify
            // The trigger time of the Timer, if modified, the Timer is still _fireTSR minimum, because the Timer is in
            // is not Firing, so the ARM operation is triggered by an internal call to Reposition from the SetFireDate operation
            // Timer is not Firing and is being skipped (see NOTE above), so rearm here
            __CFRunLoopTimerUnlock(rlt);
            __CFArmNextTimerInMode(rlm, rl);
        } else {
	    uint64_t nextFireTSR = 0LL;
            uint64_t intervalTSR = 0LL;
            if (rlt->_interval <= 0.0) {}else if (TIMER_INTERVAL_LIMIT < rlt->_interval) {
        	intervalTSR = __CFTimeIntervalToTSR(TIMER_INTERVAL_LIMIT);
            } else {
                // Core operation 6: Convert the standard time-based interval to TSR
        	intervalTSR = __CFTimeIntervalToTSR(rlt->_interval);
            }
            if (LLONG_MAX - intervalTSR <= oldFireTSR) {
                nextFireTSR = LLONG_MAX;
            } else {
                if (intervalTSR == 0) {
                    CRSetCrashLogMessage("A CFRunLoopTimer with an interval of 0 is set to repeat");
                    HALT;
                }
                uint64_t currentTSR = mach_absolute_time();
                nextFireTSR = oldFireTSR;
                // Core operation 7: iterate to find the next trigger point (critical)
                while (nextFireTSR <= currentTSR) {
                    nextFireTSR += intervalTSR;
                }
            }
            CFRunLoopRef rlt_rl = rlt->_runLoop;
            if (rlt_rl) {
                CFRetain(rlt_rl);
		CFIndex cnt = CFSetGetCount(rlt->_rlModes);
		STACK_BUFFER_DECL(CFTypeRef, modes, cnt);
		CFSetGetValues(rlt->_rlModes, (const void **)modes);
                // Core operation 8: Pull out all Modes of the Timer bound RunLoop
		for (CFIndex idx = 0; idx < cnt; idx++) {
		    CFRetain(modes[idx]);
		}
		__CFRunLoopTimerUnlock(rlt);
		for (CFIndex idx = 0; idx < cnt; idx++) {
		    CFStringRef name = (CFStringRef)modes[idx];
		    modes[idx] = (CFTypeRef)__CFRunLoopFindMode(rlt_rl, name, false);
		    CFRelease(name);
		}
		__CFRunLoopTimerFireTSRLock();
                // Core operation 9: Assign the "next trigger time" calculated above to _fireTSR and update it
                // _nextFireDate (convert _fireTSR to a standard timestamp baseline)
		rlt->_fireTSR = nextFireTSR;
                rlt->_nextFireDate = CFAbsoluteTimeGetCurrent() + __CFTimeIntervalUntilTSR(nextFireTSR);
                Reorder all timers in all Modes of the Timer bound RunLoop.
                // We operate on all modes because a Timer can be bound to multiple modes in a single RunLoop
		for (CFIndex idx = 0; idx < cnt; idx++) {
		    CFRunLoopModeRef rlm = (CFRunLoopModeRef)modes[idx];
		    if (rlm) {
                        __CFRepositionTimerInMode(rlm, rlt, true);
		    }
		}
		__CFRunLoopTimerFireTSRUnlock();
		for (CFIndex idx = 0; idx < cnt; idx++) {
		    __CFRunLoopModeUnlock((CFRunLoopModeRef)modes[idx]);
		}
		CFRelease(rlt_rl);
	    } else {
                / / ELSE_BRANCH_OF: if (rlt_rl)
		__CFRunLoopTimerUnlock(rlt);
		__CFRunLoopTimerFireTSRLock();
                // NOTE:Does what core Operation 9 does aboverlt->_fireTSR = nextFireTSR; rlt->_nextFireDate = CFAbsoluteTimeGetCurrent() + __CFTimeIntervalUntilTSR(nextFireTSR); __CFRunLoopTimerFireTSRUnlock(); }}}else {
        //ELSE_BRANCH_OF: if (__CFIsValid(RLT) && RLT ->_fireTSR <= mach_absolute_time() &&! __CFRunLoopTimerIsFiring(rlt) && rlt->_runLoop == rl)
        __CFRunLoopTimerUnlock(rlt);
    }
    CFRelease(rlt);
    return timerHandled;
}
Copy the code

The code for the Timer sort is as follows. Sort using insert sort + binary search. Note that the binary lookup is not quite the same as the generic binary lookup. The FLSL function in the code follows the following rule, that is, the binary lookup split index (1 << FLSL (CNT)) * 2 is incremented by 2 to the NTH power, for example, the first split found in a 10-element array is 8, and so on.

flsl(0) = 0
flsl(1) = 1
flsl(2) = 2
flsl(4) = 3
flsl(8) = 4
flsl(16) = 5
flsl(32) = 6.Copy the code
static void __CFRepositionTimerInMode(CFRunLoopModeRef rlm, CFRunLoopTimerRef rlt, Boolean isInArray) __attribute__((noinline));
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);
}

static CFIndex __CFRunLoopInsertionIndexInTimerArray(CFArrayRef array, CFRunLoopTimerRef rlt) __attribute__((noinline));
static CFIndex __CFRunLoopInsertionIndexInTimerArray(CFArrayRef array, CFRunLoopTimerRef rlt) {
    CFIndex cnt = CFArrayGetCount(array);
    if (cnt <= 0) {
        return 0;
    }
    if (256 < cnt) {
        CFRunLoopTimerRef item = (CFRunLoopTimerRef)CFArrayGetValueAtIndex(array, cnt - 1);
        if (item->_fireTSR <= rlt->_fireTSR) {
            return cnt;
        }
        item = (CFRunLoopTimerRef)CFArrayGetValueAtIndex(array.0);
        if (rlt->_fireTSR < item->_fireTSR) {
            return 0;
        }
    }

    CFIndex add = (1 << flsl(cnt)) * 2;
    CFIndex idx = 0;
    Boolean lastTestLEQ;
    do {
        add = add / 2;
	lastTestLEQ = false;
        CFIndex testIdx = idx + add;
        if (testIdx < cnt) {
            CFRunLoopTimerRef item = (CFRunLoopTimerRef)CFArrayGetValueAtIndex(array, testIdx);
            if (item->_fireTSR <= rlt->_fireTSR) {
                idx = testIdx;
		lastTestLEQ = true; }}}while (0 < add);

    return lastTestLEQ ? idx + 1 : idx;
}
Copy the code

Finally, the ARM mk_timer code. Ignoring some platform-specific code, the following code requires understanding two concepts:

  • SoftDeadLine: A timing event for a timer that can be triggered when the softDeadLine expires.
  • HardDeadLine: A timing event for a timer. When the time exceeds its hardDeadLine, it is considered out of scope and cannot be triggered.

Without considering extreme cases, there is a fixed relationship between the two: hardDeadLine = softDeadLine + tolerance. HardDeadLine and Tolerance are largely unhelpful when the platform uses mk_timer to implement timeliness.

NOTE: Tolerance barely works on iOS; For Mac OS, because GCD Timer is used in conjunction with timing, it may play a guiding role in specifying leeway parameters. _hardDeadLine, like _nextFireDate, only serves as an internal memo and is not really useful.

static void __CFArmNextTimerInMode(CFRunLoopModeRef rlm, CFRunLoopRef rl) {    
    uint64_t nextHardDeadline = UINT64_MAX;
    uint64_t nextSoftDeadline = UINT64_MAX;

    if (rlm->_timers) {
            if (__CFRunLoopTimerIsFiring(t)) continue;
            
            int32_t err = CHECKINT_NO_ERROR;
            // Core code 1: calculates the softDeadLine and hardDeadLine of the timer currently traversed
            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;
            }
            
            // Core code 2: Get the minimum softDeadLine and hardDeadLine by iterating through all Mode timers
            // softDeadLine and hardDeadLine for timing events
            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(rlm->_timerPort) { mk_timer_arm(rlm->_timerPort, __CFUInt64ToAbsoluteTime(nextSoftDeadline)); }}else if (nextSoftDeadline == UINT64_MAX) {
            if (rlm->_mkTimerArmed && rlm->_timerPort) {
                AbsoluteTime dummy;
                mk_timer_cancel(rlm->_timerPort, &dummy);
                rlm->_mkTimerArmed = false; }... } } rlm->_timerHardDeadline = nextHardDeadline; rlm->_timerSoftDeadline = nextSoftDeadline; }Copy the code

Fifth, RunLoopRun

CFRunLoop exposes two interfaces for executing RunLoop:

  • CFRunLoopRun: Runs the current thread’s CFRunLoop objectin its default mode indefinitely;
  • CFRunLoopRunInMode: Runs the current thread’s CFRunLoop objectin a particular mode;

The main differences between the two have been highlighted in bold. CFRunLoopRun is to pull an infinite loop, and CFRunLoopRunInMode is to specify the RunLoop to run on a particular Mode. The source code for both is as follows, which confirms the above differences:

void CFRunLoopRun(void) {	/* DOES CALLOUT */
    int32_t result;
    do {
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0 e10.false);
        CHECK_FOR_FORK(a); }while(kCFRunLoopRunStopped ! = result && kCFRunLoopRunFinished ! = result); }SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK(a);return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
Copy the code

Both methods call CFRunLoopRunSpecific, which is called repeatedly and only once.

5.1 CFRunLoopRunSpecific

CFRunLoopRunSpecific is important because it is both a RunLoop entry and a RunLoop exit. In the following source code, looking at the core code highlighted, it is found that the Observer Entry event and Exit event are triggered here, and the code surrounded by both events is the __CFRunLoopRun call code. __CFRunLoopRun is the core logic of RunLoop.

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK(a);if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
    __CFRunLoopLock(rl);
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
    if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
        // The code is not in use.
	Boolean did = false;
	if (currentMode) __CFRunLoopModeUnlock(currentMode);
	__CFRunLoopUnlock(rl);
	return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
    }
    //NOTE:Back up old per run data, back up old Mode, switch to new Mode
    volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);
    CFRunLoopModeRef previousMode = rl->_currentMode;
    rl->_currentMode = currentMode;
    int32_t result = kCFRunLoopRunFinished;

    1. Trigger Entry event before running; 2. Run in the specified Mode; 3. Trigger Exit event after completion;
    if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

    //NOTE:Restore old per run Data, cut back to old Mode
    __CFRunLoopModeUnlock(currentMode);
    __CFRunLoopPopPerRunData(rl, previousPerRun);
    rl->_currentMode = previousMode;
    __CFRunLoopUnlock(rl);
    return result;
}
Copy the code

As you can see from the above code, RunLoop execution and function call behavior are very similar:

  • Before calling a function, record the context before calling;
  • Call a function;
  • After the function returns, the context before the call is restored.

RunLoop is:

  • Before switching Mode, record old per run data and Mode.
  • Switch Mode execution;
  • Mode After the execution is complete, restore the old per run data and Mode.

Let me give you a concrete example. The following figure shows the Mode switching process in the life cycle of a RunLoop with three modes (DefaultMode, ModeA, and ModeB) :

  • callCFRunLoopRun, RunLoop starts executing in DefaultMode, firing the first Extry event;
  • In a callback to a Source in DefaultModeCFRunLoopRunInModeThe second Entry event is triggered when RunLoop is executed on ModeA.
  • In a Timer callback of ModeA, calledCFRunLoopRunInModeA third Entry event is triggered when RunLoop is executed on ModeB;
  • After all tasks are processed on ModeB, the first Exit event is triggered and the processing resumes under ModeA.
  • After all tasks are processed on ModeA, the second Exit event is triggered and DefaultMode is returned to continue processing.
  • When all tasks are processed on DefaultMode, the RunLoop ends, triggering a third Exit event;

NOTE: As you can see from the above Mode switching process, the RunLoop thread’s call stack increases when Mode is frequently switched in callbacks of various monitor objects, just as function recursion risks stack overflow and mutex death.

5.2 Core of cores

Now comes the hardcore part. If you compile for the iOS platform, remove the other platform-related code from __CFRunLoopRun and get the complete source code as follows. The most important code is __CFRunLoopServiceMachPort, which is the key to running RunLoop into hibernation. This is achieved by setting the Mach Port to receive messages. When the Mach Port is set to receive messages, the thread suspends and goes to sleep, waiting for the Mach port to respond.

In fact, event firing (waking up RunLoop to respond to events) of all these monitor objects is implemented based on Mach port:

  • Source0: through RunLoop_wakeUpPortWake up the RunLoop;
  • Source1: The Mach port (Mode) bound by Source1_portToV1SourceMap) wake up RunLoop;
  • Timers: In Mode_timerPortWake up RunLoop at the right time;

In addition, RunLoop takes “special care” of operations performed on the GCD main queue. The dispatchPort implementation looks like this: After the main thread RunLoop has been woken up and processed the necessary monitor object events, (This call to __CFRunLoopServiceMachPort does not put RunLoop to sleep because the timeout argument is passed 0). If so, the livePort will be set to dispatchPort and jump directly to the livePort == dispatchPort logical branch of handle_MSG for immediate processing. The intent should be:

  • Ensure that the “GCD schedule main thread operation” in the monitor object callback of the main thread RunLoop can respond in time.
  • When the main thread has been awakened, it can timely respond to the “GCD schedule main thread operation” from other child threads.

In short, ensure that the response priority of “GCD schedule main thread operation”. When RunLoop needs to go to sleep, dispatchPort is also added to waitSet as an input parameter to __CFRunLoopServiceMachPort, which means that dispatchPort calls from the child thread asyn on main, It can also wake up the main thread RunLoop.

NOTE: An all-caps macro of the form CFRUNLOOP_XXX_XXX, such as CFRUNLOOP_WAKEUP_FOR_WAKEUP, does nothing but serve as a placeholder, and it’s not clear why it’s necessary.

Finally, notice the poll variable in the code. Poll is basically a polling multiplexing mechanism in the operating system world. KCFRunLoopBeforeWaiting and kCFRunLoopAfterWaiting are not triggered when the Loop is in poll mode, which means the Loop thread will not sleep. The timeout value of __CFRunLoopServiceMachPort is passed to 0, as is the case with dispatchPort. Therefore, the Loop in the RunLoop that handles Resource0 is special. After processing Resource0, the RunLoop will try to retrieve any Mach port events from all the Mode Mach ports. If yes, it is processed immediately; if no, it goes to the next Loop.

NOTE: Apple Documentation defines kCFRunLoopBeforeWaiting: Inside the event processing loop before the run loop sleeps, waiting for a source or timer to fire. This activity does not occur if CFRunLoopRunInMode is called with a timeout of 0 seconds. It also does not occur in a particular iteration of the event processing loop if a version 0 source fires.

static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
    uint64_t startTSR = mach_absolute_time(a);// NOTE:If RunLoop or the current Mode status is stopped, end RunLoop unconditionally. Note: Modified stopped
    // A state of true can only be achieved with the _CFRunLoopStopMode method, which uses an internal method for CF.
    if (__CFRunLoopIsStopped(rl)) {
        __CFRunLoopUnsetStopped(rl);
	return kCFRunLoopRunStopped;
    } else if (rlm->_stopped) {
	rlm->_stopped = false;
	return kCFRunLoopRunStopped;
    }
    
    // NOTE:A Mach port dedicated to responding to "GCD schedule main thread messages". The current primary thread is RunLoop and is in CommonModes
    // when run in any Mode of dispatchPort. The Mach port will not be built by the non-main thread, that is, to the non-main thread
    // RunLoop has: dispatchPort == MACH_PORT_NULL
    mach_port_name_t dispatchPort = MACH_PORT_NULL;
    Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL== previousMode) || (! HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY &&0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ)));
    if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) dispatchPort = _dispatch_get_main_queue_port_4CF(); .// NOTE:Logic to handle RunLoop timeouts. Use the Dispatch source Timer.
    dispatch_source_t timeout_timer = NULL;
    struct __timeout_context *timeout_context = (struct __timeout_context *)malloc(sizeof(*timeout_context));
    if (seconds <= 0.0) { // instant timeout
        seconds = 0.0;
        timeout_context->termTSR = 0ULL;
    } else if (seconds <= TIMER_INTERVAL_LIMIT) {
	dispatch_queue_t queue = pthread_main_np()? __CFDispatchQueueGetGenericMatchingMain() : __CFDispatchQueueGetGenericBackground(); timeout_timer =dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0.0, queue);
        dispatch_retain(timeout_timer);
	timeout_context->ds = timeout_timer;
	timeout_context->rl = (CFRunLoopRef)CFRetain(rl);
        // NOTE:The core of the RunLoop timeout logic. The RunLoop timeout point is equal to the current time + the passed seconds parameter
	timeout_context->termTSR = startTSR + __CFTimeIntervalToTSR(seconds);
	dispatch_set_context(timeout_timer, timeout_context); // source gets ownership of context
	dispatch_source_set_event_handler_f(timeout_timer, __CFRunLoopTimeout);
        dispatch_source_set_cancel_handler_f(timeout_timer, __CFRunLoopTimeoutCancel);
        uint64_t ns_at = (uint64_t)((__CFTSRToTimeInterval(startTSR) + seconds) * 1000000000ULL);
        dispatch_source_set_timer(timeout_timer, dispatch_time(1, ns_at), DISPATCH_TIME_FOREVER, 1000ULL);
        dispatch_resume(timeout_timer);
    } else { // infinite timeout
        seconds = 9999999999.0;
        timeout_context->termTSR = UINT64_MAX;
    }

    Boolean didDispatchPortLastTime = true;
    int32_t retVal = 0;
    do {
        voucher_mach_msg_state_t voucherState = VOUCHER_MACH_MSG_STATE_UNCHANGED;
        voucher_t voucherCopy = NULL;

        uint8_t msg_buffer[3 * 1024];

        mach_msg_header_t *msg = NULL;
        
        // NOTE:The Port to which the Loop is responding
        mach_port_tlivePort = MACH_PORT_NULL; .// NOTE:Port in the current Loop wait
	__CFPortSet waitSet = rlm->_portSet;

        __CFRunLoopUnsetIgnoreWakeUps(rl);

        // NOTE:Ensure that both notifications are triggered before processing Timers and Resource events, but do not guarantee that they are triggered
        // Handle events of the corresponding type
        if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);

        // Core operation 1: process the Block added by CFRunLoopPerformBlock
	__CFRunLoopDoBlocks(rl, rlm);

        // Core operation 2: handle Sources0. The reason for executing DoBlocks later is to handle the Sources0 callback containing
        // CFRunLoopPerformBlock
        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
        if (sourceHandledThisLoop) {
            __CFRunLoopDoBlocks(rl, rlm);
	}

        // NOTE:The Loop currently handles the Source0 event or specifies the RunLoop to handle only one event (second passes 0).
        Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);

        // NOTE:If dispatchPort is not empty (the current primary RunLoop runs in any Mode in CommonModes) and
        // The last loop responded to an event that was not dispatchPort. Then try to scoop up whether there is a need to deal with from dispatchPort
        // 'goto handle_msg' immediately handles the main thread message.
        if(MACH_PORT_NULL ! = dispatchPort && ! didDispatchPortLastTime) { msg = (mach_msg_header_t *)msg_buffer;
            if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
                gotohandle_msg; }... } didDispatchPortLastTime =false;

        // NOTE:Touch only when it is not poll Mode and an Observer of Mode is observing the kCFRunLoopBeforeWaiting event
        // Issue kCFRunLoopBeforeWaiting (same as Apple Documentation).
	if(! poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);// Core operation 3: mark RunLoop to sleep
	__CFRunLoopSetSleeping(rl);

        // NOTE:Add dispatchPort to waitSet
        __CFPortSetInsert(dispatchPort, waitSet);
        
	__CFRunLoopModeUnlock(rlm);
	__CFRunLoopUnlock(rl);

        CFAbsoluteTime sleepStart = poll ? 0.0 : CFAbsoluteTimeGetCurrent(a); .if (kCFUseCollectableAllocator) {
            memset(msg_buffer, 0.sizeof(msg_buffer));
        }
        msg = (mach_msg_header_t *)msg_buffer;
        // Core operation 4: RunLoop officially goes to sleep, waiting for the ports in waitSet to receive messages.
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0: TIMEOUT_INFINITY, &voucherState, &voucherCopy); . __CFRunLoopLock(rl); __CFRunLoopModeLock(rlm); rl->_sleepTime += (poll ?0.0 : (CFAbsoluteTimeGetCurrent() - sleepStart));

        // NOTE:Remove dispatchPort from waitSet
        __CFPortSetRemove(dispatchPort, waitSet);
        
        __CFRunLoopSetIgnoreWakeUps(rl);

        // Core logic 5: flag RunLoop end sleep has been awakened
	__CFRunLoopUnsetSleeping(rl);
        // NOTE:If it is not poll Mode and a Mode Observer is observing the kCFRunLoopAfterWaiting event
        // 发kCFRunLoopAfterWaiting
	if(! poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting); handle_msg:; __CFRunLoopSetIgnoreWakeUps(rl); .if (MACH_PORT_NULL == livePort) {
            CFRUNLOOP_WAKEUP_FOR_NOTHING(a); }else if (livePort == rl->_wakeUpPort) {
            // NOTE:Wakeup via the wakeup port (such as manually after sending the Source0 signal), where no processing is done
            // It is processed formally at doSources0 above
            CFRUNLOOP_WAKEUP_FOR_WAKEUP(a); . }...else if(rlm->_timerPort ! = MACH_PORT_NULL && livePort == rlm->_timerPort) {CFRUNLOOP_WAKEUP_FOR_TIMER(a);// Core logic 6: Handle Timers
            if(! __CFRunLoopDoTimers(rl, rlm,mach_absolute_time())) {
                // NOTE:This code was created to address a Windows platform bug and can be ignored__CFArmNextTimerInMode(rlm, rl); }}else if (livePort == dispatchPort) {
            CFRUNLOOP_WAKEUP_FOR_DISPATCH(a); __CFRunLoopModeUnlock(rlm); __CFRunLoopUnlock(rl); _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6.NULL); .// NOTE:Execute the "GCD schedule main thread message" callback
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0.NULL);
            __CFRunLoopLock(rl);
            __CFRunLoopModeLock(rlm);
            sourceHandledThisLoop = true;
            didDispatchPortLastTime = true;
        } else {
            CFRUNLOOP_WAKEUP_FOR_SOURCE(a);voucher_t previousVoucher = _CFSetTSD(__CFTSDKeyMachMessageHasVoucher, (void *)voucherCopy, os_release);

            // NOTE:The corresponding Source was found through the Mach port currently responding
            CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
            if (rls) {
		mach_msg_header_t *reply = NULL;
                // Core logic 7: Handle Sources1
		sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
                // NOTE:Process the Mach Port reply message if necessary
		if (NULL! = reply) { (void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL);
		    CFAllocatorDeallocate(kCFAllocatorSystemDefault, reply); }... } _CFSetTSD(__CFTSDKeyMachMessageHasVoucher, previousVoucher, os_release); }if(msg && msg ! = (mach_msg_header_t *)msg_buffer) free(msg);

        // Core logic 8: processing Blocks
	__CFRunLoopDoBlocks(rl, rlm);
        
        //NOTE:Sets the status of the RunLoop after the Loop completes. If Mode is empty, it is marked Finished
	if (sourceHandledThisLoop && stopAfterHandle) {
	    retVal = kCFRunLoopRunHandledSource;
        } else if (timeout_context->termTSR < mach_absolute_time()) {
            retVal = kCFRunLoopRunTimedOut;
	} else if (__CFRunLoopIsStopped(rl)) {
            __CFRunLoopUnsetStopped(rl);
	    retVal = kCFRunLoopRunStopped;
	} else if (rlm->_stopped) {
	    rlm->_stopped = false;
	    retVal = kCFRunLoopRunStopped;
	} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
	    retVal = kCFRunLoopRunFinished;
	}
        
        voucher_mach_msg_revert(voucherState);
        os_release(voucherCopy);

    } while (0 == retVal);

    if (timeout_timer) {
        dispatch_source_cancel(timeout_timer);
        dispatch_release(timeout_timer);
    } else {
        free(timeout_context);
    }

    return retVal;
}
Copy the code

Then take a quick look at the __CFRunLoopServiceMachPort code. A bunch of kernel programming code, very obscure. Notice that the mach_msg function is called, and that the input parameter MACH_RCV_MSG means that the Mach port is listening to receive messages instead of sending them.

static Boolean __CFRunLoopServiceMachPort(mach_port_name_t port, mach_msg_header_t **buffer, size_t buffer_size, mach_port_t *livePort, mach_msg_timeout_t timeout, voucher_mach_msg_state_t *voucherState, voucher_t *voucherCopy) {
    Boolean originalBuffer = true;
    kern_return_t ret = KERN_SUCCESS;
    for (;;) {		/* In that sleep of death what nightmares may come ... * /
        mach_msg_header_t *msg = (mach_msg_header_t *)*buffer;
        msg->msgh_bits = 0;
        msg->msgh_local_port = port;
        msg->msgh_remote_port = MACH_PORT_NULL;
        msg->msgh_size = buffer_size;
        msg->msgh_id = 0;
        if (TIMEOUT_INFINITY == timeout) { CFRUNLOOP_SLEEP(a); }else { CFRUNLOOP_POLL(a); }// Call mach_msg to receive the message
        ret = mach_msg(msg, MACH_RCV_MSG|(voucherState ? MACH_RCV_VOUCHER : 0)|MACH_RCV_LARGE|((TIMEOUT_INFINITY ! = timeout) ? MACH_RCV_TIMEOUT :0) |MACH_RCV_TRAILER_TYPE(MACH_MSG_TRAILER_FORMAT_0)|MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_AV), 0, msg->msgh_size, port, timeout, MACH_PORT_NULL);

        voucher_mach_msg_revert(*voucherState);
        
        *voucherState = voucher_mach_msg_adopt(msg);
        
        if (voucherCopy) {
            if(*voucherState ! = VOUCHER_MACH_MSG_STATE_UNCHANGED) { *voucherCopy =voucher_copy(a); }else {
                *voucherCopy = NULL; }}CFRUNLOOP_WAKEUP(ret);
        if (MACH_MSG_SUCCESS == ret) {
            *livePort = msg ? msg->msgh_local_port : MACH_PORT_NULL;
            return true;
        }
        if (MACH_RCV_TIMED_OUT == ret) {
            if(! originalBuffer)free(msg);
            *buffer = NULL;
            *livePort = MACH_PORT_NULL;
            return false;
        }
        if(MACH_RCV_TOO_LARGE ! = ret)break;
        buffer_size = round_msg(msg->msgh_size + MAX_TRAILER_SIZE);
        if (originalBuffer) *buffer = NULL;
        originalBuffer = false;
        *buffer = realloc(*buffer, buffer_size);
    }
    HALT;
    return false;
}
Copy the code

Finally, the single Loop of RunLoop is summarized to process the serial flow of various monitoring objects, as shown in the figure below. After excluding the special “GCD scheduling main thread operation” and the simple DoBlocks operation from the flow, the core flow can be obtained in two cases:

  • Left: DoSources0 triggers one of the Source0 callbacks, which triggers RunLoop sleep;
  • Right: DoSources0 does not trigger any Source0 callbacks, which does not trigger RunLoop sleep;

NOTE: NOTE the bifurcated DoTimes and DoSource1 in the figure above, meaning that this Loop handles either DoTimers or DoSource1 events. There is no Loop that handles both Timer and Source1 once. Since livePort is a single port, the above two events are if-else in a single Loop.

NOTE: Note that CFRunLoopPerformBlock does not have any port in Mode that specifically wakes up RunLoop to do DoBlocks. So CFRunLoopPerformBlock adding blocks to a RunLoop does not wake up the RunLoop, and will wait until the RunLoop is woken up before executing the blocks “incidentally”. It’s consistent with Apple Documentation.

conclusion

At this point, the basic RunLoop source code has been through. In the process, you’ve deepened your understanding of RunLoop a little bit. For example, each Mode contains a _timerPort that triggers the mk_timer event, and RunLoop all Timer timing events actually boil down to a mk_timer. For example, notice in Apple Documentation that BeforeWaiting and AfterWaiting events do not fire this detail in every Loop, and find the corresponding source.