• Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”.

In development, it is common to put time-consuming operations into child threads to improve application performance. When the task in the child thread completes, the thread is destroyed immediately.

If you are constantly executing tasks in child threads, creating and destroying threads frequently is a waste of resources, which is not what we intended to do. At this point, we need to keep the thread alive, to make sure that the thread should be processing things when the wake up, when idle sleep.

We know that RunLoop can wake up when a transaction is needed to execute a task and sleep when it is idle to save resources. This feature can be used to handle thread survival and control the life cycle of a thread.

From discovery to success

- (void)viewDidLoad {
    [super viewDidLoad];
    
    LSThread *thread = [[LSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    [thread start];
}

- (void)run {
    NSLog(@"func -- %s thread -- %@", __func__, [NSThread currentThread]);
    [[NSRunLoop currentRunLoop] run];
    NSLog(@ end -- -- -- -- -- -- "");
}
Copy the code

LSThread inherits from NSThread and overrides the dealloc method

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

Results after execution:

You can see that the thread failed to survive:

  • Although RunLoop is started, the following closing log is still executed
  • The thread was destroyed after execution

To ensure that the thread is not destroyed after execution, you can strongly reference the thread

self.thread = [[LSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[self.thread start];
Copy the code

But that doesn’t solve the RunLoop problem. So if RunLoop is started, why isn’t it running continuously?

Let’s look at the definition of the run method

If no input sources or timers are attached to the run loop, this method exits immediately.

This method exits immediately if no sources or timers are attached to the RunLoop.

Adding a source or timer to the RunLoop should solve this problem

[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
Copy the code

Running again does not end the log, and the thread survives successfully.

Let’s continue to refine our requirements. When the thread-owning controller is destroyed, the new child thread should be destroyed as well, adding dealloc to the controller

- (void)dealloc {
    NSLog(@"-- Destroy controller -- %s", __func__);
}
Copy the code

The child thread is created when the controller is present and the console output is as follows when the controller is gone

Neither the controller nor the child thread is destroyed. Look at the code

self.thread = [[LSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
Copy the code

Here the controller strongly references Thread, which in turn holds the controller self internally, creating a reference loop.

To break the reference loop, use blocks

self.thread = [[LSThread alloc] initWithBlock:^{
        NSLog(@"func -- %s thread -- %@", __func__, [NSThread currentThread]);
        [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] run];
        NSLog(@ end -- -- -- -- -- -- "");
    }];
Copy the code

The execution result

The controller is released successfully, but the child thread is not destroyed, so the child thread becomes global. This is where RunLoop starts.

RunLoop starts in three ways

- (void)run;

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

The runMode:beforeDate: method is repeatedly called internally in NSDefaultRunLoopMode.

runUntilDate: The RunLoop method allows you to set a timeout period, during which time the RunLoop processes data from sources and, like run, The runMode:beforeDate: method is also repeatedly called in NSDefaultRunLoopMode mode.

RunMode :beforeDate: The method RunLoop runs once and exits when the timeout expires or the first source is processed.

The Apple documentation on the run method says that you should not use it if you want to exit RunLoop.

If the RunLoop has no input sources or attached timer, the RunLoop exits. This is not recommended by Apple because it is possible to add input sources to the current thread’s RunLoop, so manually removing the input source or timer does not guarantee that the RunLoop will exit.

The problem is clear: we should not use the run method to start RunLoop, because it creates a loop that does not exit, and child threads using this method cannot be destroyed. We can use the runMode:beforeDate: method like run to create a child thread that matches our criteria:

[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
Copy the code

Put it in a while loop and use a global flag whether to stop RunLoop to assist with the thread’s lifecycle

__weak typeof(self) weakSelf = self;
    self.thread = [[LSThread alloc] initWithBlock:^{
        NSLog(@"func -- %s thread -- %@", __func__, [NSThread currentThread]);
        [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
// [[NSRunLoop currentRunLoop] run];
        while(! weakSelf.isStopedThread) { [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        }
        NSLog(@ end -- -- -- -- -- -- "");
    }];
Copy the code

Stop the RunLoop method

- (void)stop {
    self.isStopedThread = YES;
    CFRunLoopStop(CFRunLoopGetCurrent());
}
Copy the code

One thing to note here is that the stop operation must be performed on the target thread. For example, calling the stop method directly does not achieve the desired effect. This is because stop is executed on the main thread by default and the target thread is not reached.

- (void)stopAction {
    [self performSelector:@selector(stop) onThread:self.thread withObject:nil waitUntilDone:YES];
    self.thread = nil;
}
Copy the code

By calling stop on the current thread, we accomplish this, successfully terminating the RunLoop and destroying the thread.