What is the RunLoop?

RunLoop is actually an event processing loop that is used to schedule work and coordinate the receipt of incoming events. In general, a single thread to perform only one task at a time, completes the thread will exit, at any time if we want the thread to handle events and not quit, then open a RunLoop in the thread, RunLoop is a running cycle, its main purpose is to let the thread when there is work to keep busy, Hibernating when there is no work has the advantage of keeping the thread from tying up resources once it has hibernated.

The RunLoop structure is as follows

RunLoop handles timer events, performSelector events, custom sources, port events, and so on during the loop.

The entire running logic of RunLoop can be summed up in the following code

function loop() {
    initialize();
    doVar message = get_next_message(); // process the message process_message(message); }while(message ! = quit); }Copy the code

Actually RunLoop is, therefore, is an object, it manages the need for processing of events and news, and provides a thread with a as above the entrance of the function, after the entrance to the function that should execute when the thread will enter a “receiving messages – > dormancy wait – > processing” cycle, until the end of the cycle.

Both Cocoa and Core Foundation frameworks provide run loop objects to help configure and manage run loops for threads: NSRunLoop and CFRunLoopRef

  • CFRunLoopRef is a pure C API provided by the Core Foundation framework. All related apis are thread-safe. Source can refer to CF source download
  • NSRunLoop encapsulates the CFRunLoopRef, is object-oriented, and is not thread-safe.

The main functions of RunLoop can be summarized as follows:

  • Keeps the program running (main thread RunLoop enabled by default)
  • Handle various events in the App (touch events, timers)
  • Save CPU resources, improve the performance of the program (there is a message to process the message, no message to enter the sleep state, do not occupy resources)

RunLoop is related to threads

Running loops is not completely automatic. When we create a thread we need to start running loops at the appropriate time and respond to incoming events. As MENTIONED above, both Cocoa and Core Foundation frameworks provide run loop objects to help configure and manage running loops for threads. So instead of explicitly creating a RunLoop object, each thread in the program, including the main thread of the application, has an associated RunLoop object. However, only child threads need to explicitly open their run loop. The application framework automatically sets up and runs RunLoop on the main thread as part of the application startup process.

Apple’s API gives us two ways to get RunLoop objects

  • In the Cocoa framework, we use [NSRunLoop mainRunLoop] to get the RunLoop object for the main thread and [NSRunLoop currentRunLoop] to get the RunLoop object for the current thread.
  • In the Core Foundation framework, we use CFRunLoopGetMain() to get the main thread’s RunLoop object, and CFRunLoopGetCurrent() to get the current thread’s RunLoop object.

Reading the CF source code, we can obtain the internal logic of the RunLoop function as follows

CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {// If current thread is nilif(pthread_equal(t, kNilPthreadT)) {pthread_main_thread_np(t, kNilPthreadT); } __CFLock(&loopsLock);if(! __CFRunLoops) { __CFUnlock(&loopsLock); // For the first time, __CFRunLoops is nil, So you need to initialize global dic 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); __CFLock(&loopsLock); CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t)); __CFUnlock(&loopsLock);if(! CFRunLoopRef newLoop = __CFRunLoopCreate(t); CFRunLoopRef newLoop = __CFRunLoopCreate(t); __CFLock(&loopsLock); loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));if(! CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop) {// Save the new RunLoop to the global dictionary; loop = newLoop; } __CFUnlock(&loopsLock); CFRelease(newLoop); }if (pthread_equal(t, pthread_self())) {
        _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
        if(0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {// Register a callback to destroy the corresponding RunLoop when the thread is destroyed. _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop); }}return loop;
}
Copy the code

This shows the relationship between threads and runloops as follows

  • There is a one-to-one correspondence between threads and Runloops, with each thread having a unique RunLoop object corresponding to it
  • Runloops are stored in a global dictionary, with threads as keys and RunLoop objects as values
  • The thread is created without a RunLoop object. The RunLoop object is created the first time it gets it
  • The RunLoop is destroyed at the end of the thread
  • The RunLoop for the main thread is created and started automatically. The child thread has no RunLoop enabled by default and can only get its RunLoop inside the thread, except for the main thread.

RunLoop underlying structure

Five classes for RunLoop are provided in the Core Foundation framework

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef

The relationship between them is as follows:

In the midst of a RunLoop object contains more than one Mode, and each Mode contains several Source0 / Source1 / Timer/Observer, RunLoop startup can only specify one of the Mode, the Mode is called CurrentMode. If you need to switch Mode, you can only exit the current Loop and then re-specify Mode before entering the Loop. The main purpose is to split the different groups of Source0 / Source1 / Timer/Observer, allow them to each other.

If there is no any Mode of Source0 / Source1 / Timer/Observer, then the RunLoop exit immediately. Here Source0 / Source1 / Timer/Observer are collectively referred to as a Mode of the item.

CFRunLoopRef

A CFRunLoopRef is essentially a RunLoop object provided by the Core Foundation framework.

CFRunLoopModeRef

CFRunLoopModeRef is actually is multiple Source0 / Source1 / Timer/collection of the Observer. Each time the RunLoop is run, a specific pattern is specified, and during the RunLoop, only the sources associated with that pattern are monitored and allowed to deliver events. Similarly, only observers associated with the pattern can listen for state changes in the RunLoop. The general structure of CFRunLoopModeRef is as follows

struct __CFRunLoopMode { CFStringRef _name; //Mode name Boolean _stopped; char _padding[3]; CFMutableSetRef _sources0; //Source0 CFMutableSetRef _sources1; //Source1 CFMutableArrayRef _observers; CFMutableArrayRef _timers; // Timer set...... } struct __CFRunLoop { __CFPort _wakeUpPort; // usedfor CFRunLoopWakeUp 
    Boolean _unused;
    volatile _per_run_data *_perRunData; // reset forruns of the run loop CFMutableSetRef _commonModes; CFMutableSetRef _commonModeItems; // All Source/Observer/Timer CFRunLoopModeRef _currentMode marked Common; // CFMutableSetRef _modes specified by the current RunLoop; // All modes set...... };Copy the code

Mode mainly contains the following elements:

  • The name of the Mode
  • The set of Source0
  • The set of Source1
  • Observers set, observers
  • Set timers

Apple provides two public modes, kCFRunLoopDefaultMode (NSDefaultRunLoopMode) and UITrackingRunLoopMode

  • NSDefaultRunLoopMode is the default Mode of RunLoop, and the main thread RunLoop will be in this state normally.
  • UITrackingRunLoopMode is a Mode used to track UIScrollView slippage. When the program listens for ScrollView slippage, RunLoop will switch to this Mode

In RunLoop objects, the set _modes contains all modes supported by RunLoop. RunLoop also supports the concept of “CommonModes”. Each Mode can be marked as a “Common” attribute (by adding the Mode name to the RunLoop’s _commonModes set). The main thread RunLoop has two preset modes: Both kCFRunLoopDefaultMode (NSDefaultRunLoopMode) and UITrackingRunLoopMode have been marked as “Common”.

The _commonModeItems collection in RunLoop is used to store the Source/Observer/Timer marked common. When the RunLoop status changes, All Source/Observer/Timer items in _commonModeItems are synchronized to all modes with the common identifier.

Take timers for example. If we add a timer to the main thread and add it to NSDefaultRunLoopMode, the timer will call back normally. At this time, if there is a ScrollView on the interface and slide the ScrollView, RunLoop will switch to the UITrackingRunLoopMode mode. At this time, since the timer only exists in NSDefaultRunLoopMode, Once switched to UITrackingRunLoopMode, the timer stops and waits until RunLoop switches back to DefaultMode. If you want the timer to work in both modes, you can add the timer to both modes. Another way is to mark the timer as “common”, which is to add the timer to the _commonModeItems of the RunLoop object. The RunLoop automatically synchronizes the timer to the Mode with the Common flag. The code is as follows:

NSTimer * timer = [NSTimer timerWithTimeInterval: 1.0 repeats: YES block: ^ (NSTimer * _Nonnull timer) {NSLog (@"Timer task"); }]; [[NSRunLoop currentRunLoop] addTimer:timerforMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode]; [[NSRunLoop currentRunLoop] addTimer:timerforMode:NSRunLoopCommonModes];

Copy the code

In addition to using the modes exposed by Apple, you can also create custom modes with the following interface

// Create Mode by RunLoop. If the Mode name is not found within the RunLoop, RunLoop creates CFRunLoopModeRef CFRunLoopAddCommonMode(CFRunLoopRef RunLoop, CFStringRef modeName); CFRunLoopRunInMode(CFStringRef modeName, ...) ;Copy the code

Meanwhile, Mode also provides operation functions on Mode items, as follows

CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
Copy the code

CFRunLoopSourceRef

CFRunLoopSourceRef, also known as the input source, is where the event is generated. The input source is to pass the event asynchronously to the thread. The source of the event depends on the type of input source, and input sources generally fall into two categories: the first category is custom input source, monitoring custom event source, also known as Source0. The second type is port-based input sources that monitor an application’s Mach port, also known as Source1

  • Source0 contains only one callback and cannot actively fire events. To use Source0, you need to call CFRunLoopSourceSignal(source) first to mark the source as pending. CFRunLoopWakeUp(runloop) is then manually called to wake up the Runloop to handle the event. Simply put, Source0 is responsible for handling events within the App (UITouch events, etc.)
  • Source1 contains a mach_port and a callback pointer that is used to communicate with other threads through the kernel. Source1 can actively wake up the thread of RunLoop. In simple terms, Source1 is used to receive events emitted by the system (such as a phone touch, shake, lock screen, etc.)

Here’s a simple example: When the App is still in the foreground, if we click the page of the App, the first thing we touch is the screen of the phone. At this point, the Event of touching the screen will be packaged as an Event and passed to Source1. Then, Source1 will actively wake up RunLoop and then pass the Event to Source0 for processing.

CFRunLoopTimerRef

CFRunLoopTimerRef is a timer source that synchronizes events to the thread at a preset point in the future. Timers are a way for a thread to tell itself to do something. The timer generates time-based notifications, but it is not real-time. Like the input source, the timer is associated with a particular mode of running the loop.

  • If the timer is not in the mode that the run cycle is currently listening to, the timer is not triggered until the run cycle switches to the run mode supported by the timer.
  • Similarly, if the timer fires while the running loop is processing a program task, the timer will wait for the next time the loop is run to fire.
  • If the thread does not have RunLoop enabled, the timer does not fire.

Timers can be configured to generate one or more events, and the repeat timer will automatically reschedule itself based on a predetermined trigger time rather than the actual trigger time. For example, if a timer fires at a specific time and every 5s thereafter, the scheduled trigger time will always fall on the original 5s interval. If the RunLoop is processing a time-consuming task, the timer is delayed. If the RunLoop performs a 12-second task, the timer will fire once as soon as the RunLoop completes the task, and then the timer will reschedule for the next scheduled trigger. This means that the timer will only be triggered once in the 12 seconds it takes.

CFRunLoopObserverRef

CFRunLoopObserverRef is the Observer, and each Observer contains a callback that the Observer receives when the RunLoop status changes. RunLoop can be observed in several states

/* Run Loop Observer Activities */ typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { kCFRunLoopEntry = (1UL << 0), KCFRunLoopBeforeTimers = (1UL << 1), // Timer kCFRunLoopBeforeSources = (1UL << 2), // Source kCFRunLoopBeforeWaiting = (1UL << 5), kCFRunLoopAfterWaiting = (1UL << 6), // Wake up kCFRunLoopExit = (1UL << 7), // Exit loop kCFRunLoopAllActivities = 0x0FFFFFFFU};Copy the code

In code, we can listen for state changes in the RunLoop in the following ways

CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        switch (activity) {
            case kCFRunLoopEntry:{
                NSLog(@"Coming to loop");
                break;
            }
            case kCFRunLoopBeforeTimers:{
                NSLog(@"Timer about to be processed");
                break;
            }
            case kCFRunLoopBeforeSources:{
                NSLog(@"Source will be processed soon");
                break;
            }
            case kCFRunLoopBeforeWaiting:{
                NSLog(@"About to go to sleep.");
                break;
            }
            case kCFRunLoopAfterWaiting:{
                NSLog(@"To awaken from sleep.");
                break;
            }
            case kCFRunLoopExit:{
                NSLog(@"Exiting loop soon");
                break;
            }
            default:
                break; }}); CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopCommonModes); CFRelease(observer);Copy the code

RunLoop runs the logic

The logic flow of RunLoop is as follows:

  1. Notifying Observers that they are about to enter loop
  2. Notify Observers that Timers are about to be processed
  3. Notification Observers: Source0 is about to be processed
  4. Start processing Source0
  5. If there is Source1 at this point, skip to step 9
  6. Notify Observers: threads are about to hibernate
  7. Let the thread sleep until it wakes up at the following time
    • To receive Source1
    • Start the Timer
    • Sets the timeout value expiration for the run loop
    • External manual wake up
  8. Notifying Observers: Threads have just been awakened
  9. The message is received and the pending event is processed
    • If a user-defined Timer is triggered, the Timer event is processed and the loop restarts, jumping to Step 2
    • If the input source is fired at this point, the event is passed
    • If the running loop is woken up manually but has not yet timed out, restart the loop and go to Step 2
  10. Observers: exit loop

Here is the flow of the official documentation, which ignores the handling of blocks

RunLoop source

Create a project, break it at any point after the project starts, and then use the LLDB directive bt to get the call stack as follows:

The CFRunLoopRunSpecific function is the entry function of the RunLoop as follows (only some of the main code is reserved here) :

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {/* DOES CALLOUT */ / Loop __CFRunLoopDoObservers(RL, currentMode, kCFRunLoopEntry); Loop result = __CFRunLoopRun(rl, currentMode, seconds,returnAfterSourceHandled, previousMode); Observers: Observers __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);return result;
}
Copy the code

The real core of loop is the __CFRunLoopRun function, source code is as follows:

static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
    
    Boolean didDispatchPortLastTime = true;
    int32_t retVal = 0;
    doObservers: they are about to handle Timers __CFRunLoopDoObservers(RL, RLM, kCFRunLoopBeforeTimers); Observers: they are about to deal with Source0(non-port) __CFRunLoopDoObservers(rl, RLM, kCFRunLoopBeforeSources); // Execute the added block __CFRunLoopDoBlocks(rl, RLM); // The Source0 callback is BooleansourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
        if (sourceHandledThisLoop) {// Execute the added block __CFRunLoopDoBlocks(rl, RLM) again; } Boolean poll =sourceHandledThisLoop || (0ULL == timeout_context->termTSR); // Check whether there is a Source1 that needs to be processed. If so, go to handle_msgif(MACH_PORT_NULL ! = dispatchPort && ! didDispatchPortLastTime) { msg = (mach_msg_header_t *)msg_buffer;if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
                goto handle_msg;
            }
        }
        didDispatchPortLastTime = false; // notify Observers that the thread where RunLoop resides is about to enter hibernationif(! poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting); // call mach_msg to put the thread to sleep. Start Timer //3, set timeout value expiration for the run loop //4, manually wake up __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy); Observers: The RunLoop thread has just woken upif(! poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting); // start processing handle_msg:;if{//9-1, if a user-defined Timer is triggered, the Timer event is processed and the loop __CFRunLoopDoTimers(rl, RLM, mach_absolute_time()} is restarted.else ifIf there is an operation to return to the main thread in the GCD child thread, then the main thread is woken up and executed block __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(MSG); }else{//9-3, if a port-based Source1 event is passed in, Events will handle Source1 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) { (void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL); }}} // Execute the added block __CFRunLoopDoBlocks(rl, RLM) again;if (sourceHandledThisLoop && stopAfterHandle) {/ / if the current event processing complete return retVal = kCFRunLoopRunHandledSource directly; }else ifRetVal = kCFRunLoopRunTimedOut; (timeout_context->termTSR < mach_absolute_time()) {retVal = kCFRunLoopRunTimedOut; }else if(__CFRunLoopIsStopped(rl)) {retVal = kCFRunLoopRunStopped; }else if (rlm->_stopped) {
            rlm->_stopped = false;
            retVal = kCFRunLoopRunStopped;
        } else if(__CFRunLoopModeIsEmpty(rl, rlm, RetVal = kCFRunLoopRunFinished; retVal = kCFRunLoopRunFinished; retVal = kCFRunLoopRunFinished; } // If RunLoop has not timed out, is not stopped, and the specified Mode has a ModeItem, run RunLoop}while (0 == retVal);

        return retVal;
}
Copy the code

