RunLoop is a very familiar term for iOS development, and with only one of them known, this chapter will take a closer look at RunLoop

RunLoop profile

What is the RunLoop

Generally speaking, a thread can only execute one task at a time, and when it is finished, the thread exits. If we need a mechanism that allows threads to handle events at any time without exiting, this model is often referred to as an Event Loop. Event Loop is implemented in many systems and frameworks, such as Node.js Event processing, Windows message Loop, and OSX/iOS RunLoop.

The key to implementing this model is to manage events/messages so that threads sleep when they are not processing messages to avoid resource usage and wake up as soon as a message arrives.

RunLoop role

  1. Keep the program running: When the program starts, it opens a main thread, which runs a RunLoop corresponding to the main thread. The RunLoop guarantees that the main thread will not be destroyed, thus keeping the program running
  2. Handle various events in the App (e.g. touch events, timer events, Selector events, etc.)
  3. Save CPU resources and improve program performance: When the program is running and nothing is being done, RunLoop tells the CPU that there is nothing to do, I need to rest, and the CPU frees up its resources to do something else. When something is being done, RunLoop gets up and does something else

Let’s take a quick look at how RunLoop works inside the API using an image

Why RunLoop

Now that you know what RunLoop does, why do you use RunLoop on Apple? There are several main points

  1. Keeps the program running and accepts user input
  2. Determines which events the program should process and when
  3. Call decoupling (Message Queue) : For example, a slippage event may trigger multiple messages, so a Message queue-like module must be used to decouple them, forming a Queue to process them in turn, so that the user’s caller and handler are completely decoupled
  4. Save CPU time and efficiency

RunLoop underlying principles

Let’s look at the source code to explore

RunLoop code hierarchy

  1. NSRunloopThe top layer of NSRunloop is an encapsulation of CFRunloop implemented in C. In fact, it does nothing. For example, CFRunloop has an expiration time of type double, NSRunloop changes it to NSDate.
  2. CFRunloopThe source code is open source and cross-platform.
  3. System layerThe underlying implementation uses GCD. The Mach kernel is an Apple kernel. For example, runloop sleep and wake up are implemented using the Mach kernel.

RunLoop entrance

In the OC code, Runloop is enabled by default. In the main function, the main thread and Runloop are enabled. UIApplicationMain does not return a Runloop, and UIApplicationMain does not return a Runloop. UIApplicationMain does not return a Runloop. It also ensures the continuous operation of the program.

int main(int argc, char * argv[]) {
    @autoreleasepool {
        returnUIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); }}Copy the code

Then we look at the source code, found CFRunLoopRun underlying implementation structure is also very simple, is a do… While loop, we can think of RunLoop as an infinite loop. Without RunLoop, the UIApplicationMain function will return directly after execution, and the program will not run continuously.

void CFRunLoopRun(void) {    /* DOES CALLOUT */
    int32_t result;
    do{result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10,false);
        CHECK_FOR_FORK();
    } while(kCFRunLoopRunStopped ! = result && kCFRunLoopRunFinished ! = result); }Copy the code

RunLoop in relation to threads

First, iOS development encounters two thread objects: pthread_t and NSThread. There was an Apple document that stated that NSthreads were simply wrappers of pthread_t, but that document is no longer valid, and now it is possible that they are all wrapped directly from low-level Mach threads. Apple doesn’t provide an interface for converting these two objects to each other, but you can be sure that pthread_T and NSThread correspond one to one. For example, you can get the mainThread by pthread_main_thread_np() or [NSThread mainThread]; The currentThread can also be retrieved by pthread_self() or [NSThread currentThread]. CFRunLoop is managed based on pThreads.

Apple does not allow the creation of runloops directly, and only provides two auto-fetching functions: CFRunLoopGetMain() and CFRunLoopGetCurrent(). The internal logic of these two functions looks something like this:

