Runloop

What is the Runloop

Sun Yuan runloop video explanation

As the name suggests, Runloop means to run a loop, doing something in a loop while the program is running.

Application scope:

  • Timer, PerformSelector
  • GCD Async Main Queue
  • Event response, gesture recognition, interface refresh
  • Network request
  • AutoreleasePool

Without Runloop, main exits after execution, but with Runloop, the program does not exit immediately, but stays running.

Runloop’s basic functions:

  • Keep the program running
  • Handle various events in the App (such as touch events, timer events, etc.)
  • Save CPU resources, improve program performance: work when you should work, rest when you should rest
  • .

Runloop object

IOS has two sets of apis to access and use RunLoop

  • Foundation: NSRunLoop
  • The Core Foundation: CFRunLoopRef

Both NSRunLoop and CFRunLoopRef represent RunLoop objects

NSRunLoop is a layer of OC encapsulation based on CFRunLoopRef

CFRunLoopRef is open source, open source address

Runloop with thread

  • 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 thread is created without a RunLoop object; the RunLoop 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 automatically acquired (created), and RunLoop is not enabled for the child thread by default

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

// Core Foundation
CFRunLoopGetCurrent(a);// Get the RunLoop object for the current thread
CFRunLoopGetMain(a);// Get the main thread RunLoop object

dispatch_async(dispatch_get_main_queue(), ^{
    // Child threads do not create runloops by default. They are created automatically when first fetched
    [NSRunLoop currentRunLoop];
});
Copy the code

Runloop-related classes

Five classes for RunLoop in Core Foundation

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef

CFRunLoopModeRef

  • CFRunLoopModeRef indicates the running mode of RunLoop

  • A RunLoop contains several Mode, each Mode and contains several Source0 / Source1 / Timer/Observer

  • Only one of these modes can be selected as currentMode when RunLoop starts

  • If you need to switch Mode, you can only exit the Loop and select another Mode to enter

    • Different groups of Source0 / Source1 / Timer/Observer can be separated, each other
  • If there is no any Source0 / Source1 / Timer Mode/Observer, RunLoop immediately exit

There are two common modes:

  • KCFRunLoopDefaultMode (NSDefaultRunLoopMode) : The default Mode of the App, in which the main thread usually runs

  • UITrackingRunLoopMode: interface tracing Mode, used by ScrollView to track touch sliding, ensuring that the interface sliding is not affected by other modes

Resources in Mode:

  • Source0
    • Touch event handling
    • performSelector:onThread:
  • Source1
    • Port-based communication between threads
    • System event capture, which is then handed to Source0 for processing
  • Timers
    • NSTimer
    • performSelector:withObject:afterDelay:
  • Observers
    • Used to listen for the status of RunLoop
    • UI refresh (BeforeWaiting)
    • Autorelease Pool (BeforeWaiting)

CFRunLoopObserverRef

We can listen on the Runloop and observe the status of the Runloop.

/* Run Loop Observer Activities */
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),						// Exit Runloop
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

// Add an Observer to listen for all RunLoop status
void observeRunLoopActicities(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
    switch (activity) {
        case kCFRunLoopEntry:
            NSLog(@"kCFRunLoopEntry");
            break;
        case kCFRunLoopBeforeTimers:
            NSLog(@"kCFRunLoopBeforeTimers");
            break;
        case kCFRunLoopBeforeSources:
            NSLog(@"kCFRunLoopBeforeSources");
            break;
        case kCFRunLoopBeforeWaiting:
            NSLog(@"kCFRunLoopBeforeWaiting");
            break;
        case kCFRunLoopAfterWaiting:
            NSLog(@"kCFRunLoopAfterWaiting");
            break;
        case kCFRunLoopExit:
            NSLog(@"kCFRunLoopExit");
            break;
        default:
            break; }} - (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    // kCFRunLoopCommonModes default modes include kCFRunLoopDefaultMode and UITrackingRunLoopMode
    / / create the Observer
    CFRunLoopObserverRef observer1 = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES.0, observeRunLoopActicities, NULL);
    // Add an Observer to the RunLoop
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer1, kCFRunLoopCommonModes);
    / / release
    CFRelease(observer1);
    
    / / create the Observer
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES.0And ^ (CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        switch (activity) {
            case kCFRunLoopEntry: {
                CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
                NSLog(@"kCFRunLoopEntry - %@", mode);
                CFRelease(mode);
                break;
            }
                
            case kCFRunLoopExit: {
                CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
                NSLog(@"kCFRunLoopExit - %@", mode);
                CFRelease(mode);
                break;
            }
                
            default:
                break; }});// Add an Observer to the RunLoop
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
    / / release
    CFRelease(observer);
}
Copy the code

