This article explores the underlying Runloop in order to gain a better understanding of what runloops are for. When will it be used? How does it work?

1. What is runloop?

A runloop is a loop that runs continuously. When an iOS application is opened, a main thread is created and a Runloop is created by default to keep the main thread running.

Let’s go to the official documentation and search for Runloop, as shown in the figure below

Thus, there is an ambiguous relationship between Runloop and thread.

Look at the CFRunLoopRun function in the CFRunloop source code

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

As we can see, Runloop is essentially a do… The while loop. Take a look at how Runloop is executed using the Runloop structure provided in the official documentation.

A Runloop is a loop attached to a thread that receives events from Input sources and Timer sources and then hands them to the thread to handle the events.

So what is Runloop? A Runloop is a loop created for threads, and is essentially a do… The while loop

2. What Runloop does

  • 1. Keep the program running Continuously. Normally, a thread exits when it finishes executing a task, which is needed if we want it to perform more tasks without exitingRunloop.
  • Runloop receives App events (touch events, UI refresh time, timer, performSelector) through input source and timer source during loop.
  • 3. Improve performance. Sleep when threads are not working, saving CPU resources.

That’s what Runloop does. This is just a summary of what Runloop does.

3, Runloop and thread relationship

Let’s take a look at the Runloop source code to see how it relates to threads. Find the _CFRunLoopGet0 function, which gets the Runloop object

CF_EXPORT cfrunloop_cfrunloopget0 (pthread_t t) {CF_EXPORT cfrunloop_cfrunloopget0 (pthread_t t) {if(! __CFRunLoops) {CFMutableDictionaryRef dict = CFMutableDictionaryRef dict = CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks); Runloop 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); __CFSpinLock(&loopsLock); CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t)); __CFSpinUnlock(&loopsLock);if(! CFRunLoopRef newLoop = __CFRunLoopCreate(t); __CFSpinLock(&loopsLock); loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));if(! Loop) {CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop); loop = newLoop; }}}Copy the code

CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_NP ()), mainLoop); “, meaning that the thread is the key, runloop is the value, and runloop is stored in a global dictionary. This brings us to the first conclusion: threads and runloops have a one-to-one correspondence.

CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np()); , meaning to create a main thread Runloop. This leads to the second conclusion: Runloops are created with thread parameters and stored in the global dictionary.

Look at the second half of the code

CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t)); __CFSpinUnlock(&loopsLock);if(! CFRunLoopRef newLoop = __CFRunLoopCreate(t); __CFSpinLock(&loopsLock); loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));if(! Loop) {CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop); loop = newLoop; }}Copy the code

Loops get a Runloop object from __CFRunLoops. If not, create one with a thread argument and store it in __CFRunLoops. The Runloop for the main thread is created automatically, and the Runloop for the child thread is created when the child thread manually retrives the Runloop.

Threads and runloops have a one-to-one relationship. Runloop is created as a thread parameter and stored in the global dictionary. 3. The main thread Runloop is automatically created by the system, and the child thread Runloop must be created when the child thread obtains the Runloop manually. The Runloop is created on the first fetch and destroyed when the thread is destroyed.

4. Five objects of Runloop

  1. __CFRunLoop * CFRunLoopRef;
  2. __CFRunLoopSource * CFRunLoopSourceRef;
  3. __CFRunLoopObserver * CFRunLoopObserverRef;
  4. __CFRunLoopTimer * CFRunLoopTimerRef;
  5. CFRunloopModeRef(Runloop does not expose RunloopMode)

Let’s take a look at each of the Runloop objects and their relationships, as shown in the figure below

CFRunloopModeRef (Running mode)
CFRunloopModeRef
CFRunLoopSourceRef(Input source)
CFRunLoopTimerRef (timing source)
CFRunLoopObserverRef (Watch source)
CFRunloopModeRef (Running mode)
CFRunloopModeRef
CFRunloopModeRef (Running mode)

Why is this structure? A: This is mainly done to separate the Source/Timer/Observer from each other.

4.1 CFRunLoopRef (Runloop object)

CFRunLoopRef is a RunLoop object class in the Core Foundation framework and can be obtained in the following ways

// Get the current thread's RunLoop object CFRunLoopGetCurrent(); // Get the main thread RunLoop object CFRunLoopGetMain();Copy the code

