Introduction to RunLoop

Runloops are part of the basic infrastructure associated with threads. A RunLoop is an event processing loop that you use to schedule work and coordinate the receipt of incoming events. The purpose of RunLoop is to keep the thread busy when there is work to be done and to put it to sleep when there is no work to be done. Until the user closes the program.

  • A RunLoop, as the name suggests, is actually onedo.. while..The following is pseudocode that briefly describes the RunLoop logic:
int main(int argc,char * argv[]){
    bool running = 0;
    do {
        // Hibernate to wait for tasks
        // There is a task, execute the task, finish continue to sleep wait
    }while(0 == running);
    return 0;
}
Copy the code
  • The basic function of RunLoop is to keep the program running; Event handling in the program (such as touch events, timer events, etc.); Save CPU resources, improve program performance and so on.

Second, RunLoop exploration

1. The RunLoop object

There are two apis for using RunLoop in iOS. Is a set ofFoundationNSRunLoop; The other one isCore FoundationCFRunLoopRef.NSRunLoopCFRunLoopRefBoth represent RunLoop objects, butNSRunLoopIs based onCFRunLoopRefA layer of OC packaging.

CF open source download address.

1.1 Relationship between Runloops and threads

  • Each thread has a unique RunLoop object that corresponds to it;
  • RunLoop is stored in a global dictionary, thread for Key, RunLoop for Value;
  • The thread is created without a RunLoop object. The RunLoop is created the first time it retrieves it. (This means that when you retrieve the current thread’s RunLoop and find that no RunLoop has been created, it will automatically create a RunLoop for the current thread);
  • The RunLoop is destroyed at the end of the thread;
  • The main thread RunLoop is inmainRunLoop is not enabled by default for child threads.

1.2 How to obtain a RunLoop object

In the Foundation.

[NSRunLoop currentRunLoop]; // Get the RunLoop object for the current thread
[NSRunLoop mainRunLoop]; // Get the main thread RunLoop object
Copy the code

The Core Foundation of

CFRunLoopGetCurrent(a);// Get the RunLoop object for the current thread
CFRunLoopGetMain(a);// Get the main thread RunLoop object
Copy the code

2. Runloop-related classes

Core Foundation classes for RunLoop

The name of the class introduce
CFRunLoopRef RunLoop object
CFRunLoopModeRef RunLoop Running mode
CFRunLoopSourceRef input sources
CFRunLoopTimerRef Timer sources
CFRunLoopObserverRef Listen for RunLoop status changes

Their relationship is roughly as follows: After creating a RunLoop (CFRunLoopRef), the default running mode is mode (CFRunLoopModeRef). You can also specify a running mode for the RunLoop. A Timer Sources (CFRunLoopTimerRef) or input Sources (CFRunLoopSourceRef) event must also be in run mode, otherwise the RunLoop exits. Listening for (CFRunLoopObserverRef) RunLoop mode changes is required

1.1 CFRunLoopRef

CFRunLoopRef:

typedef struct __CFRunLoop * CFRunLoopRef;
struct __CFRunLoop {
    pthread_t _pthread; / / thread
    CFMutableSetRef _commonModes; // Common mode
    CFMutableSetRef _commonModeItems; 
    CFRunLoopModeRef _currentMode; // Current mode
    CFMutableSetRef _modes; // Set of modes of type CFRunLoopModeRef. };Copy the code

1.2 CFRunLoopModeRef

CFRunLoopModeRef

typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {
    CFStringRef _name; // The schema name
    CFMutableSetRef _sources0; // CFRunLoopSourceRef Input source 0 set
    CFMutableSetRef _sources1; // CFRunLoopSourceRef Input source 1 set
    CFMutableArrayRef _observers; // Listen on events
    CFMutableArrayRef _timers; // CFRunLoopTimerRef set of timer sources. }Copy the code
  • _sources0: Touch event handling;
  • _sources1: Inter-thread communication based on Port, system event capture;
  • _timers: The essence isNSTimer;
  • _observers: Used to listen for RunLoop status, such as listening to sleep, refreshing UI before sleep, Autorelease pool cleaning and releasing objects before sleep, etc.

CFRunLoopModeRef represents the running mode of RunLoop. A RunLoop contains several modes, each of which contains _sources0, _sources1, _observers, and _timers. Only one of these modes can be selected as currentMode when starting RunLoop. To switch mode, you must exit the current RunLoop and re-select mode before starting. Note here that the RunLoop will stop running if _sources0, _sources1, _observers, and _timers are all empty.

All modes of CFRunLoopModeRef:

mode Name Description
Default NSDefaultRunLoopMode (Cocoa) kCFRunLoopDefaultMode (Core Foundation) The default mode is the mode used for most operations. Most of the time, you should use this mode to start the run loop and configure the input source
Connection NSConnectionReplyMode (Cocoa) Cocoa uses this pattern to combineNSConnectionObject monitor reply, this mode is used less
Modal NSModalPanelRunLoopMode (Cocoa) Cocoa uses this pattern to identify events for modal panels
Event tracking NSEventTrackingRunLoopMode (Cocoa) Cocoa uses this pattern to limit incoming event loops and other types of user interface trace loops as the interface scrolls.
Common modes NSRunLoopCommonModes (Cocoa) kCFRunLoopCommonModes (Core Foundation) This is a configurable common pattern. Combine an input source with this mode and also group it with each mode. Cocoa applications, this collection includes default, modal, and event tracking default modes. Including Core Foundation, which was originally the default mode. You can add custom patterns to useCFRunLoopAddCommonModeSet of functions.

CFRunLoopModeRef uses two modes:

  • kCFRunLoopDefaultMode(NSDefaultRunLoopMode) : The default Mode of the App, in which the main thread is normally run.
  • UITrackingRunLoopModeInterface tracing mode, used forUIScrollViewTrack touch sliding and switch to this mode when the interface is scrolling to ensure that the interface is not affected by other modes when sliding.

1.3 CFRunLoopObserverRef

CFRunLoopObserverRef Main member variables of the underlying structure:

typedef struct __CFRunLoopObserver * CFRunLoopObserverRef;
struct __CFRunLoopObserver {
    CFOptionFlags _activities; // The activity status that RunLoop listens to. };/* The active state that RunLoop listens to */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0), // About to enter RunLoop
    kCFRunLoopBeforeTimers = (1UL << 1),  // 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),  // Just woke up from hibernation
    kCFRunLoopExit = (1UL << 7),         // 即将退出 RunLoop
    kCFRunLoopAllActivities = 0x0FFFFFFFU // Contains all of the above states
};
Copy the code

3. RunLoop Runs logic

If we want to explore the underlying logic, we need to know where RunLoop starts in the source code. We can use the screen click event listening method under the ViewController:Then tap in the command windowbtCommand, we can get information about the underlying call, and find that the entry function of RunLoop is calledCFRunLoopRunSpecific:We learn fromCoreFoundationFound in the source codeCFRunLoopRunSpecificMethod, because the code is too abstract, we peel the key code from the source code:

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */.// Notify all observers that the program is about to enter RunLoop
	if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    // Specific things to do
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
	// Notify all Observers that they are about to exit RunLoop
    if(currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit); .return result;
}
Copy the code

__CFRunLoopRun is the way RunLoop handles things. We can strip the key code from the source code (stripping the key code from the source code is a better way to understand it. After stripping the key code, it will be very clear when looking back at the RunLoop logic diagram. You can download the source code for yourself and try it out) :

static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
    int32_t retVal = 0;
    do {
        __CFRunLoopUnsetIgnoreWakeUps(rl);
        // Inform all observers that Timers are about to be processed
        if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        // Notify all observers that Sources is about to be processed
        if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
        / / processing Blocks
	    __CFRunLoopDoBlocks(rl, rlm);
        / / Sources0 processing
        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
        if (sourceHandledThisLoop) {
            / / processing Blocks
            __CFRunLoopDoBlocks(rl, rlm);
        }
        
        Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
        if(MACH_PORT_NULL ! = dispatchPort && ! didDispatchPortLastTime) { msg = (mach_msg_header_t *)msg_buffer;
            // Check whether there is a Sources1
            if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
            // If there is Sources1, jump
                goto handle_msg;
            }
        }
        didDispatchPortLastTime = false;
        // Notify all observers that hibernation is imminent
        if(! poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);// Start sleep
        __CFRunLoopSetSleeping(rl);
        do {
             // Wait for another message to wake up another thread. If it does not wake up, it will stay blocked. If it wakes up, it will go down
            // The CPU does not allocate resources to the dormant thread
            // __CFRunLoopServiceMachPort mach_msg is a kernel level API that hibernates the thread if there is no message or wakes it up if there is a message
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
        } while (1);
        // Stop sleep
        __CFRunLoopUnsetSleeping(rl);
        // Notify all observers to end hibernation
        if(! poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting); handle_msg:// The Timer wakes up
            if(modeQueuePort ! = MACH_PORT_NULL && livePort == modeQueuePort) {CFRUNLOOP_WAKEUP_FOR_TIMER(a);/ / handle the Timer
                if(! __CFRunLoopDoTimers(rl, rlm,mach_absolute_time())) { __CFArmNextTimerInMode(rlm, rl); }}/ / same as above
            else if(rlm->_timerPort ! = MACH_PORT_NULL && livePort == rlm->_timerPort) {CFRUNLOOP_WAKEUP_FOR_TIMER(a);if(! __CFRunLoopDoTimers(rl, rlm,mach_absolute_time())) { __CFArmNextTimerInMode(rlm, rl); }}// Wake up by GCD
            else if (livePort == dispatchPort) {
                // Handle gCD-related issues
                // GCD has its own processing logic. Many things do not rely on RunLoop, but only from the child thread back to the main thread, such as the child thread after the main thread to refresh the UI.
                __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            } else { // Wake up by Source1
                / / processing Source1
                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);
                    CFAllocatorDeallocate(kCFAllocatorSystemDefault, reply); }}/ / processing Blocks
            __CFRunLoopDoBlocks(rl, rlm);
        // Set the return value
        if (sourceHandledThisLoop && stopAfterHandle) {
            retVal = kCFRunLoopRunHandledSource;
            } else if (timeout_context->termTSR < mach_absolute_time()) {
                retVal = kCFRunLoopRunTimedOut;
        } else if (__CFRunLoopIsStopped(rl)) { // Stop RunLoop and exit while
                __CFRunLoopUnsetStopped(rl);
            retVal = kCFRunLoopRunStopped;
        } else if (rlm->_stopped) {  // Stop RunLoop and exit while
            rlm->_stopped = false;
            retVal = kCFRunLoopRunStopped;
        } else if(__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) { retVal = kCFRunLoopRunFinished; }}while (0 == retVal);
    return retVal;
}
Copy the code