The running logic of Runloop

Application of Runloop in real development

1. Control the thread lifecycle (thread survival)

@interface ViewController(a)
@property (strong.nonatomic) MJThread *thread;
@property (assign.nonatomic.getter=isStoped) BOOL stopped;
@end

void observeRunLoopActicities(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
    switch (activity) {
        case kCFRunLoopEntry:
            NSLog(@"kCFRunLoopEntry");
            break;
        case kCFRunLoopBeforeTimers:
            NSLog(@"kCFRunLoopBeforeTimers");
            break;
        case kCFRunLoopBeforeSources:
            NSLog(@"kCFRunLoopBeforeSources");
            break;
        case kCFRunLoopBeforeWaiting:
            NSLog(@"kCFRunLoopBeforeWaiting");
            break;
        case kCFRunLoopAfterWaiting:
            NSLog(@"kCFRunLoopAfterWaiting");
            break;
        case kCFRunLoopExit:
            NSLog(@"kCFRunLoopExit");
            break;
        default:
            break; }}@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    __weak typeof(self) weakSelf = self;
    
    self.stopped = NO;
    self.thread = [[MJThread alloc] initWithBlock:^{
        NSLog(@"%@----begin----"[NSThread currentThread]);
        
        // Add Source Timer Observer to RunLoop
        [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
        CFRunLoopObserverRef observer1 = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES.0, observeRunLoopActicities, NULL);
        // Add an Observer to the RunLoop
        CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer1, kCFRunLoopCommonModes);
        / / release
        CFRelease(observer1);
      	// Check if self has been freed
        while(weakSelf && ! weakSelf.isStoped) {NSLog(@" hahaha");
            // Re-enter the Runloop here
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        }
        
        NSLog(@"%@----end----"[NSThread currentThread]);
        
        // The run method of NSRunLoop cannot be stopped. It is specifically used to start an undestructible thread (NSRunLoop).
        // [[NSRunLoop currentRunLoop] run];
        /* it runs the receiver in the NSDefaultRunLoopMode by repeatedly invoking runMode:beforeDate:. In other words, This method effectively begins an infinite loop that processes data from the run loop's input sources and timers */
        
    }];
    [self.thread start];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}

// The task that the child thread needs to perform
- (void)test
{
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
}

- (IBAction)stop {
    // It cannot be called after it has stopped
    if (!self.stopped) {
      	// Stop is called by the child thread (waitUntilDone is set to YES, which means the child thread's code is finished)
        [self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:YES]; }}// Stop the RunLoop for the child thread
- (void)stopThread
{
    // Set the flag to NO
    self.stopped = YES;
    
    / / stop RunLoop
    CFRunLoopStop(CFRunLoopGetCurrent());
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
}

- (void)dealloc
{
    NSLog(@"%s", __func__);
    // If waitUntilDone is NO, bad memory access will occur,
    // Since the main thread and the child thread are executing simultaneously, the child thread has been destroyed while executing the code
    [self stop];
}

@end
Copy the code
Thread alive encapsulates OC
/** MJThread **/
@interface MJThread : NSThread
@end
@implementation MJThread
- (void)dealloc
{
    NSLog(@"%s", __func__);
}
@end

/** MJPermenantThread **/
@interface MJPermenantThread(a)
@property (strong.nonatomic) MJThread *innerThread;
@property (assign.nonatomic.getter=isStopped) BOOL stopped;
@end

@implementation MJPermenantThread
#pragma mark - public methods
- (instancetype)init
{
    if (self = [super init]) {
        self.stopped = NO;
        
        __weak typeof(self) weakSelf = self;
        
        self.innerThread = [[MJThread alloc] initWithBlock:^{
            [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
            
            while(weakSelf && ! weakSelf.isStopped) { [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDatedistantFuture]]; }}]; [self.innerThread start];
    }
    return self;
}

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

- (void)executeTask:(MJPermenantThreadTask)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
{
    NSLog(@"%s", __func__);
    
    [self stop];
}

