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 on
NSURLConnection
The implementation;NSURLConnection
Designed to send asynchronously, called-start
Methods after,NSURLConnection
I’m going to create some new threads using the underlyingCFSocket
To send and receive requests, notifying the original thread after some event has occurredRunLoop
Go back to the event. That is to say,NSURLConnection
The proxy callback is also passedRunLoop
The trigger; - We usually use it ourselves
NSURLConnection
When implementing a network request,NSURLConnection
The 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, in
NSURLConnection
After the callback comes back, yeahResponse
Some 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 receiving
NSURLConnection
The 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
-dealloc
Methods. - 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…