Third, RunLoop application

1. Control the thread lifecycle (thread survival)

In iOS development, sometimes something takes a long time to block the main thread, and we usually run it in child threads to prevent the interface from stalling. If we are constantly executing tasks in a child thread, the frequent creation and destruction of threads will be a waste of resources. This is where RunLoop is used to keep threads alive for long periods of time. This is called thread-alive or permanent thread. However, threads can only execute one task at a time and exit when they are finished: easyThread. h and easyThread. m

// EasyThread.h
@interface EasyThread : NSThread
@end
// EasyThread.m
@implementation EasyThread

- (void)dealloc {
    NSLog(@"%s", __func__);
}

@end
Copy the code

ViewController.m

- (void)viewDidLoad {
    [super viewDidLoad];
    EasyThread *thread = [[EasyThread alloc] initWithTarget:self selector:@selector(doSomething) object:NULL];
    [thread start];
}

- (void)doSomething {
    NSLog(@"do");
}
Copy the code

Print logs:

2021-04-19 08:15:56.994035+0800 StudyIOSApp[65423:10339378] do
2021-04-19 08:15:56.994341+0800 StudyIOSApp[65423:10339378] -[EasyThread dealloc]
Copy the code

The thread will be destroyed when something is done. How can we use a RunLoop to keep a thread alive? We now add the RunLoop method to the doSomething method above:

 - (void)doSomething {
    NSLog(@"%s", __func__);
    [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
    // run is the start method of RunLoop
    [[NSRunLoop currentRunLoop] run];
    // Check to see if the method has finished executing
    NSLog(@"ok");
}
Copy the code

The project adds the above code and finds that it does not execute to NSLog(@” OK “); , the RunLoop will always be stuck in the upper part. But why is the run method stuck here after it has been executed?

The method name introduce
run unconditional
runUntilDate Set a time limit
runMode:beforeDate: In a particular mode

The three methods are introduced and summarized as follows:

  • The first: unconditional entry is the easiest, but least recommended. This causes the thread to go into an infinite loop, which is bad for controlling the RunLoop, and the only way to kill the RunLoop is to kill it;
  • Second: if we set the timeout, the RunLoop will end after the event has been processed or after the timeout, at which point we can choose to restart the RunLoop. This method is superior to the former one;
  • Third: This is relatively the best way to start, and we can specify which mode RunLoop runs in compared to the second way.

At this point we can design:

// The thread is alive
__weak typeof(self)weakSelf = self;
self.thread = [[NSThread alloc] initWithBlock:^{
    // Keep RunLoop running by adding Source1
    [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
    // Use a while loop to keep threads alive
    while(weakSelf && weakSelf.isStoped) {
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDatedistantFuture]]; }}];Copy the code