An encapsulated Runloop can also be obtained using NSRunloop in the Foundation framework, which encapsulates the CFRunLoopRef

// Get the current thread's RunLoop object [NSRunLoop currentRunLoop]; NSRunLoop mainRunLoop [NSRunLoop mainRunLoop];Copy the code
4.2 CFRunLoopSourceRef (Input Source)

Take a look at the source code

struct __CFRunLoopSource { CFRuntimeBase _base; uint32_t _bits; pthread_mutex_t _lock; CFIndex _order; /* immutable */ CFMutableBagRef _runLoops; Union {// corresponding to Source0 CFRunLoopSourceContext version0; /* immutable, except invalidation */ / corresponding to Source1 CFRunLoopSourceContext1 version1; /* immutable, except invalidation */ } _context; };Copy the code

Source is divided into two versions

  • 1. Source0: This version of Source cannot actively trigger events, but must be manually triggered by users or developers, such as touch events and UIEvent within App such as performSelector, which need to be carried out firstCFRunLoopSourceSignalMark and pass againCFRunLoopWakeUpWake up Runloop to handle events.
  • Source1: A port-based Source used for communication between the kernel and the thread. This Source can actively wake up the Runloop thread, as we’ll see in a moment.

Looking at an example of a touch event triggering Source0, create a project with a button on top that breaks the point in the callback of the click event, as shown

Here is an example use of Source0

To create a Source0 input source, perform six steps, CFRunloopSourceSignal = CFRunLoopWakeUp = CFRunLoopWakeUp = CFRunLoopWakeUp = CFRunLoopWakeUp = CFRunLoopWakeUp

- (void)sourceCFRunLoopSourceContext Context = {0, NULL, NULL, NULL, NULL, NULL, NULL, schedule, cancel, perform, }; Pass NULL or kCFAllocatorDefault to use the current default allocator. Parameter two: Priority index indicating the order in which run loop sources are processed. Here I pass 0 for the automatic callback argument 3: the structure that holds context information for the run input source */ CFRunLoopSourceRefsource0 = CFRunLoopSourceCreate(CFAllocatorGetDefault(), 0, &context); CFRunLoopRef RLP = CFRunLoopGetCurrent(); //4, bind Source, Runloop, ModesourceCFRunLoopAddSource(RLP,source0, kCFRunLoopDefaultMode); // CFRunLoopSourceSignal(source0); CFRunLoopWakeUp(RLP); // CFRunLoopRemoveSource(RLP,source0, kCFRunLoopDefaultMode);
    CFRelease(rlp);
}

void schedule(void *info, CFRunLoopRef rl, CFRunLoopMode mode){
    NSLog(@"Prepare to handle the incident.");
}

void perform(void *info){
    NSLog(@"Boy, do the deed.");
}

void cancel(void *info, CFRunLoopRef rl, CFRunLoopMode mode){
    NSLog(@"Remove input source, event stops execution");
}
Copy the code

CFRunLoopSourceSignal is called and CFRunLoopWakeUp is called to wake up the Runloop to execute the Source0 event.

The console displays the following results

Here is an example of Source1 using Port for interthread communication

create

@interface ViewController ()<NSPortDelegate> @property (nonatomic, strong) NSPort* subThreadPort; port@property (nonatomic, strong) NSPort* mainThreadPort; Port @end@implementation ViewController - (void)viewDidLoad {[super viewDidLoad]; [self portCommunicateTest]; } - (void)portCommunicateTest{ self.mainThreadPort = [NSPort port]; self.mainThreadPort.delegate = self; // port -source1 -- runloop / / the port is operating Source1, so also depend on the runloop [[NSRunLoop currentRunLoop] addPort: self. MainThreadPortforMode:NSDefaultRunLoopMode]; // Dispatch_async (dispatch_get_global_queue(0, 0), ^{// instantiate Port self.subThreadPort = [NSPort Port]; self.subThreadPort.delegate = self; [[NSRunLoop currentRunLoop] addPort:self.subThreadPortforMode:NSDefaultRunLoopMode]; [[NSRunLoop currentRunLoop] run]; }); } // the proxy method for NSPort, the callback for communication between threads - (void)handlePortMessage:(id)message {NSLog(@"The current thread is == %@", [NSThread currentThread]); // 3 1
    NSLog(@"Incoming message content = %@", [[NSString alloc] initWithData:[message valueForKey:@"components"][0] encoding:NSUTF8StringEncoding]);
    sleep(1);
    if(! [[NSThread currentThread] isMainThread]) {NSMutableArray* Components = [NSMutableArray array]; NSData* data = [@"world"dataUsingEncoding:NSUTF8StringEncoding]; [components addObject:data]; [self.mainThreadPort sendBeforeDate:[NSDate date] components:components from:self.subThreadPort reserved:0]; }} - (void)touch began :(NSSet< touch *> *)touches withEvent:(UIEvent *)event {// Component must pass NSMutableArray* Components = [NSMutableArray array] as NSData; NSData* data = [@"Hello" dataUsingEncoding:NSUTF8StringEncoding];
    [components addObject:data];
    
    [self.subThreadPort sendBeforeDate:[NSDate date] components:components from:self.mainThreadPort reserved:0];
}
Copy the code

After clicking the screen, the following results are printed

Source1

4.3, CFRunloopModeRef

Runloop operates in five modes

