Runloop profile

A Runloop is simply a waiting loop in which an event wakes up the thread to perform related event processing, while no event wakes up the thread to sleep. The events it receives include two types:

  • input sources
    • Port System definition event
    • Custom User-defined events
    • So perform is a method that you’re familiar with
  • timer souces

The Runloop create

Apple provides two interfaces, CFRunLoopGetMain() and CFRunLoopGetCurrent(), to create or get a thread-bound runloop. Source code we refer to Apple open source Swift cross-platform implementation, we focus on the analysis of CFRunLoopGetCurrent:

CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    return _CFRunLoopGet0(pthread_self());
}
Copy the code

There are two main steps from the source code:

  1. Introduced to the key__CFTSDKeyRunLoopThrough the_CFGetTSDGets a CFRunLoopRef, if any
  2. If step 1 is used_CFRunLoopGet0Get CFRunLoopRef

What is _CFGetTSD? Guess is to get some cache reference. Find the source code confirmation:

CF_EXPORT void *_CFGetTSD(uint32_t slot) {
    return _CFGetTSDCreateIfNeeded(slot, true);
}
CF_EXPORT void *_CFGetTSDCreateIfNeeded(const uint32_t slot, const Boolean create) CF_RETURNS_NOT_RETAINED {
  ...
    void * result = NULL;
    __CFTSDTable *table = __CFTSDGetTable(create); //code below
    if (table) {
        uintptr_t *slots = (uintptr_t *)(table->data);
        result = (void *)slots[slot];
    }
   ...;
    return result;
}

_CFSTDTable *__CFTSDGetTable(constBoolean create){ __CFSTDTable *table = __CFTSDGetSpecific(); . ; __CFSDSetSpecific(table); }void *__CFTSDGetSpecific(){

    return pthread_getspecific(__CFTSDIndexKey);
}
Copy the code

Confirm that _CFGetTSD is actually retrieving __CFTSDTable via pthread_getSpecific thread-local storage TLS (not shared by multiple threads but only accessed by the current thread), The CFRunLoopRef is then retrieved from the table via key__CFTSDKeyRunLoop.

If _CFGetTSD cannot get a runloop from the cache, proceed with _CFRunLoopGet0.

CF_EXPORT CFRunLoopRef _CFRunLoopGet0(_CFThreadRef t) {
    if (pthread_equal(t, kNilPthreadT)) {
	t = pthread_main_thread_np();
    }
    __CFLock(&loopsLock);
    if(! __CFRunLoops) { CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault,0.NULL, &kCFTypeDictionaryValueCallBacks);
	CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
	CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
	if(! OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
	    CFRelease(dict);
	}
	CFRelease(mainLoop);
    }
    CFRunLoopRef newLoop = NULL;
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    if(! loop) { newLoop = __CFRunLoopCreate(t); CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop); loop = newLoop; } __CFUnlock(&loopsLock);// don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
    if (newLoop) { CFRelease(newLoop); }
    if (pthread_equal(t, pthread_self())) {
        _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
        if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
#if _POSIX_THREADS
            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS- 1), (void(*) (void *))__CFFinalizeRunLoop);
#else
            _CFSetTSD(__CFTSDKeyRunLoopCntr, 0, &__CFFinalizeRunLoop);
#endif}}return loop;
}

Copy the code

If _CFRunLoopGet0 is not passed, mainRunLoop is created.

  • Get the runloop from the global container map cache, where key is the thread pointer CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
  • If not, create a new runloopnewloop = __CFRunLoopCreate(t)
  • Use Settings to cacheCFDictionarySetValue(global) +_CFSetTSD(TLS)

The question here is, why do you store two copies of dictionary and _CFSetTSD?

Runloop state/Mode

Runloop state

You can use the Observer to specify function callbacks for state changes

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry         = (1UL << 0), 
    kCFRunLoopBeforeTimers  = (1UL << 1), // Before processing Timer
    kCFRunLoopBeforeSources = (1UL << 2), // before processing Source
    kCFRunLoopBeforeWaiting = (1UL << 5), // About to sleep
    kCFRunLoopAfterWaiting  = (1UL << 6), // Just woke up from hibernation
    kCFRunLoopExit          = (1UL << 7), / / out of the Loop
};
Copy the code

RunloopMode