✅// Get the RunLoop object of the current thread and internally call _CFRunLoopGet0 CFRunLoopRef CFRunLoopGetCurrent(void) {CHECK_FOR_FORK(); CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);if (rl) return rl;
    return_CFRunLoopGet0(pthread_self()); } ✅// check the _CFRunLoopGet0 method CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {✅// If null t is set to the primary threadif(pthread_equal(t, kNilPthreadT)) { t = pthread_main_thread_np(); } __CFLock(&loopsLock); ✅// If runloop does not exist, create oneif(! __CFRunLoops) { __CFUnlock(&loopsLock); CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks); ✅// Run CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np()); ✅// Save mainline -key and runloop-value to the dictionary CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);if(! OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) { CFRelease(dict); } CFRelease(mainLoop); __CFLock(&loopsLock); } ✅// From the dictionary, CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t)); __CFUnlock(&loopsLock); ✅// If loop is empty, a new loop is created, so runloop is created on the first fetchif(! loop) { CFRunLoopRef newLoop = __CFRunLoopCreate(t); __CFLock(&loopsLock); loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t)); ✅// After the creation, the thread is used as the key and runloop is used as the value, which is stored one-to-one in the dictionary. The next time the dictionary is retrieved, the runloop in the dictionary is directly returnedif(! loop) { CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop); loop = newLoop; } / /donot release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it __CFUnlock(&loopsLock); ✅// Thread termination is to destroy loop CFRelease(newLoop); } ✅// If the incoming thread is the same as the current threadif(pthread_equal(t, pthread_self())) {✅// register a callback to destroy the corresponding runloop_cfsettsd (__CFTSDKeyRunLoop, (void *)loop, NULL) when the thread is destroyed;if(0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) { _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop); }}return loop;
}
Copy the code

According to the source code analysis, there is a one-to-one correspondence between threads and runloops, which is stored in a Dictionary. So when we create a child thread RunLoop, we simply get the RunLoop object of the current thread in the child thread [NSRunLoop currentRunLoop]; . If not, the child thread does not create the RunLoop associated with it, and only retrieves its RunLoop inside a thread.

When called by [NSRunLoop currentRunLoop]; The RunLoop () method first looks to see if there is a RunLoop in the dictionary for the child thread, returns a RunLoop if there is one, creates one if there is none, and stores the child thread to the dictionary. When the thread terminates, the RunLoop is destroyed.

To summarize Runloop’s relationship to threads

  1. Each thread has a unique RunLoop object corresponding to it
  2. Runloops are stored in a global Dictionary, with threads as keys and runloops as values
  3. The RunLoop for the main thread is created automatically, and the RunLoop for the child thread needs to be created actively
  4. The RunLoop is created on the first fetch and destroyed at the end of the thread

RunLoop underlying structure

CFRunLoopRef

Through the source code we find the __CFRunLoop structure