RunLoop callback

Every time RunLoop makes a callback, it calls a particularly long function, such as __CFRunLoopDoObservers, to notify Observers that they are about to enter RunLoop, The CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION function is called internally. The following converts the corresponding function in the source code to the actual function called, as follows

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {/* DOES CALLOUT */ / Loop __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry); Loop result = __CFRunLoopRun(rl, currentMode, seconds,returnAfterSourceHandled, previousMode); Observers: Observers __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);return result;
}

static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {

    Boolean didDispatchPortLastTime = true;
    int32_t retVal = 0;
    do{// step 2, notify Observers: they are about to handle Timers __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers); // Observers: Source0(non-port) __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources); // Execute the added block __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block); // The Source0 callback is BooleansourceHandledThisLoop = __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
        if (sourceHandledThisLoop) {// Execute the added block __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block) again; } Boolean poll =sourceHandledThisLoop || (0ULL == timeout_context->termTSR); // Check whether there is a Source1 that needs to be processed. If so, go to handle_msgif(MACH_PORT_NULL ! = dispatchPort && ! didDispatchPortLastTime) { msg = (mach_msg_header_t *)msg_buffer;if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
                goto handle_msg;
            }
        }
        didDispatchPortLastTime = false; // Observers, __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting); // call mach_msg to put the thread to sleep. Start Timer //3, set timeout value expiration for the run loop //4, manually wake up __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy); Observers: the RunLoop thread is astounded __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting); // start processing handle_msg:;if{//9-1, if a user - defined Timer is triggered, the Timer event is processed and the loop is restarted __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(Timer); }else if{//9-2, if there is an operation to return to the main thread in the GCD child thread, then wake up the thread, Execute the main thread block __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block); }else{//9-3, handle the Source1 event __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1); } // Execute the added block __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block) again; // If RunLoop has not timed out, is not stopped, and the specified Mode has a ModeItem, run RunLoop}while (0 == retVal);
        
        return retVal;
}
Copy the code