If you want to turn off the destruction thread you can use the Core Foundation framework CFRunLoopStop(CFRunLoopGetCurrent()); This method. With this in mind, we can do a thread alive wrapper: qzhThread.h

@interface QZHThread : NSObject
/ / run
- (void)run;
// The thread executes the task
- (void)executeTask:(void(^) (void))task;
/ / stop- (void)stop;
@end
Copy the code

QZHThread.m

@interface QZHThread(a)

// a thread
@property(nonatomic ,strong) NSThread *thread;
// Whether to stop RunLoop
@property(nonatomic ,assign) BOOL    isStoped;

@end

@implementation QZHThread

#pragmaMark - --------- Open method ---------
- (instancetype)init
{
    self = [super init];
    if (self) {
        self.isStoped = NO;
        // The thread is alive
        __weak typeof(self)weakSelf = self;
        self.thread = [[NSThread alloc] initWithBlock:^{
            // Keep RunLoop running by adding Source1
            [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
            // Use a while loop to keep threads alive
            while(weakSelf && ! weakSelf.isStoped) { [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDatedistantFuture]]; }}]; }return self;
}

// Start a thread
- (void)run {
    if (!self.thread) return;
    [self.thread start];
}

// The thread executes the task
- (void)executeTask:(void(^) (void))task {
    if(! task)return;
    if (!self.thread) return;
    // Execute the task in the current thread
    [self performSelector:@selector(__executeTask:) onThread:self.thread withObject:task waitUntilDone:NO];
}

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

#pragmaMark - --------- Private method ---------
// Destroy destructor methods
- (void)dealloc {
    NSLog(@"%s",__func__);
    [self stop];
}

/ / stop RunLoop
- (void)__stopRunLoop {
    self.isStoped = YES;
    CFRunLoopStop(CFRunLoopGetCurrent());
    self.thread = nil;
}

// Execute the task
- (void)__executeTask:(void(^) (void))task {
    task();
}

@end

Copy the code

Used in viewController.m:

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.thread = [[QZHThread alloc] init];
    [self.thread run];
}

- (void)doSomething {
    NSLog(@ "% @"[NSThread currentThread]);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    __weak typeof(self) weakSelf = self;
    [self.thread executeTask:^{
        [weakSelf doSomething];
    }];
}

@end
Copy the code

Click on the screen to print the result:

The 2021-04-19 09:06:20. 156588 + 0800 StudyIOSApp [67303-10393065] < NSThread: 0x600000474EC0 >{Number = 7, name = (null)} 2021-04-19 09:06:22.003031+0800 StudyIOSApp[67303:10393065] <NSThread: 0x600000474EC0 >{Number = 7, name = (null)} 2021-04-19 09:06:22.371167+0800 StudyIOSApp[67303:10393065] <NSThread: 0x600000474EC0 >{Number = 7, name = (null)} 2021-04-19 09:06:22.726126+0800 StudyIOSApp[67303:10393065] <NSThread: 0x600000474ec0>{number = 7, name = (null)}Copy the code

2. Handle the failure of NSTimer when sliding

Generally, the timer in use is in NSDefaultRunLoopMode by default. However, when the page is sliding, the timer in use will be in UITrackingRunLoopMode, so the timer will stop, stop scrolling, the timer will continue to run. How do we solve it? We add timer to NSRunLoopCommonModes, The default NSRunLoopCommonModes are equivalent to NSDefaultRunLoopMode combined with UITrackingRunLoopMode.

NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerEvent) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
Copy the code

3. Use RunLoop to monitor application lag

If the thread of the RunLoop takes too long to execute the pre-sleep method and is unable to go to sleep, or if the thread wakes up and takes too long to receive messages and is unable to proceed to the next step, the thread is considered blocked. If the thread is the main thread, it will appear to be stuck. Therefore, we can determine whether the thread is stuck by listening to the duration of two states: kCFRunLoopBeforeSources and kCFRunLoopAfterWaiting. The general idea is as follows:

  • To create aCFRunLoopObserverContextThe observer;
  • Add the created Observer Observer to the main thread RunLoopNSRunLoopCommonModesObservation in mode;
  • Add + 1 to the semaphore whenever Observer notifications are listened on;
  • Create a persistent child thread that uses the semaphore to monitor the main thread RunLoop status and set the waiting time of the semaphore.
  • If the child thread is not awakened after the wait time, it is considered to have stalled.

If you are interested, you can refer to the blog to implement feature class encapsulation “ios uses RunLoop principle to monitor Caton”.

4. Etc.

Reference documentation

  1. Threading Programming Guide