    1. UIInitializationRunLoopMode: When the App enters the running mode just after startup, it will switch to kCFRunLoopDefaultMode after startup and will not be used anymore.
    1. kCFRunLoopDefaultMode: Default running mode, running in this mode on the main thread.
    1. UITrackingRunLoopMode: Interface tracking mode. When scrolling, it will switch to this running mode to ensure that it is not affected by other modes.
    1. GSEventReceiveRunLoopMode: Accepts the internal running mode of system events.
    1. kCFRunLoopCommonModes: placeholder mode, usually used for markingkCFRunLoopDefaultModeandUITrackingRunLoopModeIf NSTimer adds this mode, it will not be affected by running mode switching.
4.4, CFRunloopTimerRef

CFRunloopTimerRef is a time trigger that contains a length of time and a callback (function callback). When a Runloop is added, the Runloop registers a point in time after which the Runloop will be woken up to execute the callback. At the bottom of the Timer is a CFRunloopTimerRef, which is affected by Mode switching. If the Timer is added to kCFRunLoopCommonModes it will not be affected by the switch, as shown below

NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval: 1.0 f repeats: YES block: ^ (NSTimer * _Nonnull timer) {NSLog (@"log NSTimer runloop");
}];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
Copy the code
4.5, CFRunloopObserverRef

CFRunloopObserverRef is an observer that monitors the status of the Runloop. It has the following states

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { kCFRunLoopEntry = (1UL << 0), // About to enter Loop 1 kCFRunLoopBeforeTimers = (1UL << 1), // About to process Timer 2 kCFRunLoopBeforeSources = (1UL << 2), Source 4 kCFRunLoopBeforeWaiting = (1UL << 5), 32 kCFRunLoopAfterWaiting = (1UL << 6), // Just woke up from hibernation 64 kCFRunLoopExit = (1UL << 7), // about to exit Loop 128};Copy the code

For example

CFRunLoopObserverContext Context = {0, ((__bridge void *)self), NULL, NULL, NULL}; CFRunLoopRef RLP = CFRunLoopGetCurrent(); CFRunLoopObserverRef CFRunLoopObserverRef observerRef = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, runLoopObserverCallBack, &context); CFRunLoopAddObserver(RLP, observerRef, kCFRunLoopDefaultMode); } void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){ switch (activity) {case kCFRunLoopEntry:
        NSLog(@"RunLoop into");
        break;
    case kCFRunLoopBeforeTimers:
        NSLog(@"RunLoop should handle Timers");
        break;
    case kCFRunLoopBeforeSources:
        NSLog(@"RunLoop will handle Sources");
        break;
    case kCFRunLoopBeforeWaiting:
        NSLog(@"RunLoop is going to rest.");
        break;
    case kCFRunLoopAfterWaiting:
        NSLog(@"RunLoop wakes up.");
        break;
    case kCFRunLoopExit:
        NSLog(@"RunLoop exits.");
        break;
        
    default:
        break; }}Copy the code

As we scroll through the view, the console prints the following

☺ the status of the Runloop is listened to with CFRunloopObserverRef.

There are many applications of Runloop. The previous application of Runloop in the project was to divide UI tasks that put a lot of pressure on CPU into multiple small tasks, and to monitor the idle time of Runloop’s Observer and force it to perform small tasks when idle, so as to efficiently use system resources to improve performance. RunLoopWorkDistribution (😏)