The specific flow chart is summarized as follows:

The process is interspersed with the processing of blocks, which can be added by the CFRunLoopPerformBlock function.

RunLoop application

Thread to keep alive

One important aspect of RunLoop is that it can be used to control the life cycle of a thread, that is, thread packet activity. The main thread’s RunLoop is enabled by default, so the main thread is always active, but the child thread does not have RunLoop enabled by default, so the child thread is destroyed after it completes its task. Early AFNetworking 2.x versions used RunLoop to keep threads alive. The concrete implementation is as follows:

The source code for xlthread. h is as follows

#import <Foundation/Foundation.h>NS_ASSUME_NONNULL_BEGIN typedef void(^XLThreadTask)(void); @interface XLThread: NSObject /** Start task */ - (void)executeTask:(XLThreadTask)task; /** stop the thread */ - (void)stop; @end NS_ASSUME_NONNULL_ENDCopy the code

XLThread. M is as follows

#import "XLThread.h"
#import <objc/runtime.h>

@interface XLThread ()

@property(nonatomic, strong)NSThread *innerThread;

@end

@implementation XLThread

#pragma mark - Public
- (instancetype)init
{
    self = [super init];
    if (self) {
        
        self.innerThread = [[NSThread alloc] initWithTarget:self selector:@selector(__initThread) object:nil];
        [self.innerThread start];
    }
    return self;
}