typedef struct __CFRunLoop * CFRunLoopRef; Struct __CFRunLoop {// CoreFoundation runtime base CFRuntimeBase; // Lock pthread_mutex_t _lock for mode list operations; /* lockedforAccessing mode list */ // Awakening port __CFPort _wakeUpPort; // usedforCFRunLoopWakeUp Boolean _unused; Volatile _per_run_data *_perRunData; volatile _perrun_data *_perRunData; // resetforRuns of the runloop // pthread_t _pthread; uint32_t _winthread; CFMutableSetRef _commonModes; CFMutableSetRef _commonModeItems; // runloop current mode CFRunLoopModeRef _currentMode; CFMutableSetRef _modes; Struct _block_item *_blocks_head; struct _block_item *_blocks_head; Struct _block_item *_blocks_tail; CFAbsoluteTime _runTime; CFAbsoluteTime _sleepTime; CFTypeRef _counterpart; }; // typedef struct _per_run_data {uint32_t a; uint32_t b; uint32_t stopped; // runloop Whether to stop uint32_t ignoreWakeUps; } _per_run_data; Struct _block_item {struct _block_item *_next; CFTypeRef _mode; CFTypeRef _mode; CFTypeRef _mode; Void (^_block)(void); }; };Copy the code

By looking at the underlying structure of RunLoop, we can see that RunLoop is also a structure object, which has several main variables:

  • CFRunLoopModeRef _currentMode:runloop Current mode
  • CFMutableSetRef _modes: Stores the set of modes

Through the above variables, we can know:

  • RunLoop can have multiple mode objects
  • A Runloop can only and must Run under a particular Mode at any one time. To change a Mode, you must stop the current Loop and then restart the new Loop, which means to exit the current while Loop and reset a new while

CFRunLoopModeRef

CFRunLoopModeRef is actually a pointer to the __CFRunLoopMode structure, the source of which is shown below

typedef struct __CFRunLoopMode *CFRunLoopModeRef; Struct __CFRunLoopMode {// CoreFoundation runtime basic information CFRuntimeBase _base; Pthread_mutex_t _lock; pthread_mutex_t lock; /* must have the run loop locked before locking this */ / CFStringRef _name; // mode whether to stop Boolean _stopped; char _padding[3]; //source0
    CFMutableSetRef _sources0;
    // source1 CFMutableSetRef _sources1; // observers CFMutableArrayRef _observers; // timers CFMutableArrayRef _timers; CFMutableDictionaryRef _portToV1SourceMap; // The set of ports __CFPortSet _portSet; // Observer mask CFIndex _observerMask; // If the GCD timer is defined#if USE_DISPATCH_SOURCE_FOR_TIMERS// GCD timer dispatch_source_t _timerSource; // dispatch_queue_t _queue; // Set to when GCD timer is triggeredtrue
    Boolean _timerFired; // set to true by the source when a timer has fired
    Boolean _dispatchTimerArmed;
#endif// if MK_TIMER is used#if USE_MK_TIMER_TOO
    // MK_TIMER 的 port
    mach_port_t _timerPort;
    Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
    DWORD _msgQMask;
    void (*_msgPump)(void);
#endif// Uint64_t _timerSoftDeadline; /* TSR */ // uint64_t _timerHardDeadline; /* TSR */ };Copy the code

We find that a CFRunLoopModeRef also contains many variables, including _sources0 and _sources0 sets, and _observers and _timers.

This shows that a mode can contain multiple items modes

CFRunLoopSourceRef

CFRunLoopSourceRef is the event source (input source). Through the source code can be found, it is divided into source0 and source1 two.

  • source0: Handles intra-app events, which the App is responsible for managing (triggering), such asUIEvent.CFSocketAnd so on;
  • source1: managed by Runloop and the kernel,mach portDrivers such as CFMachPort (a lightweight interprocess communication method that NSPort encapsulates, and Runloop sleep and wake up through),CFMessagePort;
typedef struct __CFRunLoopSource * CFRunLoopSourceRef; Struct __CFRunLoopSource {// CoreFoundation runtime basic information CFRuntimeBase _base; uint32_t _bits; // mutex pthread_mutex_t _lock; //sourceCFIndex _order; CFIndex _order; CFMutableBagRef _runLoops; /* immutable loops */ // a consortiumsourceEither forsource0, or 0source1
    union {
        CFRunLoopSourceContext version0;  /* immutable, except invalidation */
        CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
    } _context;
};

typedef struct {
    CFIndex version;
    // sourceVoid * info; const void *(*retain)(const void *info); void (*release)(const void *info); CFStringRef (*copyDescription)(const void *info); / / determinesourceBoolean (*equal)(const void *info1, const void *info2); CFHashCode (*hash)(const void *info);
    void    (*schedule)(void *info, CFRunLoopRef rl, CFStringRef mode);
    void    (*cancel)(void *info, CFRunLoopRef rl, CFStringRef mode);
    // sourceVoid (*perform)(void *info); } CFRunLoopSourceContext;Copy the code

CFRunLoopObserverRef

CFRunLoopObserverRef is the Observer, and each Observer contains a callback (function pointer) that the Observer receives when the state of the RunLoop changes. This is used to report changes to the current state of the Runloop to the outside world.

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {kCFRunLoopEntry = (1UL << 0),// About to enter Loop kCFRunLoopBeforeTimers = (1UL << 1),// About to process Timer KCFRunLoopBeforeSources = (1UL << 2),// About to process Source kCFRunLoopBeforeWaiting = (1UL << 5),// about to enter sleep kCFRunLoopAfterWaiting KCFRunLoopExit = (1UL << 7),// About to exit Loop kCFRunLoopAllActivities = 0x0FFFFFFFU // all events};Copy the code
typedef struct __CFRunLoopObserver * CFRunLoopObserverRef; Struct __CFRunLoopObserver {// CoreFoundation runtime base information CFRuntimeBase _base; // mutex pthread_mutex_t _lock; // Observer corresponding runloop CFRunLoopRef _runLoop; // The observer observed how many runloop CFIndex _rlCount; CFOptionFlags _activities; /* immutable */ / observer priority CFIndex _order; Immutable / * * / / / the observer callback function CFRunLoopObserverCallBack _callout; /* immutable */ / observer context CFRunLoopObserverContext _context; /* immutable, except invalidation */ }; typedef struct { CFIndex version; void * info; const void *(*retain)(const void *info); void (*release)(const void *info); CFStringRef (*copyDescription)(const void *info); } CFRunLoopObserverContext; typedef void (*CFRunLoopObserverCallBack)(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info);Copy the code

CFRunLoopTimerRef

CFRunLoopTimerRef is a time-based trigger that is toll-free bridged and can be mixed with NSTimer. It contains a length of time and a callback (function pointer).

When it is added to the RunLoop, the RunLoop registers the corresponding point in time, and when that point is reached, the RunLoop is woken up to execute that callback.

To summarize the structure of RunLoop

  1. RunLoopEssence is also a structure object
  2. RunloopModeAn event loop must run in a certain mode, several of which are predefined by the system. A Runloop has multiple modes;
  3. CFRunloopSource.CFRunloopTimer.CFRunloopObserverThese elements are in Mode, and Mode corresponds to these elements in a one-to-many relationship. But there must be at least oneSourceorTimerBecause if Mode is empty, RunLoop runs into empty Mode without idling and exits immediately.
  4. CFRunloopSourceDivided intosource0(handle user events) andsource1(Handling kernel events)
  5. CFRunloopObserverListen for and notify Runloop status

The RunLoop Mode

RunLoop has five operating modes, of which 1.2 are the most common.

1. KCFRunLoopDefaultMode: the default Mode of the App, in which the main thread usually runs Interface tracking Mode, to track ScrollView touch sliding, guarantee the interface slip is not affected by other Mode 3. UIInitializationRunLoopMode: When just start the App the first to enter the first Mode, after the completion of the start will no longer use, will switch to the 4 kCFRunLoopDefaultMode GSEventReceiveRunLoopMode: 5. KCFRunLoopCommonModes accept internal modes for system events, usually not used: This is a placeholder Mode, used to mark kCFRunLoopDefaultMode and UITrackingRunLoopMode, not a real ModeCopy the code

Switch between modes

When we slide UIScrollView to do something with NSTimer every once in a while, NSTimer will pause. When we stop sliding, NSTimer will resume again. This is because the RunloopMode must run in the same mode.

The main RunLoop has two preset modes: kCFRunLoopDefaultMode and UITrackingRunLoopMode. Both modes have been marked with the “Common” attribute. DefaultMode is the normal state of App, TrackingRunLoopMode is to track the state of ScrollView sliding. When you create a Timer and add DefaultMode, the Timer gets repeated callbacks, but when you slide a TableView, RunLoop switches mode to TrackingRunLoopMode, and the Timer doesn’t get called back. And it doesn’t affect the sliding operation.

Sometimes you need a Timer that gets callbacks in both modes. One way to do this is to add the Timer to both modes. Another option is to add the Timer to the top RunLoop’s “commonModeItems.” CommonModeItems is automatically updated by RunLoop to all modes with the Common property.

A Mode can mark itself as a “Common” attribute (by adding its ModeName to RunLoop’s “commonModes”). Whenever the contents of the RunLoop change, the RunLoop automatically synchronizes the Source/Observer/Timer in _commonModeItems to all modes with the “Common” flag.

RunLoop starts the logic

CFRunLoopRunSpecific () : CFRunLoopRunSpecific () : CFRunLoopRunSpecific () : CFRunLoopRunSpecific ()

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

CFRunLoopRunSpecific

Then we look at the CFRunLoopRunSpecific function, which is summarized in the following steps according to its code:

  • Find the given mode from the runloop
  • Assign the found mode to runloop’s _curentMode, that is, the runloop completes the mode switch
  • Call the core function __CFRunLoopRun
  • If an observer is registered, the runloop is notified to start, run, end, etc
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */ CHECK_FOR_FORK(); // If the runloop is being recycled, return kCFRunLoopRunFinished to indicate that the runloop is completeif (__CFRunLoopIsDeallocating(rl))
        returnkCFRunLoopRunFinished; // Lock runloop __CFRunLoopLock(rl); ✅// Find the given mode from runloop CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName,false); ✅// If no mode can be found and currentMode of the current runloop is empty, enterifIf the // __CFRunLoopModeIsEmpty result is empty, runloop has completed all tasksif (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode))
    {
        Boolean did = false; // If currentMode is not nullif(currentMode) // Unlock __CFRunLoopModeUnlock(currentMode); // Unlock runloop __CFRunLoopUnlock(rl);returndid ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished; } per_run_data volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl); ✅// Fetch the current mode of runloop CFRunLoopModeRef previousMode = rl->_currentMode; ✅// Assign the found mode to the _curentMode of runloop, which means that runloop has completed mode switching rl->_currentMode = currentMode; ✅// Initialization result int32_t result = kCFRunLoopRunFinished; ✅// Notify the Observer if the observer is registered to listen to the kCFRunLoopEntry status (about to enter loop)if(currentMode->_observerMask & kCFRunLoopEntry) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry); ✅✅✅✅// runloop core function __CFRunLoopRun result = __CFRunLoopRun(rl, currentMode, seconds,returnAfterSourceHandled, previousMode); ✅// Notify the Observer if you have registered an observer to listen for kCFRunLoopExit status (loop is about to be rolled out)if(currentMode->_observerMask & kCFRunLoopExit) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit); // Unlock currentMode __CFRunLoopModeUnlock(currentMode); // Restore previousPerRun __CFRunLoopPopPerRunData(rl, previousPerRun); Rl ->_currentMode = previousMode; // Unlock runloop __CFRunLoopUnlock(rl);return result;
}
Copy the code

CFRunLoopRun

CFRunLoopRun is the core function of RunLoop. A run is a run of CFRunLoopRun. The five parameters represent the following meanings:

  • CFRunLoopRef rlObject: CFRunLoopRef
  • CFRunLoopModeRef rlm: Indicates the name of mode
  • CFTimeInterval seconds: Timeout period
  • Boolean stopAfterHandle: Whether to return directly after processing source
  • CFRunLoopModeRef previousMode: Mode of the previous run cycle
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode)
Copy the code

Because the function of CFRunLoopRun is too long and the logic is complicated, we simplify the code and only explain some of the core logic, mainly including the following steps:

  1. usedispatch_source_tCreate a timer to handle timeout logic, default to a very large number if not set
  2. Start thedo... whileThe loop starts processing events
  3. noticeObservers: RunLoop is about to triggerTimerThe callback.
  4. noticeObservers: RunLoop is about to triggerSource0(Non-port) callback.
  5. Execute the added block
  6. If you haveSource1(Port based) in ready state, process the Source1 directly and jump to process the message.
  7. noticeObserversThe thread of RunLoop is about to go to sleep.
  8. callmach_msgWait formach_portThe news. The thread will sleep until it is awakened by one of the following events.
    • Port-based Source events;
    • Timer Indicates that the time is up.
    • RunLoop’s own timeout is up
    • Manually awakened by what other caller
  9. Notify Observers: The threads of RunLoop have just been awakened.
  10. The message is received and processed.
    • If a Timer runs out of time, the Timer’s callback is triggered.
    • If a block is dispatched to main_queue, execute the block.
    • If a Source1 (port-based) emits an event, handle the event
  11. Execute the block added to the Loop
  12. Determine whether to proceed to the next loop based on the current RunLoop state. When an external force stops or loop times out, the next loop is not continued; otherwise, the next loop continues.
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {dispatch_source_t timeout_timer = NULL; Struct __timeout_context *timeout_context = (struct __timeout_context) *)malloc(sizeof(*timeout_context)); . int32_t retVal = 0;do{// notify Observers that they are about to handle Timers __CFRunLoopDoObservers(RL, RLM, kCFRunLoopBeforeTimers); // Notify Observers that they are about to deal with Sources __CFRunLoopDoObservers(rl, RLM, kCFRunLoopBeforeSources); // Handle Blocks __CFRunLoopDoBlocks(rl, RLM); / / Source0 processingif(__CFRunLoopDoSources0(rl, RLM, stopAfterHandle)) {__CFRunLoopDoBlocks(rl, RLM); } Boolean poll =sourceHandledThisLoop || (0ULL == timeout_context->termTSR); // Check whether Source1 existsif(__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {// if there is Source1, goto handle_msg goto handle_msg; } didDispatchPortLastTime =false; // Notify Observers that they are about to hibernate __CFRunLoopDoObservers(rl, RLM, kCFRunLoopBeforeWaiting); __CFRunLoopSetSleeping(rl); CFAbsoluteTime sleepStart = poll ? 0.0: CFAbsoluteTimeGetCurrent ();do
        {
            if(kCFUseCollectableAllocator) { // objc_clear_stack(0); // <rdar://problem/16393959> memset(msg_buffer, 0, sizeof(msg_buffer)); } msg = (mach_msg_header_t *)msg_buffer; __CFRunLoopServiceMachPort(wait for another message to wake up the current threadwaitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);

            if(modeQueuePort ! = MACH_PORT_NULL && livePort == modeQueuePort) { // Drain the internal queue. If one of the callout blocks sets the timerFired flag,break out and service the timer.
                while (_dispatch_runloop_root_queue_perform_4CF(rlm->_queue))
                    ;
                if (rlm->_timerFired)
                {
                    // Leave livePort as the queue port, and service timers below
                    rlm->_timerFired = false;
                    break;
                }
                else
                {
                    if (msg && msg != (mach_msg_header_t *)msg_buffer)
                        free(msg);
                }
            }
            else
            {
                // Go ahead and leave the inner loop.
                break; }}while(1); // user callouts now OK again __CFRunLoopUnsetSleeping(rl); // Notify Observers to end dormancy __CFRunLoopDoObservers(rl, RLM, kCFRunLoopAfterWaiting); handle_msg:if{// Handle timers __CFRunLoopDoTimers(rl, RLM, mach_absolute_time()); }else if{// Process the GCD main queue __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(MSG); }else{/ / be awakened Source1 __CFRunLoopDoSource1 (rl, RLM, RLS, MSG, MSG - > msgh_size, & reply) | |sourceHandledThisLoop; } // handle Blocks __CFRunLoopDoBlocks(rl, RLM);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);

    return retVal;
}

Copy the code

RunLoop exit

  1. The main thread destroys RunLoop and exits
  2. There are timers, sources, and observers in the Mode that ensure that the RunLoop is not idling and running when the Mode is not empty, and that the RunLoop exits immediately when the Mode is empty
  3. Can we set when to stop RunLoop when we start it
[NSRunLoop currentRunLoop]runUntilDate:<#(nonnull NSDate *)#>
[NSRunLoop currentRunLoop]runMode:<#(nonnull NSString *)#> beforeDate:<#(nonnull NSDate *)#>
Copy the code

The application of the RunLoop

Application of RunLoop in system

AutoreleasePool

The App starts, apple registered in the main thread RunLoop two Observer, the callback is _wrapRunLoopWithAutoreleasePoolHandler ().

The first Observer monitors an event called Entry(about to enter Loop), which creates an automatic release pool within its callback by calling _objc_autoreleasePoolPush(). Its order is -2147483647, the highest priority, ensuring that the release pool is created before all other callbacks.

The second Observer monitors two events: calling _objc_autoreleasePoolPop() and _objc_autoreleasePoolPush() while waiting (ready to sleep) torelease the old pool and create a new one; _objc_autoreleasePoolPop() is called upon Exit(about to Exit the Loop) torelease the automatic release pool. The order of this Observer is 2147483647, the lowest priority, ensuring that its release pool occurs after all other callbacks.

The code that executes on the main thread is usually written inside such callbacks as event callbacks and Timer callbacks. These callbacks are surrounded by AutoreleasePool created by RunLoop, so there is no memory leak and the developer does not have to show that the Pool was created.

Incident response

Apple registered a Source1 (based on the Mach port) to the receiving system, the callback function as __IOHIDEventSystemClientQueueCallback ().

When a hardware event (touch/lock/shake, etc.) occurs, an IOHIDEvent event is first generated by IOKit. Framework and received by SpringBoard. Details of this process can be found here. SpringBoard only receives events such as buttons (lock screen/mute etc.), touch, acceleration and proximity sensor, and then forwards them to the App process using Mach port. Then apple registered the Source1 will trigger the callback, and call the _UIApplicationHandleEventQueue () distribution within the application.

_UIApplicationHandleEventQueue () will wrap IOHIDEvent process, and as a UIEvent processing or distribution, including identifying UIGesture/processing screen rotation/send UIWindow, etc. Usually click event such as a UIButton, touchesBegin/Move/End/Cancel events are completed in the callback.

Gesture recognition

When the above _UIApplicationHandleEventQueue () to identify a gesture, the first call will Cancel the current touchesBegin/Move/End series callback to interrupt. The system then marks the corresponding UIGestureRecognizer as pending.

Apple registered a Observer monitoring BeforeWaiting (Loop entering hibernation) events, the callback function Observer is _UIGestureRecognizerUpdateObserver (), Internally it gets all GestureRecognizer that has just been marked for processing and executes the GestureRecognizer callback.

This callback is handled when there are changes to the UIGestureRecognizer (create/destroy/state change).

Interface to update

When in operation the UI, such as changing the Frame, update the UIView/CALayer level, or manually invoked the UIView/CALayer setNeedsLayout/setNeedsDisplay method, The UIView/CALayer is marked to be processed and submitted to a global container.

Apple registers an Observer to listen for BeforeWaiting(about to go to sleep) and Exit (about to Exit Loop) events, calling back to perform a long function: _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv (). This function iterates through all the UIViews/Calayers to be processed to perform the actual drawing and adjustment, and update the UI.

The internal call stack looks something like this:

_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

The timer

NSTimer is essentially a CFRunLoopTimerRef. It’s toll-free bridged between them. Once an NSTimer is registered with a RunLoop, the RunLoop registers events for its repeated time points. Such as 10:00, 10:10, 10:20. To save resources, RunLoop does not call back this Timer at a very precise point in time. The Timer has a property called Tolerance that indicates the maximum error allowed when the time point is up.

If a point in time is missed, such as when a long task is executed, the callback at that point will also be skipped without delay. Like waiting for a bus, if I’m busy with my phone at 10:10 and I miss the bus at that time, I’ll have to wait for the 10:20 bus.

CADisplayLink is a timer with the same refresh rate as the screen (but the implementation is more complicated, unlike NSTimer, which actually operates on a Source). If a long task is executed between screen refreshes, one frame will be skipped (similar to NSTimer) and the interface will feel stuck. Even a frame stalling is noticeable to the user when swiping a TableView quickly. Facebook’s open source AsyncDisplayLink is designed to solve this problem and uses RunLoop internally.

PerformSelecter

When calling NSObject performSelecter: afterDelay: after its internal will actually create a Timer and add to the current thread RunLoop. So if the current thread does not have a RunLoop, this method will fail.

When the performSelector: onThread: when, in fact, it will create a Timer is added to the corresponding thread, in the same way, if the corresponding thread no RunLoop this method will fail.

About the GCD

NSTimer uses mk_timer from the XNU kernel. I checked and found that NSTimer is actually driven by mk_timer, not GCD). However, some GCD interfaces also use RunLoop, such as dispatch_async().

When dispatch_async(dispatch_get_main_queue(), block) is called, libDispatch sends a message to the main thread’s RunLoop, the RunLoop wakes up and takes the block from the message, This block is executed in the __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__() callback. But this logic is limited to dispatches to the main thread, dispatches to other threads are still handled by libDispatch.

About Network Request

In iOS, the interface for network requests has the following layers from bottom to top:

CFSocket
CFNetwork       ->ASIHttpRequest
NSURLConnection ->AFNetworking
NSURLSession    ->AFNetworking2, Alamofire
Copy the code
  • CFSocket is the lowest level interface and is only responsible for socket communication.
  • CFNetwork is the upper layer encapsulation based on CFSocket and ASIHttpRequest works on this layer.
  • NSURLConnection is a higher level encapsulation based on CFNetwork that provides an object-oriented interface. AFNetworking works at this layer.
  • NSURLSession is a new interface in iOS7, ostensibly alongside NSURLConnection, But the underlying still use the NSURLConnection features (such as com. Apple. NSURLConnectionLoader thread), AFNetworking2 and Alamofire work in this layer.

The following describes the working process of NSURLConnection.

Usually with NSURLConnection, you pass in a Delegate, and when [Connection start] is called, the Delegate will receive event callbacks. In fact, the start function gets the CurrentRunLoop internally and adds four source0s (sources that need to be triggered manually) to DefaultMode.

CFMultiplexerSource is responsible for various Delegate callbacks,

CFHTTPCookieStorage handles all kinds of cookies.

When the network transmission, we can see NSURLConnection created two new thread: com. Apple. NSURLConnectionLoader and com. Apple. CFSocket. Private. The CFSocket thread processes the underlying socket connection. The NSURLConnectionLoader thread uses RunLoop internally to receive events from the underlying socket and notify the upper Delegate via the previously added Source0.

Application of RunLoop in real development

AFNetworking

The AFURLConnectionOperation class is built on NSURLConnection and is expected to receive Delegate callbacks in the background thread. AFNetworking creates a separate thread for this purpose and starts a RunLoop in this thread:

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}
 
+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        [_networkRequestThread start];
    });
    return _networkRequestThread;
}
Copy the code

