A thread usually executes one task at a time and exits when it is finished. However, if the child thread exits immediately after performing an asynchronous operation (such as a network request), the returned data cannot be received and processed properly, so we need to keep the thread alive to ensure that the asynchronous operation can be completed successfully.

To execute a task without exiting, a resident thread, you can use RunLoop.

First, we need to understand the relationship between threads and runloops:

  • Runloops and threads correspond one to one;
  • After the child thread is created, the corresponding RunLoop is not created and run. The RunLoop is created only after the child thread is acquired, and the RunLoop is started by calling the run method.
  • When a thread adds a RunLoop and runs it, it is actually executing a do-while loop, which means the task is always executing and therefore does not exit.

Why did EARLY AFNetworking need thread survival

  • Early AFNetworking(2.x) network requests were based onNSURLConnectionThe implementation;NSURLConnectionDesigned to send asynchronously, called-startMethods after,NSURLConnectionI’m going to create some new threads using the underlyingCFSocketTo send and receive requests, notifying the original thread after some event has occurredRunLoopGo back to the event. That is to say,NSURLConnectionThe proxy callback is also passedRunLoopThe trigger;
  • We usually use it ourselvesNSURLConnectionWhen implementing a network request,NSURLConnectionThe creation and callback are generally in the main thread, the main thread has been existing all the callbacks without problem;
  • AFN as a network layer framework, inNSURLConnectionAfter the callback comes back, yeahResponseSome operations, such as serialization and error handling, are put into the child thread, and then returned to the main thread, and then called back to the user through the AFN’s own agent;
  • AFN receivingNSURLConnectionThe callback is normally executed by this thread[connection start]After sending the network request, it immediately exits, and subsequent callbacks cannot be called. The thread alive ensures that the thread does not exit and the callback succeeds.

AFNetworking threads are alive

In the early days of AFNetworking thread preservation, a similar approach was adopted by referring to the relationship between threads and RunLoop:

+ (void)networkRequestThreadEntryPoint:(id)__unused object { @autoreleasepool { [[NSThread currentThread] setName:@"AFNetworking"]; NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; / / get the current RunLoop [RunLoop addPort: [NSMachPort port] forMode: NSDefaultRunLoopMode]; [runLoop run]; }}Copy the code

Create resident thread

+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        [_networkRequestThread start];
    });

    return _networkRequestThread;
}
Copy the code

_networkRequestThread is the resident thread created to get a RunLoop and run it; So the thread will not exit or be destroyed unless the RunLoop stops; This implements resident thread survival.

AFNetworking 3.x no longer requires thread survival

AFNetworking 3.x is implemented based on NSUrlSession. NSUrlSession maintains a thread pool for scheduling and management of Request threads. So afn3. x does not require a resident thread, just CFRunLoopRun(); Start RunLoop and end with CFRunLoopStop(CFRunLoopGetCurrent()); Stop RunLoop.

Implement a thread save yourself

Create a custom thread class by referring to AFN thread survival:

#import <Foundation/Foundation.h>

@interface KeepAliveThread : NSThread

@end
--------------------
#import "KeepAliveThread.h"

@implementation KeepAliveThread

- (void)dealloc {
    NSLog(@"%@ - %s", NSStringFromClass([self class]), __func__);
}

@end
Copy the code

Create a thread in VC and execute:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.td = [[KeepAliveThread alloc] initWithBlock:^{
        NSLog(@"%@, start", [NSThread currentThread]);
        
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
        
        NSLog(@"%@, end", [NSThread currentThread]);
    }];
    [self.td start];
}
Copy the code

The output is as follows:

<KeepAliveThread: 0x600000b5a600>{number = 10, name = (null)}, start
Copy the code

The reason for this result is that after a call to [runLoop run], it is actually stuck in the do-while loop. It never exits, so it does not continue to execute. The code in the block is not finished, and the thread is not finished and destroyed.

We can also verify that threads are alive in other ways:

  • Exit the current page, and no custom thread class is executed-deallocMethods.
  • Add the following click event code:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self performSelector:@selector(loglog) onThread:self.td withObject:nil waitUntilDone:NO];
}

- (void)loglog {
    NSLog(@"%@ - %s", NSStringFromClass([self class]), __func__);
}
Copy the code

We find that every click responds, which proves that the thread is always alive.

NewViewController - -[NewViewController loglog]
Copy the code

Why add Port to RunLoop?

The question here is why add a Port to RunLoop at all?

This step is the key to keeping the thread alive. Without this line, the thread would have finished immediately, as follows:

<KeepAliveThread: 0x600000553b80>{number = 8, name = (null)}, start
<KeepAliveThread: 0x600000553b80>{number = 8, name = (null)}, end
Copy the code

Take a RunLoop and run it, let it run, and with no source to wait on, the RunLoop will execute and exit immediately; Once a RunLoop is assigned a message source to wait on, the RunLoop executes and enters the wait state, which is called “alive”.

Add Timer and Source to RunLoop to keep thread alive:

[runLoop addTimer:timer forMode:NSDefaultRunLoopMode]
Copy the code

How to exit a keepalive thread

As you can see from the previous example, thread retention is now permanent, consistent with the life cycle of the App, so exiting the thread retention sheet will not cause the thread to exit, which is a memory leak risk.

Refer to the RunLoop’s run method documentation:

If no input sources or timers are attached to the run loop, this method exits immediately; otherwise, it runs the receiver in the NSDefaultRunLoopMode by repeatedly invoking run(mode:before:). In other words, this method effectively begins an infinite loop that processes data from the run loop’s input sources and timers. Manually removing all known input sources and timers from the run loop is not a guarantee that the run loop will exit. macOS can install and remove additional input sources as needed to process requests targeted at the receiver’s thread. Those sources could therefore prevent the run loop from exiting. If you want the run loop to terminate, you shouldn’t use this method. Instead, use one of the other run methods and also check other arbitrary conditions of your own, in a loop. A simple example would be:

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

It is clearly documented that if you want RunLoop to end, you should use another run method wrapped in the loop. The code modification is as follows:

#import "NewViewController.h" #import "KeepAliveThread.h" @interface NewViewController () @property (nonatomic, strong) KeepAliveThread *td; @property (nonatomic, assign) BOOL isKeepAlive; @end @implementation NewViewController - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. self.isKeepAlive = YES; __weak typeof (self) weakSelf = self; self.td = [[KeepAliveThread alloc] initWithBlock:^{ NSLog(@"%@, start", [NSThread currentThread]); NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; while (weakSelf.isKeepAlive) { [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate } NSLog(@"%@, end", [NSThread currentThread]);}]; [self.td start]; } - (IBAction)clickOnTerminateLoop:(id)sender { [self performSelector:@selector(terminateLoop) onThread:self.td withObject:nil waitUntilDone:NO]; } - (void)terminateLoop { self.isKeepAlive = NO; CFRunLoopStop(CFRunLoopGetCurrent()); } - (IBAction)clickOnLog:(id)sender {[self performSelector:@selector(loglog) onThread:self.td withObject:nil waitUntilDone:NO]; } - (void)loglog { NSLog(@"%@ - %s", NSStringFromClass([self class]), __func__); } @endCopy the code

Execute as follows:

<KeepAliveThread: 0x600002489280>{number = 10, name = (null)}, start
NewViewController - -[NewViewController loglog]
<KeepAliveThread: 0x600002489280>{number = 10, name = (null)}, end
KeepAliveThread - -[KeepAliveThread dealloc]
Copy the code

This controls the exit of the keepalive thread.

Refer to the article: www.jianshu.com/p/771b68a41…