- (void)executeTask:(XLThreadTask)task{
    if(! self.innerThread || ! task) {return;
    }
    [self performSelector:@selector(__executeTask:) onThread:self.innerThread withObject:task waitUntilDone:NO];
}


- (void)stop
{
    if(! self.innerThread)return;
    
    [self performSelector:@selector(__stop) onThread:self.innerThread withObject:nil waitUntilDone:YES];
}

- (void)dealloc
{
    SBPLog(@"%s", __func__);
    
    [self stop];
}

#pragma mark - Private- (void)__stop{ CFRunLoopStop(CFRunLoopGetCurrent()); self.innerThread = nil; } - (void)__executeTask:(XLThreadTask)task{ task(); } - (void)__initThread{// Create context CFRunLoopSourceContext context = {0}; / / createsource
    CFRunLoopSourceRef source= CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context); // Add to runloopsource
    CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode); / / destroysource
    CFRelease(source); // Start runLoop CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10,false);
}

@end
Copy the code

AutoreleasePool

In iOS, AutoreleasePool is also implemented based on RunLoop. When the App starts, two observers are registered in the RunLoop of the main thread.

  • The first Observer listens for an Entry event in a RunLoop and creates an automatic release pool by calling _objc_autoreleasePoolPush() when an Entry event is heard.
  • The second Observer listens for two kinds of events
    • When RunLoop is ready to go to sleep (in the BeforeWaiting state), _objc_autoreleasePoolPop() is called torelease the old pool, and _objc_autoreleasePoolPush() is called to create a new automatic release pool
    • When RunLoop exits (in Exit state), _objc_autoreleasePoolPop() is called torelease AutoreleasePool