Before RunLoop starts, there must be at least one Timer/Observer/Source, so AFNetworking creates a new NSMachPort and adds it to it before [RunLoop run]. Normally, the caller needs to hold this NSMachPort (mach_port) and send messages inside the loop through this port from the external thread; But port is added here only to keep the RunLoop from exiting, not to actually send messages.

- (void)start {
    [self.lock lock];
    if ([self isCancelled]) {
        [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    } else if ([self isReady]) {
        self.state = AFOperationExecutingState;
        [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    }
    [self.lock unlock];
}
Copy the code

When need the background threads execute tasks, AFNetworking by calling [NSObject performSelector: onThread:..] Throw the task into the RunLoop of the background thread.

TableView lazily loads images

I’m going to put setImage in NSDefaultRunLoopMode, so it’s not going to call this method when I’m sliding, it’s going to call it when I’m sliding and I’m going to switch to NSDefaultRunLoopMode.

UIImage *downLoadImage = ... ; [self.avatarImageView performSelector:@selector(setImage:)  
                        withObject:downloadImage  
                        afterDelay:0  
                        inModes:@[NSDefaultRunLoopMode]];
Copy the code

Compatible handling of Crash

  1. Program Received Signal :SIGABRT SIGABRT is usually caused by over releasing or sending unrecogized selector.
  2. EXC_BAD_ACCESSWild pointer error due to access to freed memory. The system sends the signal to the App. After receiving the signal, the program will kill the RunLoop of the main thread and Crash. This example is valid only for the Crash caused by SIGABRT.

Detection of caton

When the main thread of the App suddenly freezes, we can use RunLoop to listen to the corresponding stack information, and then optimize the processing.

  • To listen for runloops, you first need to create oneCFRunLoopObserverContextThe observer
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,kCFRunLoopAllActivities,YES,0,&runLoopObserverCallBack,&context);
Copy the code
  • Add the created runLoopObserver to the common mode of the main RunLoop. A persistent child thread is then created specifically to monitor the RunLoop status of the main thread.
  • Once it is found that the state of kCFRunLoopBeforeSources before entering sleep or the state of kCFRunLoopAfterWaiting after awakening does not change within the set time threshold, it can be judged as being stuck. We can then dump the stack information to further analyze which methods are taking too long.
Dispatch_async (dispatch_get_global_queue(0, 0), ^{// The dispatch_get_global_queue(0, 0) starts a continuous loop to monitorwhile (YES) {
        long semaphoreWait = dispatch_semaphore_wait(dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC));
        if(semaphoreWait ! = 0) {if(! runLoopObserver) { timeoutCount = 0; dispatchSemaphore = 0; runLoopActivity = 0;return; } //BeforeSources and AfterWaiting are two states that can detect delaysif(runLoopActivity = = kCFRunLoopBeforeSources | | runLoopActivity = = kCFRunLoopAfterWaiting) {/ / the stack information reported to server code in here} / / end activity }// end semaphorewait
        timeoutCount = 0;
    }// end while
});
Copy the code