struct __CFRunLoopMode {. ; CFStringRef _name; CFMutableSetRef _sources0; CFMutableSetRef _sources1; CFMutableArrayRef _observers; CFMutableArrayRef _timers; CFMutableDictionaryRef _portToV1SourceMap; __CFPortSet _portSet; CFIndex _observerMask;#if USE_DISPATCH_SOURCE_FOR_TIMERS
    dispatch_source_t _timerSource;
    dispatch_queue_t _queue;
    Boolean _timerFired; // set to true by the source when a timer has fired
    Boolean _dispatchTimerArmed;
#endif__CFPort _timerPort; Boolean _mkTimerArmed; . ; };Copy the code

Each RunloopMode can contain the name name, Sources timers event; There are two kinds of timer in Swift CFRunloop open source implementation, one is based on Diapatch and the other is based on CFPort.

struct __CFRunLoop {. ; __CFPort _wakeUpPort;// used for CFRunLoopWakeUp_CFThreadRef _pthread; CFMutableSetRef _commonModes; CFMutableSetRef _commonModeItems; CFRunLoopModeRef _currentMode; . ; };Copy the code

As you can see from the code structure, Runloop contains multiple Runloopmodes, each of which can contain different source/timer events. So we can add the defined source/port etc to the system preset or mode we defined.

CFRunloop Run

The internal logic of RunLoop


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

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();
    if (modeName == NULL || modeName == kCFRunLoopCommonModes || CFEqual(modeName, kCFRunLoopCommonModes)) {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            CFLog(kCFLogLevelError, CFSTR("invalid mode '%@' provided to CFRunLoopRunSpecific - break on _CFRunLoopError_RunCalledWithInvalidMode to debug. This message will only appear once per execution."), modeName);
            _CFRunLoopError_RunCalledWithInvalidMode();
        });
        return kCFRunLoopRunFinished;
    }
    if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
    __CFRunLoopLock(rl);
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
    if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {/ / 1
	if (currentMode) { __CFRunLoopModeUnlock(currentMode); }
	__CFRunLoopUnlock(rl);
	return kCFRunLoopRunFinished;
    }
    volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);
    CFRunLoopModeRef previousMode = rl->_currentMode;
    rl->_currentMode = currentMode;
    int32_t result = kCFRunLoopRunFinished;

    if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);/ / 2

    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled,previousMode); / / 3

    if (currentMode->_observerMask & kCFRunLoopExit ){
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}
...;
    return result;
}

Copy the code

CFRunLoopRun calls CFRunLoopRunInMode and eventually CFRunLoopRunSpecific. The logical implementation is within CFRunLoopRunSpecific.

  1. __CFRunLoopFindMode(rl, modeName, false), currentMode (rl, modeName, false), At this point the runloop exits. This explains why afNetwork runloop starts with an empty port: to avoid runloop exit.

  2. The notification goes to runloop, kCFRunLoopEntry

    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry)

  3. Execute __CFRunLoopRun, the underlined internal implementation with the following code and main logic

    • Notice the Observer:kCFRunLoopBeforeTimers.kCFRunLoopBeforeSources
    • Notify source0 and process source0 __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
    • Notification of impending hibernation__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
    • Sleep, waiting for the event port message to wake up__CFRunLoopServiceMachPortcallmach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg}
    • Notifies the thread that it has been awakened afterwaiting__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
    • If timer wakes up, process timerCFRUNLOOP_WAKEUP_FOR_TIMER
    • If it is a dispatch wake up, processDISPATCH
    • If the source1 event wakes up, processSource1
    • Above the loop
static int32_t__CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) { ... ; __CFRunLoopUnsetIgnoreWakeUps(rl);//1. Notify kCFRunLoopBeforeTimers && kCFRunLoopBeforeSources
        if (rlm->_observerMask & kCFRunLoopBeforeTimers) {
            __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        }
        
        if (rlm->_observerMask & kCFRunLoopBeforeSources) {
            __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
        }
        // Execute the added BLOCK
	__CFRunLoopDoBlocks(rl, rlm);
        / / 2. Handle Sources0Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle); .//3. Notify kCFRunLoopBeforeWaiting that sleep is imminent
	if(! poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);4.dormancy__CFRunLoopServiceMachPort mach_msg(a)
	__CFRunLoopSetSleeping(rl);
        __CFPortSetInsert(dispatchPort, waitSet);
        CFAbsoluteTime sleepStart = poll ? 0.0: CFAbsoluteTimeGetCurrent(); . msg = (mach_msg_header_t *)msg_buffer;
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0: TIMEOUT_INFINITY, &voucherState, &voucherCopy, rl, rlm); . ; __CFRunLoopSetIgnoreWakeUps(rl);// user callouts now OK again
	__CFRunLoopUnsetSleeping(rl);
        // 6. Notify afterwaiting
	if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
        handle_msg:;
        __CFRunLoopSetIgnoreWakeUps(rl);

