A RunLoop, as the name suggests, is a program that does something in a loop as it runs.

RunLoop profile

When we create a terminal project, there is no RunLoop in the main function. So the program exits after running main.

An iOS application, by default, opens a RunLoop in the main thread, so an App can handle timer events, sliders, etc., without exiting immediately.

Each thread in an iOS project has a RunLoop object stored in a hash table with the thread as the key.

The main thread is created with RunLoop enabled by default, while other child threads are created with RunLoop([NSRunLoop currentRunLoop] or CFRunLoopGetCurrent()) for the first time.

Normally, a RunLoop follows the thread through its life and is destroyed when the thread terminates.

IOS provides a Foundation framework NSRunLoop API and a Core Foundation-based CFRunLoopRef API to use RunLoop. Where NSRunLoop is based on CFRunLoopRef to make a layer of OC encapsulation.

RunLoop structure

The basic structure of RunLoop in CFRunLoop source code is as follows:

A CFRunLoopRef is a __CFRunLoop structure that holds many mode-dependent members.

Where _currentMode is of type CFRunLoopModeRef, which is a structure pointer of type __CFRunLoopMode. RunLoop is used to indicate the running state of the RunLoop.

A RunLoop contains many modes. _commonModes is a mutable set that holds many modes. RunLoop can only select one Mode as the current state of RunLoop execution, which is _currentMode.

Mode is of the CFRunLoopMode type. CFRunLoopMode is obtained via typedf__CFRunLoopMode. __CFRunLoopMode contains source0 for touch events, source1 for system time capture, timers for timers, observers for listening to RunLoop status, and so on.

In addition, if the RunLoop needs to change the running state, it must exit the current Mode before entering the new Mode. RunLoop exits immediately if all sources, timers, and observers in the current Mode are present.

Common RunLoopMode have default modekCFRunLoopDefaultMode, tracking interface (such as: guarantee slip is not affected by other mode) UITrackingRunLoopMode. There’s also a kCFRunLoopCommonModes in the API but that’s not really a mode, it doesn’t exist in _commonModes, it’s just a flag.

RunLoop listeners listen for a number of RunLoop states:

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0), // About to enter runloop
    kCFRunLoopBeforeTimers = (1UL << 1), // The timer is about to be processed
    kCFRunLoopBeforeSources = (1UL << 2), // Source is about to be processed
    kCFRunLoopBeforeWaiting = (1UL << 5), // About to go to sleep
    kCFRunLoopAfterWaiting = (1UL << 6), // About to be awakened from hibernation
    kCFRunLoopExit = (1UL << 7), // Exit RunLoop
    kCFRunLoopAllActivities = 0x0FFFFFFFU // All states
};
Copy the code

Explore the execution flow of RunLoop

We can use Xcode’s LLDB to find the entry function of RunLoop by checking the function call stack with the bt command

Finding this function in the cfrunloop. c file, we see that it listens into RunLoop via __CFRunLoopDoObservers(RL, currentMode, kCFRunLoopEntry). This is followed by a call to the __CFRunLoopRun function, which encapsulates the event handling logic of RunLoop.

Let’s just focus on the __CFRunLoopRun main code: we find that there is a do-while() loop in this function that continues when retVal==0, and when retVal! = 0 returns the function CFRunLoopRunSpecific, which calls __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit); Exit the RunLoop.

The main processes in __CFRunLoopRun are described in the figure below. To sum up:

  1. Observers are first notified that Timers are about to be processed
  2. Notifying Observers that Sources is about to be processed
  3. Processing blocks
  4. Process source0 and, if so, blocks again
  5. If source1 is present, it jumps to handle_msg, if not, it notifies the listener that it is about to go to sleep
  6. Hibernation waits for messages to wake up the current thread
  7. If there is a wake up message, go to handle_MSG to process the timer, GCD, source1 information
  8. Process blocks again
  9. Gets the return value retVal
  10. Enter thedo-while()If theretVal == 0The cycle continues. Otherwise return toCFRunLoopRunSpecificFunction to exit RunLoop

The application of the RunLoop

Timer

When we use + (NSTimer *) scheduledTimerWithTimeInterval: (NSTimeInterval) interval repeats: (BOOL) repeats block (void (^) (NSTimer *timer))block; Create a timer and the page has a scrollView. Sliding the scrollView timer stops running. This is because runloop exists in NSDefaultRunLoopMode at the beginning. When the sliding event responds, runloop will enter UITrackingRunLoopMode mode to process the sliding event, and all timers will lose processing.

We can use + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer) *timer))block; Create a timer and place it in mode with an NSRunLoopCommonModes flag. The timer can work in mode stored in the _commonModes array. This solves the problem of the slip event conflicting with the timer timer event.

Thread to keep alive

When we create a thread, the thread does not have a RunLoop. The RunLoop is created in this thread when we first get the RunLoop.

So to create a thread that will always exist, we need to add a RunLoop to the thread that will not be reclaimed, so that the do-while() will always exist, so that the RunLoop will always have something to do, and retVal will not be anything other than 0.

Example code:

#import "SoCPermanentThread.h"
@interface SoCPermanentThread(a)

@property (nonatomic.strong) NSThread *thread;

@end

@implementation SoCPermanentThread

- (instancetype)init {
    if (self = [super init]) {
        self.thread = [[NSThread alloc] initWithBlock:^{
            CFRunLoopSourceContext context = {0};
            CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
            CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
            CFRelease(source);
            CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0 e10.false);
        }];
        [self.thread start];
    }
    return self;
}

- (void)executeTask:(SoCPermenantThreadTask)task {
    if(! _thread || ! task)return;
    [self performSelector:@selector(__task:) onThread:_thread withObject:task waitUntilDone:NO];
}

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

- (void)__task:(SoCPermenantThreadTask)task {
    task();
}

- (void)__stop {
    CFRunLoopStop(CFRunLoopGetCurrent());
    _thread = nil;
}

- (void)dealloc {
    [self stop];
}

@end
Copy the code

The code above is thread alive using Core Foundation. The important thing is to first add a Source to the RunLoop to make sure the RunLoop has something to do. CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false); The final BOOL argument to this method is returnAfterSourceHandled, with a value of flase indicating that no return is made when the function is handled, and true indicating that the return is made immediately after the source is handled.

conclusion

Based on Core Foundation API (open source Core Foundation), this paper briefly describes the basic concept of RunLoop and its calling process. Because NSRunLoop is OC encapsulation based on CFRunLoop, its principle and process are the same. Two other examples of using RunLoop were introduced.