The detailed implementation of AutoreleasePool is covered in a later section.

NSTimer

CFRunLoopTimerRef = NSTimer, NSTimer must be triggered based on RunLoop, and RunLoop must be enabled. Normally we use timers in the main thread because the RunLoop is enabled by default for the main thread. If you want to use timers in child threads, you need to manually enable the child thread RunLoop.

When we create an NSTimer and register it with a RunLoop, the RunLoop registers events at repeated points in time according to the preset firing time. For example, the timer is set to start firing at 5:10 and every 5m. The timer triggers at a fixed time of 5:10, 5:15, 5:20, 5:25, etc., but RunLoop doesn’t fire the timer at exactly the right time to save resources. If the RunLoop executes a task so long that it misses a preset time point before triggering the timer, the timer will fire once the long task is complete and then again at the next preset time point.

If the timer is scheduled to trigger at 5:10, but the RunLoop executes the time-consuming task until 5:22, the timer will be triggered immediately after the time-consuming task is completed, and then the timer will be triggered again at 5:25.

PerformSelector

As mentioned earlier, the input sources in RunLoop are divided into two types: port-based input sources and custom input sources.

The Cocoa framework provides us with a custom Perform Selector Source that allows us to Perform a Selector on any thread and is automatically removed from the RunLoop after the Selector is executed. When we perform a Selector in another thread using performSelector, we must ensure that the target thread has a RunLoop enabled, so we need to explicitly start the target thread’s RunLoop.