...;
 else if(rlm->_timerPort ! = MACH_PORT_NULL && livePort == rlm->_timerPort) { CFRUNLOOP_WAKEUP_FOR_TIMER();if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
                rlm->_timerSoftDeadline = UINT64_MAX;
                rlm->_timerHardDeadline = UINT64_MAX;
                __CFArmNextTimerInMode(rlm, rl);
            }
        }
...;
         /* --- DISPATCHES --- */
        
#if __HAS_DISPATCH__
        else if (livePort == dispatchPort) {
            CFRUNLOOP_WAKEUP_FOR_DISPATCH();

            _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6.NULL);
            CFRUNLOOP_ARP_BEGIN;
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            CFRUNLOOP_ARP_END;
            _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0.NULL);
            sourceHandledThisLoop = true;
            didDispatchPortLastTime = true;
        }
#endif
        
        /* --- SOURCE1S --- */
        
        else {
            CFRUNLOOP_WAKEUP_FOR_SOURCE();
            CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
            if (rls) {
		mach_msg_header_t *reply = NULL;
		sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
		if (NULL! = reply) {// switch mach_msg to kernel mode and wait for the event to occur
		    (void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL); CFAllocatorDeallocate(kCFAllocatorSystemDefault, reply); }}/* --- BLOCKS --- */
        
        if(msg && msg ! = (mach_msg_header_t *)msg_buffer) free(msg);
        // Execute the block added to the runloop__CFRunLoopDoBlocks(rl, rlm); retVal = .... ; }while (0== retVal); . ;return retVal;
}

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 * _Nonnull voucherState, voucher_t *voucherCopy, CFRunLoopRef rl, CFRunLoopModeRef rlm) {
    kern_return_t ret = KERN_SUCCESS;
    for (;;) {		/* In that sleep of death what nightmares may come ... * /. ret = mach_msg(msg, MACH_RCV_MSG|MACH_RCV_VOUCHER|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); . } HALT;return false;
}


Copy the code

supplement

Autoreleasepool and runloop

Corresponding runloop timing:

  • Entry:_objc_autoreleasePoolPush
  • Exit:_objc_autoreleasePoolPop
  • BeforeWaiting:_objc_autoreleasePoolPop->_objc_autoreleasePoolPush

Runloop and UIView drawing

The system registers a beforeWaiting observer that iterates through the view hierarchy previously changed by setFrame, setNeedsLayout/Display, before going to sleep. Call layer. Layout_if_needed – > layoutsublayers – > the layoutsubviews – > display – drawRect

_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
    QuartzCore:CA::Transaction::observer_callback:
        CA::Transaction::commit();
            CA::Context::commit_transaction();
                CA::Layer::layout_and_display_if_needed();
                    CA::Layer::layout_if_needed();
                        [CALayer layoutSublayers];
                            [UIView layoutSubviews];
                    CA::Layer::display_if_needed();
                        [CALayer display];
                            [UIView drawRect];
Copy the code

timer

NSTimer registers with runloop repeatedly for each callback point. After a certain point in time + tolerance, the next callback will be skipped.

GCD

Dispatch_async (dispatch_get_main_queue(),block), registered to runloop, The block is executed by __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__; Non-main threads are handled by libDispatch.

PerformSelecter

Essentially, the timer is set with runloop and the call is executed when the time is up.

tips

Swift open source CFRunloop has a long source code because it is suitable for various platforms. The core is that runloop’s extremely low CPU usage without events is due to the mach_msg system call in the while loop, which wakes up the thread only when an event occurs, and can handle different types of events at appropriate times, as well as notifying the user via callback functions. My view on source code is that for system-level developers, you need to focus on the underlying details; For those engaged in application development, individuals should have the ability to read the source code, but not too rigidly in the details of the implementation, understanding its ideas and principles of attention to solve the awareness of engineering problems is more important.