Start RunLoop

To obtain the runloop of the current thread, run [NSRunLoop currentRunLoop] or CFRunLoopGetCurrent(). According to Apple documentation, there are three ways to start a Runloop:

- (void)run; - (void) runUntilDate (limitDate NSDate *); - (void)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;Copy the code

The runloop exits immediately if there is no input source or timer attached to the runloop, regardless of which of the three methods the runloop is started. (1) With the first startup mode, runloop keeps running, processing data from the input source during this time, and repeatedly calling the runMode:beforeDate: method in NSDefaultRunLoopMode; (2) With the second startup method, you can set the timeout period, and runloop will run until the timeout period is reached, during which time runloop will process data from the input source. The runMode:beforeDate: method is also repeatedly called in NSDefaultRunLoopMode; (3) Using the third startup method, the runloop will run once and exit when the timeout expires or the first input source is processed.

A review of Apple documentation shows that the first two boot methods repeatedly call the runMode:beforeDate: method. Interested children’s shoes can be verified, after verification you will find, in fact, redundant. However, the first thing to do is to create a new NSRunLoop subclass and override the runMode:beforeDate: method. If you do this, you’ll find that it doesn’t work. Either of the above methods is implemented in a class called NSRunloopcompromise. Either of the above methods is implemented in a class called NSRunloopcompromise. Either of the above methods is implemented in a class called NSRunloopCompromise.