#pragma mark - private methods
- (void)__stop
{
    self.stopped = YES;
    CFRunLoopStop(CFRunLoopGetCurrent());
    self.innerThread = nil;
}

- (void)__executeTask:(MJPermenantThreadTask)task
{
    task();
}

@end
Copy the code
Thread preservation encapsulates C language
/** MJThread **/
@interface MJThread : NSThread
@end
@implementation MJThread
- (void)dealloc
{
    NSLog(@"%s", __func__);
}
@end

/** MJPermenantThread **/
@interface MJPermenantThread(a)
@property (strong.nonatomic) MJThread *innerThread;
@end

@implementation MJPermenantThread
#pragma mark - public methods
- (instancetype)init
{
    if (self = [super init]) {
        self.innerThread = [[MJThread alloc] initWithBlock:^{
            NSLog(@"begin----");
            
            // Create the context (to initialize the structure)
            CFRunLoopSourceContext context = {0};
            
            / / create the source
            CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
            
            // Add source to Runloop
            CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
            
            / / the destruction of the source
            CFRelease(source);
            
            / / start
            CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0 e10.false);
            
// while (weakSelf && ! weakSelf.isStopped) {
// // third parameter: returnAfterSourceHandled, set to true to exit the loop after the source has been handled
/ / CFRunLoopRunInMode (kCFRunLoopDefaultMode, 1.0 e10, true);
/ /}
            
            NSLog(@"end----");
        }];
        
        [self.innerThread start];
    }
    return self;
}

- (void)executeTask:(MJPermenantThreadTask)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
{
    NSLog(@"%s", __func__);
    
    [self stop];
}

#pragma mark - private methods
- (void)__stop
{
    CFRunLoopStop(CFRunLoopGetCurrent());
    self.innerThread = nil;
}

- (void)__executeTask:(MJPermenantThreadTask)task
{
    task();
}

@end
Copy the code

2. Solve the problem that NSTimer stops working when sliding

static int count = 0;
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
		NSLog(@"%d", ++count);
}];

// NSDefaultRunLoopMode and UITrackingRunLoopMode are the real modes
// NSRunLoopCommonModes are not real modes, they are just tokens
// Timer can work in modes stored in the _commonModes array
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
Copy the code

3. Monitoring applications are stuck

You can add an Observer to the main RunLoop to monitor the lag by listening for the time it takes for the RunLoop state to change

// Create an observer
CFRunLoopObserverContext context = {0,(__bridge void*)self.NULL.NULL};
runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,kCFRunLoopAllActivities,YES.0,&runLoopObserverCallBack,&context);
// Add the observer to runloop's common mode
CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);

// Create a child thread monitor
dispatch_async(dispatch_get_global_queue(0.0), ^ {// The child thread opens a continuous loop for monitoring
    while (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 delays
            if (runLoopActivity == kCFRunLoopBeforeSources || runLoopActivity == kCFRunLoopAfterWaiting) {
                // Put the code that reports the stack information to the server here
            } //end activity
        }// end semaphore wait
        timeoutCount = 0;
    }// end while
});
Copy the code

The interview questions

  1. Talk about RunLoop. Is that useful in the project?

  2. Implementation logic inside Runloop?

  3. How does runloop relate to threads?

    Threads have a one-to-one relationship with runloop. Runloop is enabled by default for the main thread, and runloop is not enabled when the child thread is created, but when it first obtains a Runloop.

  4. Relation between Timer and Runloop?

    Runloop controls when the timer is executed.

  5. If I add an NSTimer that responds every 3 seconds, when I drag the tableview, the timer may not respond. What can I do?

    Add timer to Runloop and set it to commonMode

    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    Copy the code
  6. How does runloop respond to user actions?

    1. Source1 captures user events
    2. It is then handed to Source0 for processing
  7. Talk about several states of runLoop

    • KCFRunLoopEntry // About to enter Runloop
    • KCFRunLoopBeforeTimers // Timer is about to be processed
    • KCFRunLoopBeforeSources // About to process Source
    • KCFRunLoopBeforeWaiting // Is about to go to sleep
    • KCFRunLoopAfterWaiting // Just woke up from sleep
    • KCFRunLoopExit // About to exit Runloop
  8. What is the mode of runloop?

    Resources in different modes (source and timer) are isolated, and only resources in one mode need to be processed to improve fluency and save resources.