So here’s the performSelector method declared in NSObject

methods describe
performSelectorOnMainThread:withObject:waitUntilDone:

performSelectorOnMainThread:withObject:waitUntilDone:modes:
Executes the specified Selector during the next cycle of the RunLoop of the application’s main thread.

And that method can block the current thread until the Selector is executed.
performSelector:onThread:withObject:waitUntilDone:

performSelector:onThread:withObject:waitUntilDone:modes:
Points to the specified Selector on any thread.

And that method can block the current thread until the Selector is executed.
performSelector:withObject:afterDelay:

performSelector:withObject:afterDelay:inModes:
Executes the specified selector on the current thread during the next run cycle and after an optional delay.

Because you wait until the next run cycle to execute the selector, these methods provide minimal automatic delay for the current code execution.

Multiple queued selectors are executed in the order they are queued
cancelPreviousPerformRequestsWithTarget:

cancelPreviousPerformRequestsWithTarget:selector:object:
Cancels the message sent to the current thread

As a simple example, add the following sample code to the touchesBegan: method

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"Task 1");
    [self performSelector:@selector(test) withObject:nil afterDelay:.0];
    NSLog(@"Task 3");
}

- (void)test{
    NSLog(@Task 2 "");
}
Copy the code

When the page is clicked, the order of execution is task 1, task 3, and task 2. The reason is very simple, because the performSelector: withObject: afterDelay method NSTimer essence is to create a timer, and add a timer to the RunLoop for processing. The whole process is roughly as follows:

  • By first clicking the screen, you receive a Source1 event, which wakes up the RunLoop, and then passes the event to Source0
  • RunLoop is awakened and re-enters the loop, so the Source0 event is processed first, so tasks 1 and 3 are executed
  • Task 2 is executed by adding NSTimer. Although it is delayed for 0s, it is always a timer event, while RunLoop processes NSTimer events only when it is woken up. Therefore, task 2 is executed only when the RunLoop is woken up next time.

Refer to the article

Official RunLoop documentation

Deep understanding of RunLoop | Garan no dou

conclusion

The above content is purely personal understanding, if there is anything wrong, welcome to comment.

Learn together and make progress together