我们经常在子线程中处理一些非UI的任务,我们知道子线程处理完任务后就会自动销毁,假设某个界面需要经常在子线程中处理事情,那么线程的不断创建,销毁也是消耗性能的,因此需求就来了:

如何设计一个子线程,使其命可以一直不死?

1.自定义一个线程继承自NSThread,重写dealloc方法,这样我们监听线程的是否挂掉

@interface DXThread : NSThread

@end

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

2.把自定义的线程,声明为属性,那样不就可以保命了吗?

#import "DXThread.h"

@interface ViewController ()
@property (strong, nonatomic) DXThread *thread;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.thread = [[DXThread alloc] initWithTarget:self selector:@selector(test) object:nil];
    [self.thread start];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}

// 子线程需要执行的任务
- (void)test{
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
}

启动程序,打印如下,发现并没有打印dealloc方法,也就说线程没有挂掉,这样的确是保命了

[ViewController test] <DXThread: 0x60000081c100>{number = 3, name = (null)}

然后发现点击屏幕,没有任何打印,说明这个线程已经不能做事情了

如何给线程保命?然后还可以让线程处理事情?

self.thread = [[DXThread alloc] initWithTarget:self selector:@selector(saveThreadLife) object:nil];
[self.thread start];

// 这个方法的目的:线程保活
- (void)saveThreadLife {
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
    
    // 往RunLoop里面添加Source\Timer\Observer
    [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];
    
    NSLog(@"%s ----end----", __func__);
}

代码如上,

事实上,下面这句代码会产生循环引用

self.thread = [[DXThread alloc] initWithTarget:self selector:@selector(saveThreadLife) object:nil];
[self.thread start];

线程会强引用持有controller,controller也会强引用线程,控制器不会调用dealloc方法

可以使用以下方法initWithBlock

 self.thread = [[DXThread alloc] initWithBlock:^{
 
     NSLog(@"%@----begin----", [NSThread currentThread]);
     // 往RunLoop里面添加Source\Timer\Observer
     [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
     NSLog(@"%@----end----", [NSThread currentThread]);
}];

好了现在,控制器pop时候,controller是有销毁了,但是线程任然没有销毁 那么问题来了

controller都销毁了,为什么线程没有挂掉?

我们知道,任务做完,线程就会自动销毁

// 线程一直在执行这个任务,任务不结束,线程怎么可能销毁呢?
[[NSRunLoop currentRunLoop] run];

进入run方法,查看它的介绍

it runs the receiver in the NSDefaultRunLoopMode by repeatedly invoking runMode:beforeDate:.
In other words, this method effectively begins an infinite loop that processes data from the run loop’s input sources and timers

在defaultRunLoopMode下,重复调用runMode:beforeDate:相当于如下代码

while (1) {
    // [NSDate distantFuture] 很遥远,可能是几百年之后
    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}

NSRunLoop的run方法是无法停止的,它专门用于开启一个永不销毁的线程(NSRunLoop)

换言之,它的适用场景是专门用于开启一个永不销毁的线程,不能直接用run方法

所以,最终我们解决方案是,自定义一个标记,自定义一个循环,是否要停止线程

@property (assign, nonatomic, getter=isStoped) BOOL stopped;

__weak typeof(self) weakSelf = self;
while (!weakSelf.isStoped) {
    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}

停止线程

- (IBAction)stop {
    // 在子线程调用stop
    [self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:NO];
}

// 用于停止子线程的RunLoop
- (void)stopThread{
    // 设置标记为YES
    self.stopped = YES;
    
    // 停止RunLoop
    CFRunLoopStop(CFRunLoopGetCurrent());
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
}

如果想要控制器dealloc,线程也挂掉,那么需要在控制器的dealloc

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

结果运行,发现pop控制器的时候就崩溃了

因为这行代码设置了NO,

[self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:NO];

不等任务结束,直接执行后面的代码(dealloc执行完大括号,那么控制器一定挂了),那么控制器就销毁了,self == nil 所以while循环中(!weakSelf.isStoped) == !false == true,由于代码崩溃在这行

[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];

查看代码执行顺序,发现崩溃时候,停止线程的方法stopThread并没有来得及执行,就崩溃了

由于给nil发消息不会崩溃,所以我大胆猜测,这个 [self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:NO];这个导致[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];方法崩溃,具体原因也不清楚啊

因此,要设置waitUntilDone:YES,串行执行代码,这样控制器还没彻底挂掉之前,先停止线程

最终while条件是 (weakSelf.isStoped && weakSelf != nil)