NSMachPort *_port; //global NSRunLoop *_theRL; NSThread *_thread; - (void)viewDidLoad { [super viewDidLoad]; _thread = [[NSThread alloc] initWithTarget:self selector:@selector(createRunLoopInNewThread) object:nil]; [_thread setName:@"com.xindong.thread"]; [_thread start]; } - (void)createRunLoopInNewThread { _theRL = [NSRunLoop currentRunLoop]; _port = (NSMachPort *)[NSMachPort port]; / / add a port as the input source [_theRL addPort: _port forMode: NSDefaultRunLoopMode]; [_theRL run]; // [_theRL runUntilDate:[NSDate distantFuture]]; // [_theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; }Copy the code
@implementation NSRunLoop (Hook) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self _swizzleImpWithOrigin:@selector(runMode:beforeDate:) swizzle:@selector(xd_runMode:beforeDate:)]; }); } + (void)_swizzleImpWithOrigin:(SEL)originSelector swizzle:(SEL)swizzleSelector { Class _class = [self class]; Method originMethod = class_getInstanceMethod(_class, originSelector); Method swizzleMethod = class_getInstanceMethod(_class, swizzleSelector); IMP originIMP = method_getImplementation(originMethod); IMP swizzleIMP = method_getImplementation(swizzleMethod); BOOL add = class_addMethod(_class, originSelector, swizzleIMP, method_getTypeEncoding(swizzleMethod)); if (add) { class_addMethod(_class, swizzleSelector, originIMP, method_getTypeEncoding(originMethod)); } else { method_exchangeImplementations(originMethod, swizzleMethod); } } - (BOOL)xd_runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate { NSThread *thread = [NSThread currentThread];  If ([thread.name isEqualToString:@"com.xindong.thread"]) {if ([thread.name isEqualToString:@"com.xindong.thread"]) {if ([thread.name isEqualToString:@"com.xindong.thread"]) { NSLog(@"runloop+hook: com.xindong.thread thread \n\n"); return YES; } NSLog(@"runloop+hook: other possibly unknown threads %@\n\n", thread.name);} NSLog(@"runloop+hook: other possibly unknown threads %@\n\n", thread.name); return [self xd_runMode:mode beforeDate:limitDate]; } @endCopy the code

The results are as follows:





Console. PNG




Thread stack. PNG

OK, verification successful. Now that you’ve looked at the three ways runloop can start, how do YOU exit Runloop correctly?

Exit RunLoop

At present, I think of the following methods to try :(1) remove input sources or timer; (2) Set the timeout period or add a timing source; (3) Force exit thread; (4) Stop the runloop with the CFRunLoopStop method. To see if all four methods work, read on…

  • The first boot moderun

    1. According to the documentation, if you want to exit runloop, you should not use the first startup method to start Runloop. If no input sources or timers are attached to the run loop, this method exits immediately. If the runloop has no input sources or attached timer, the runloop will exit. If the runloop has no input sources or attached timer, the runloop will exit. Let’s try it. The code looks like this:

    NSMachPort *_port; //global NSRunLoop *_theRL; NSThread *_thread; - (void)viewDidLoad {// start a new thread _thread = [[NSThread alloc] initWithTarget:self selector:@selector(createRunLoopInNewThread) object:nil]; [_thread setName:@"com.xindong.thread"]; [_thread start]; } - (void)createRunLoopInNewThread { _theRL = [NSRunLoop currentRunLoop]; _port = (NSMachPort *)[NSMachPort port]; / / add a port to runloop as input source, ensure that can process the message at any time [_theRL addPort: _port forMode: NSDefaultRunLoopMode]; // This method creates a timer in the runloop of the current thread, And execute selector in the current thread [self performSelector:@selector(excuteInNewThread:) withObject:@" Param1 "afterDelay:2]; [self performSelector:@selector(excuteInNewThread:) withObject:@"param2" afterDelay:3]; [_theRL run]; // If the current thread's runloop does not exit, the code after '[_theRL run]' will not be executed. NSLog(@"runloop exited "); // This is executed only when runloop exits. This can be verified by registering runloop observers, but the code will not be posted here. Please check out the code in the demo. } - (void)excuteInNewThread:(NSString *)param { NSLog(@"%@", param); / / removes the current thread port in the runloop [_theRL removePort: _port forMode: NSDefaultRunLoopMode]; }Copy the code

    Console output:



    image.png

    Note: The excuteInNewThread: method has been executed, indicating that runloop has been successfully enabled, otherwise the method will not execute. In the excuteInNewThread: method, port is removed from the current runloop and the runloop exits after the param1 and param2 events are processed (i.e., after the two timer fires). While it is possible to exit the runloop, Apple does not recommend doing so because it is possible to add some input sources to the current thread’s runloop, so manually removing the input source or timer does not guarantee that the runloop will exit.

    2. As we know, runloop receives input events from two different sources: input source and timer source. Input sources deliver asynchronous events, usually messages from other threads or programs. The timing source delivers synchronous events that occur at a specific time or at repeated time intervals. Since the run startup mode cannot set a timeout, we will test it by adding a timing source to runloop and modify the above code slightly:

    - (void)createRunLoopInNewThread { _theRL = [NSRunLoop currentRunLoop]; / / note: It repeats parameters to be set to ` NO `. NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval: 1.5 repeats: NO block: ^ (NSTimer * _Nonnull timer) { NSLog(@"timer block"); }]; / / add a timing sources [_theRL addTimer: timer forMode: NSDefaultRunLoopMode]; [_theRL run]; // If the current thread's runloop does not exit, the code after '[_theRL run]' will not be executed. NSLog(@"runloop exited "); // This is executed only when runloop exits. This can be verified by registering runloop observers, but the code will not be posted here. Please check out the code in the demo. }Copy the code

    The console output is as follows:



    image.png

    After the timer is triggered, the runloop exits because there is no source to monitor. If you don’t want runloop to exit, simply set the repeats parameter to YES for creating the timer. However, there is an obvious drawback to this setup: Runloops wake up from hibernation every 1.5 seconds, causing unnecessary consumption of CPU resources and system memory. Print by registering a Runloop observer on the console:



    image.png

    3. Let’s try forcing the thread to exit and see if the current thread’s runloop exits as well. The excuteInNewThread: method is modified as follows:

    - (void)createRunLoopInNewThread { _theRL = [NSRunLoop currentRunLoop]; _port = (NSMachPort *)[NSMachPort port]; / / add a port to runloop as input source, ensure that can process the message at any time [_theRL addPort: _port forMode: NSDefaultRunLoopMode]; // This method creates a timer in the runloop of the current thread, And execute selector in the current thread [self performSelector:@selector(excuteInNewThread:) withObject:@" Param1 "afterDelay:2]; [self performSelector:@selector(excuteInNewThread:) withObject:@"param2" afterDelay:3]; [_theRL run]; // If the current thread's runloop does not exit, the code after '[_theRL run]' will not be executed. NSLog(@"runloop exited "); // This is executed only when runloop exits. This can be verified by registering runloop observers, but the code will not be posted here. Please check out the code in the demo. } - (void)excuteInNewThread:(NSString *)param { NSLog(@"%@", param); [NSThread exit]; }Copy the code

    Console output:



    image.png

    When excuteInNewThread: excuteInNewThread: excuteInNewThread: excuteInNewThread: excuteInNewThread: excuteInNewThread: excuteInNewThread: excuteInNewThread: excuteInNewThread: If it does not execute, the current thread’s runloop does not exit, and some resources on the thread stack are not released, causing a memory leak, as shown below. Therefore, this method is not desirable.



    image.png

    4. At this point, the last exit method left is the CFRunLoopStop function in Core Foundation. Can I exit the runloop using CFRunLoopStop? Let’s test the code with a few modifications:

    - (void)createRunLoopInNewThread {// Register runloop observer static CFRunLoopObserverRef _observer; RegisterRunLoopObserver(kCFRunLoopAllActivities, _observer, 0, kCFRunLoopDefaultMode, (__bridge void*)self, RunLoopCallBack); _theRL = [NSRunLoop currentRunLoop]; _port = (NSMachPort *)[NSMachPort port]; [_theRL addPort:_port forMode:NSDefaultRunLoopMode]; [self performSelector:@selector(excuteInNewThread:) withObject:@"param1" afterDelay:2]; [self performSelector:@selector(excuteInNewThread:) withObject:@"param2" afterDelay:3]; [_theRL run]; // If the current thread's runloop does not exit, the code after '[_theRL run]' will not be executed. NSLog(@"runloop exited "); // This is executed only when runloop exits. This can be verified by registering runloop observers, but the code will not be posted here. Please check out the code in the demo. } - (void)excuteInNewThread:(NSString *)param { NSLog(@"%@", param); CFRunLoopStop(CFRunLoopGetCurrent()); }Copy the code

    Console output:



    image.png

    The runloop observer’s callback tells us that the runloop does exit each time the CFRunLoopStop method is executed, but since the run startup repeatedly calls the runMode:beforeDate: method, This will restart runloop after exiting the current runloop, so the NSLog(@” Runloop exited “) method in the above code will not execute. Similarly, the second method, runUntilDate: starting the runloop, cannot be exited using the CFRunLoopStop method.


  • Second boot moderunUntilDate:

    Starting this way, you can exit runloop by setting a timeout. In addition, it is similar to the first startup method, Run, which is not described here.


  • Third boot moderunMode:beforeDate:

    Starting this way, the runloop runs once and exits when the timeout expires or the first input source is processed. The test code is as follows:

    - (void)createRunLoopInNewThread {// Register runloop observer static CFRunLoopObserverRef _observer; RegisterRunLoopObserver(kCFRunLoopAllActivities, _observer, 0, kCFRunLoopDefaultMode, (__bridge void*)self, RunLoopCallBack); _theRL = [NSRunLoop currentRunLoop]; _port = (NSMachPort *)[NSMachPort port]; [_theRL addPort:_port forMode:NSDefaultRunLoopMode]; [_theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; // If the current thread's runloop does not exit, the code after '[_theRL run]' will not be executed. NSLog(@"runloop exited "); // This is executed only when runloop exits. This can be verified by registering runloop observers, but the code will not be posted here. Please check out the code in the demo. } #pragma mark - Touch - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { UIColor *color = self.view.backgroundColor; self.view.backgroundColor = color == [UIColor redColor] ? [UIColor yellowColor] : [UIColor redColor]; / / the communication between threads (this is the main thread) [self performSelector: @ the selector (communicateToNewThreadFromMainThread) onThread: _thread withObject:nil waitUntilDone:NO]; } - (void)communicateToNewThreadFromMainThread { NSLog(@"communicate successfully\n\n"); Com.xindong.thread. The first input source event is processed.Copy the code

    The console output is as follows:



    image.png

    When we touch screen, communicateToNewThreadFromMainThread method implemented, namely the input source event is processed, then the runloop exit. If we want to control the exit timing of runloop instead of exiting after processing an input source event, we need to repeatedly call runMode:beforeDate: as described in the Apple documentation:

    BOOL shouldKeepRunning = YES; // global
    NSRunLoop *theRL = [NSRunLoop currentRunLoop];
    while (shouldKeepRunning && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);Copy the code

    We then changed the code so that we could control the exit of runloop as follows:

    - (void)createRunLoopInNewThread {// Register runloop observer static CFRunLoopObserverRef _observer; RegisterRunLoopObserver(kCFRunLoopAllActivities, _observer, 0, kCFRunLoopDefaultMode, (__bridge void*)self, RunLoopCallBack); _theRL = [NSRunLoop currentRunLoop]; _port = (NSMachPort *)[NSMachPort port]; [_theRL addPort:_port forMode:NSDefaultRunLoopMode]; while (shouldKeepRunning && [_theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]); // If the current thread's runloop does not exit, the code after '[_theRL run]' will not be executed. NSLog(@"runloop exited "); // This is executed only when runloop exits. This can be verified by registering runloop observers, but the code will not be posted here. Please check out the code in the demo. } #pragma mark - Touch - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { UIColor *color = self.view.backgroundColor; self.view.backgroundColor = color == [UIColor redColor] ? [UIColor yellowColor] : [UIColor redColor]; / / the communication between threads (this is the main thread) [self performSelector: @ the selector (communicateToNewThreadFromMainThread) onThread: _thread withObject:nil waitUntilDone:NO]; } - (void)communicateToNewThreadFromMainThread { NSLog(@"communicate successfully\n\n"); Com.xindong. Thread [self quitRunLoop]; } - (void)quitRunLoop { shouldKeepRunning = NO; CFRunLoopStop(CFRunLoopGetCurrent()); }Copy the code

    Starting and exiting the Runloop in this manner causes no memory leaks, no memory growth, and the timing of the exit of the Runloop is freely controlled. Relatively speaking, it is better to use this scheme.

    3. To summarize

    If you don’t want to exit runloop, you can start runloop in the first way. If you start runloop in the second way, you can exit by setting a timeout. If you use the third method to start runloop, you can exit by setting a timeout or using the CFRunLoopStop method. Demo please stamp here… If there is wrong in the article, but also hope that friends are not stingy corrections. After all, the ability level is limited, can not guarantee accuracy.

    References:

    Bestswifter.com/runloop-and… Blog.ibireme.com/2015/05/18/… Blog.csdn.net/yxh265/arti… www.dreamingwish.com/frontui/art…