conclusion

  • What RunLoop does and means
    • To keep the program running, the program opens a main thread as soon as it starts and accepts user input
    • Handles events in your App and determines which events should be handled and when
    • Save CPU resources, improve program performance, sleep when necessary
    • Call decoupling, the user call time is queued to handle, caller and handler completely decoupled
  • Runloop in relation to threads
    • Each thread has a unique RunLoop object corresponding to it
    • Runloops are stored in a global Dictionary, with threads as keys and runloops as values
    • The RunLoop for the main thread is created automatically, and the RunLoop for the child thread needs to be created actively
    • The RunLoop is created on the first fetch and destroyed at the end of the thread
  • The nature and structure of RunLoop
    • RunLoop is also essentially a structure object
    • RunloopMode means that an event loop must run in a certain mode. The system defines several modes. A Runloop has multiple modes
    • CFRunloopSource, CFRunloopTimer, and CFRunloopObserver are elements in Mode, and the corresponding relationship between Mode and these elements is also 1-to-many. But there must be at least one Source or Timer, because if Mode is empty, the RunLoop runs into empty Mode without idling and exits immediately
    • CFRunloopSource is divided into source0(handling user events) and source1(handling kernel events)
    • The CFRunloopObserver listens for and notifies Runloop status
  • RunLoop starts the logic
    • Find the given mode from the runloop
    • Assign the found mode to runloop’s _curentMode, that is, the runloop completes the mode switch
    • The core function __CFRunLoopRun
    • If an observer is registered, the runloop is notified to start, run, end, etc
  • CFRunLoopRun logic
    • Use dispatch_source_t to create a timer to handle timeout-related logic, default to a particularly large number if not set
    • Start the do… The while loop starts processing the event
    • Notify Observers: RunLoop is about to trigger Timer and Source0 callbacks.
    • Execute the added block
    • If a Source1 (port-based) is ready, process the Source1 directly and jump to processing the message.
    • Notify Observers: RunLoop threads that they are about to enter sleep.
    • The mach_msg call waits for a message to accept mach_port. The thread will sleep until it is awakened by one of the following events.
    • Notify Observers: The threads of RunLoop have just been awakened.
    • The message is received and processed. (Timer, Dispatch,Source1, etc.) and executes the block callback
    • Determine whether to proceed to the next loop based on the current RunLoop state. When an external force stops or loop times out, the next loop is not continued; otherwise, the next loop continues.
  • RunLoop exit
    • The main thread destroys RunLoop and exits
    • There are timers, sources, and observers in the Mode that ensure that the RunLoop is not idling and running when the Mode is not empty, and that the RunLoop exits immediately when the Mode is empty
    • Can we set when to stop RunLoop when we start it

reference

Ibireme understands RunLoop in depth

Sun Yuan Runloop share

Deming iOS Master class

– RunLoop for iOS