【iOS面试题】1.谈一谈AFNetworking线程保活

4,194 阅读5分钟

线程一般一次只执行一个任务,执行完毕就会退出;但是如果在子线程执行异步操作(如网络请求)后马上退出,就会导致返回的数据无法正常接收和处理,因此我们需要线程保活,确保异步的操作能够顺利完成。

要执行任务后不退出,即常驻线程,可以使用RunLoop实现。

首先,我们需要先搞清楚线程与RunLoop的关系:

  • RunLoop和线程是一一对应的;
  • 子线程创建后,对应的RunLoop并没有创建和运行,而需要主动去获取才会创建,调用run方法才会开始运行;
  • 线程添加了RunLoop并运行,实际上是在执行do-while循环,相当于任务一直在执行,因此不会退出。

早期AFNetworking为什么需要线程保活

  • 早期的AFNetworking(2.x)网络请求是基于NSURLConnection实现的;NSURLConnection被设计成异步发送,调用了-start方法后,NSURLConnection会新建一些线程用底层的CFSocket去发送和接收请求,在发送和接收的一些事件发生后通知原来线程的RunLoop去回调事件。也就是说NSURLConnection的代理回调,也是通过RunLoop触发的;
  • 平常我们自己使用NSURLConnection实现网络请求时,NSURLConnection的创建与回调一般都是在主线程,主线程本来一直存在所有回调没有问题;
  • AFN作为网络层框架,在NSURLConnection回调回来之后,对Response做了一些诸如序列化、错误处理的操作的,这些操作都放在子线程去做,处理后接着回到主线程,再通过AFN自己的代理回调给用户;
  • AFN的接收NSURLConnection回调的这个线程,正常情况下在执行[connection start]发送网络请求后就立即退出了,后续的回调就调用不了;而线程保活就能确保该线程不退出,回调成功;

AFNetworking的线程保活

参考前面线程与RunLoop的关系,AFNetworking早期线程保活也采用了类似的处理方式:

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];

        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];      // 获取当前RunLoop
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}

创建常驻线程

+ (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;
}

_networkRequestThread就是创建的常驻线程,在线程里获取RunLoop并运行起来;所以这个线程不会被退出、销毁,除非RunLoop停止;这样就实现了常驻线程保活。

AFNetworking 3.x不再需要线程保活

AFNetworking 3.x是基于NSUrlSession实现的,NSUrlSession内部自己维护了一个线程池,做Request线程的调度与管理;因此AFN3.x无需常驻线程,只是用的时候CFRunLoopRun();开启RunLoop,结束的时候CFRunLoopStop(CFRunLoopGetCurrent());停止RunLoop即可。

自己实现一个线程保活

参考AFN的线程保活,先创建一个自定义的线程类:

#import <Foundation/Foundation.h>

@interface KeepAliveThread : NSThread

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

@implementation KeepAliveThread

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

@end

在VC内创建线程,并执行:

- (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];
}

结果输出如下:

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

出现这个结果的原因是在调用[runLoop run]后,实际是卡在do-while循环中了,始终没有退出,因此也不会继续执行后面的内容,block内的代码也没有执行完,线程也就不会运行结束并销毁,就达到了线程保活的目的。

通过其他的方式,我们也可以侧面验证线程还存活着:

  • 退出当前页面,也没有执行自定义线程类的-dealloc方法。
  • 添加如下点击事件代码:
- (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__);
}

我们发现每次点击都能响应,这也证明线程一直存活。

NewViewController - -[NewViewController loglog]

为什么要添加Port到RunLoop?

这里有疑问的一点是,为什么要添加一个Port到RunLoop呢?

其实这一步才是线程保活的关键,如果没有这一行,线程就会立刻执行完毕,如下:

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

可以这样理解,获取RunLoop并运行,让它跑起来,而没有需要等待的消息来源,那么RunLoop就会立刻执行并退出;而一旦给RunLoop指定了需要等待的消息来源,那么RunLoop就会执行,并进入等待状态,也就是我们说的保活。

不仅是Port,为RunLoop添加Timer、Source都会让线程保活:

[runLoop addTimer:timer forMode:NSDefaultRunLoopMode]

保活线程如何退出

从前面的例子可以看出,现在线程保活是永久性的,与App的生命周期一致,因此退出线程保活页面也不会让该线程退出,这就存在内存泄露风险。

参考RunLoop的run方法文档:

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]]);

这里文档写得很清晰,如果希望RunLoop结束,应该采用另一个包裹在循环中的run方法。代码改造如下:

#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 distantFuture]];   // 此方法调用一次,只执行一次
        }
        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__);
}

@end

执行如下:

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

这样就可以控制保活线程退出了。

参考文章: www.jianshu.com/p/771b68a41…