RunLoop 二 : RunLoop在实际中的应用

在上一篇我们讲过RunLoop的底层数据机构以及它内部的工作流程,这一篇我们就来讲一下RunLoop在实际的工作中有哪些应用.

一: 解决 Timer 在滑动中停止工作的问题:

这个问题大家都遇到过,Timer在拖动UIScrollView及其子类控件的时候会停止.我们可以这样解决:

    NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        
    }];
    [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];

真实的模式只有NSDefaultRunLoopModeUITrackingRunLoopMode两种.NSRunLoopCommonModes这种模式并不存在,它只是一个标记,表示timer可以在设置了common标记的模式下运行.换句话说就是timer可以在runloop中的_commonModels集合中装的模式下运行,事实上这个集合就装了NSDefaultRunLoopModeUITrackingRunLoopMode两个模式.而能在common模式下运行的控件都被放在runloop中的_commonModelItems集合中.

一: 控制线程的生命周期 (线程保活)

实例代码:

//执行任务按钮
- (IBAction)startExecuteTask:(id)sender {
    // NSThread 频繁创建线程
    MYThread *thread = [[MYThread alloc]initWithTarget:self selector:@selector(downLoad) object:nil];
    [thread start];
}

//执行任务
- (void)downLoad{
    NSLog(@"执行任务");
}

我们每次点击按钮,都会去执行任务:

2019-12-08 14:14:33.924315+0800 线程保活[1123:57183] 执行任务
2019-12-08 14:14:33.924523+0800 线程保活[1123:57183] MYThread dealloc
2019-12-08 14:14:34.785481+0800 线程保活[1123:57185] 执行任务
2019-12-08 14:14:34.785788+0800 线程保活[1123:57185] MYThread dealloc

可以看到,任务一执行完,线程就释放了.并且这样频繁的创建线程,很消耗资源,我们可以用RunLoop来延长线程的生命周期,不让线程挂掉,我们在- (void)downLoad方法中添加如下代码:

//执行任务
- (void)downLoad{
    NSLog(@"---------- start -----------");
    [[NSRunLoop currentRunLoop]run];
    NSLog(@"执行任务");
    NSLog(@"---------- end -----------");
}

在运行一下,发现线程执行完任务还是会挂掉:

2019-12-08 14:19:08.073066+0800 线程保活[1148:61181] ---------- start -----------
2019-12-08 14:19:08.073368+0800 线程保活[1148:61181] 执行任务
2019-12-08 14:19:08.073519+0800 线程保活[1148:61181] ---------- end -----------
2019-12-08 14:19:08.073796+0800 线程保活[1148:61181] MYThread dealloc

这是因为,我们虽然获取了当前的RunLoop,并且调用run方法让RunLoop跑起来了,而run方法底层调用的是- (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;方法,这个方法会把RunLoop添加到NSDefaultRunLoopMode模式下.Model中如果没有任何Source0 , Source1 , Timer , Observer,RunLoop会立马退出. 所以我们需要往RunLoop中添加任务,任何任务都可以.比如这样:

//执行任务
- (void)downLoad{
    NSLog(@"---------- start -----------");
    // 往 RunLoop 中添加 Source0 , Source1 , Timer , Source 等任务
    NSPort *port = [NSPort port];
    [[NSRunLoop currentRunLoop]addPort:port forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop]run];
    NSLog(@"执行任务");
    NSLog(@"---------- end -----------");
}

在运行线程就不会执行完任务就挂掉了,而是执行完任务就休眠:

//执行完任务后并没有销毁线程,说明 runloop 现在已经进入休眠状态
2019-12-08 14:26:15.869758+0800 线程保活[1188:68013] ---------- start -----------

现在我们已经能保证不让线程死亡,如果我们要唤醒线程,让线程去执行任务,代码还需要再改一下,因为上面的代码thread是一个局部变量,每次执行任务都会重新创建,所以我们把线程设置成一个属性:

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

//开始执行任务按钮
- (IBAction)startExecuteTask:(id)sender {
    [self performSelector:@selector(threadTask) onThread:self.thread withObject:nil waitUntilDone:YES];
    NSLog(@"123");
}

//线程执行任务的函数
- (void)threadTask{
    NSLog(@"线程真正需要执行的任务");
}


//保住线程的命,不让线程过早的死亡
- (void)saveThreadLife{
    NSLog(@"---------- start -----------");
    // 往 RunLoop 中添加 Source0 , Source1 , Timer , Source 等任务
    NSPort *port = [NSPort port];
    [[NSRunLoop currentRunLoop]addPort:port forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop]run];
    NSLog(@"---------- end -----------");
}

其中performSelector:onThread:withObject:waitUntilDone:最后一个参数需要注意:

image.png

如果waitUntiDoneYES,就表示子线程调用threadTask函数的代码执行完毕后,才会继续往下走:

2019-12-08 14:50:12.187779+0800 线程保活[1256:89195] ---------- start -----------
2019-12-08 14:50:13.119734+0800 线程保活[1256:89195] 线程真正需要执行的任务
2019-12-08 14:50:13.119975+0800 线程保活[1256:89116] 123
2019-12-08 14:50:23.760491+0800 线程保活[1256:89195] 线程真正需要执行的任务
2019-12-08 14:50:23.760698+0800 线程保活[1256:89116] 123

waitUntiDoneNO表示,不需要等子线程threadTask函数执行完,就执行NSLog (@"123")输出语句,也就是说子线程的函数和当前主线程的函数是同时执行的:

2019-12-08 14:57:14.127894+0800 线程保活[1282:95331] ---------- start -----------
2019-12-08 14:57:15.037212+0800 线程保活[1282:95247] 123
2019-12-08 14:57:15.037290+0800 线程保活[1282:95331] 线程真正需要执行的任务
2019-12-08 14:57:16.668619+0800 线程保活[1282:95247] 123
2019-12-08 14:57:16.668655+0800 线程保活[1282:95331] 线程真正需要执行的任务

上面的做法虽然保住了线程的命,并且线程也能执行其他任务,但是退出控制器后,线程和控制器都没有销毁.控制器和线程可能产生了强引用:


控制器和线程都没有释放

我们把创建线程的方法换一种写法:

- (void)viewDidLoad {
    [super viewDidLoad];
//    self.thread = [[MYThread alloc]initWithTarget:self selector:@selector(saveThreadLife) object:nil];
    self.thread = [[MYThread alloc]initWithBlock:^{
        NSLog(@"---------- start -----------");
        // 往 RunLoop 中添加 Source0 , Source1 , Timer , Source 等任务
        NSPort *port = [NSPort port];
        [[NSRunLoop currentRunLoop]addPort:port forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop]run];
        NSLog(@"---------- end -----------");
    }];
    [self.thread start];
}

//退出控制器后,控制器释放了,但是 线程 还是没有释放
2019-12-08 17:06:20.707379+0800 线程保活[1855:201278] ---------- start -----------
2019-12-08 17:06:23.238592+0800 线程保活[1855:201186] 123
2019-12-08 17:06:23.238636+0800 线程保活[1855:201278] 线程真正需要执行的任务
2019-12-08 17:06:24.135559+0800 线程保活[1855:201186] 123
2019-12-08 17:06:24.135597+0800 线程保活[1855:201278] 线程真正需要执行的任务
2019-12-08 17:06:25.740854+0800 线程保活[1855:201186] -[ViewController dealloc]

很奇怪,控制器都释放了,按理说控制器内部的所有东西都应该释放了呀,我们在ViewControllerdealloc方法中把thread置为nil:

- (void)dealloc{
    self.thread = nil;
    NSLog(@"%s",__func__);
}
// 退出控制器后,threa 还是没有释放
2019-12-08 17:35:02.698319+0800 线程保活[1894:215509] ---------- start -----------
2019-12-08 17:35:03.531820+0800 线程保活[1894:215408] 123
2019-12-08 17:35:03.531866+0800 线程保活[1894:215509] 线程真正需要执行的任务
2019-12-08 17:35:06.525486+0800 线程保活[1894:215408] -[ViewController dealloc]

thread强制置为nil,thread还是没有释放.这个问题的根源就在于这几行代码:

    self.thread = [[MYThread alloc]initWithBlock:^{
        NSLog(@"---------- start -----------");
        // 往 RunLoop 中添加 Source0 , Source1 , Timer , Source 等任务
        NSPort *port = [NSPort port];
        [[NSRunLoop currentRunLoop]addPort:port forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop]run];
        NSLog(@"---------- end -----------");
    }];

我们发现NSLog(@"---------- end -----------");这行代码始终没有调用,说明了block代码块中的任务始终没有执行完,执行到[[NSRunLoop currentRunLoop]run];这一行时RunLoop就已经进入休眠状态了,不会继续往下走了.任务始终没有执行完,所以线程始终没有释放.
如果我们想要精准的控制线程的生命周期,比如说控制器销毁的时候,线程也销毁,那应该怎么做呢?我们可以像下面这样手动停止RunLoop:

- (void)stopRunLoop{
    CFRunLoopStop(CFRunLoopGetCurrent());
    NSLog(@"%s",__func__);
    
}

- (void)dealloc{
    [self performSelector:@selector(stopRunLoop) onThread:self.thread withObject:nil waitUntilDone:NO];
    NSLog(@"%s",__func__);
}
// stopRunLoop 虽然执行了,并且ViewController 也已经销毁了,但是 thread 仍然没有销毁
2019-12-08 18:23:48.470564+0800 线程保活[1987:246690] ---------- start -----------
2019-12-08 18:23:49.344555+0800 线程保活[1987:246534] 123
2019-12-08 18:23:49.344608+0800 线程保活[1987:246690] 线程真正需要执行的任务
2019-12-08 18:23:51.177254+0800 线程保活[1987:246534] -[ViewController dealloc]
2019-12-08 18:23:51.177421+0800 线程保活[1987:246690] -[ViewController stopRunLoop]

stopRunLoop 虽然执行了,并且ViewController 也已经销毁了,但是thread 仍然没有销毁,这是为什么呢?这是因为[[NSRunLoop currentRunLoop]run]run方法导致的,我们看看run方法做了什么:

Discussion

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 
runMode:beforeDate:. 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:
// 翻译:
讨论
如果运行循环没有附加输入源或计时器,则此方法立即退出;
否则,它通过反复调用runMode:beforeDate:在NSDefaultRunLoopMode
中运行接收器。换句话说,这个方法有效地开始了一个无限循环,
处理来自运行循环的输入源和计时器的数据。
从运行循环中手动删除所有已知的输入源和计时器并不能
保证运行循环将退出。macOS可以根据需要安装和删除额外的输入源,
以处理针对接收方线程的请求。因此,这些源可以防止run循环退出。
如果希望run循环终止,则不应使用此方法。相反,在循环中使用
其他运行方法之一,并检查您自己的其他任意条件。一个简单的例子是:

从官方的注释中可以看到,run方法底部是反复调用runMode:beforeDate:这个方法,并且无线循环,也就是说如果一旦调用run方法启动一个RunLoop是无法停掉的.所以我们可以手动调用runMode:beforeDate:这个方法:

- (void)viewDidLoad {
    [super viewDidLoad];
//    self.thread = [[MYThread alloc]initWithTarget:self selector:@selector(saveThreadLife) object:nil];
    self.thread = [[MYThread alloc]initWithBlock:^{
        NSLog(@"---------- start -----------");
        // 往 RunLoop 中添加 Source0 , Source1 , Timer , Source 等任务
        NSPort *port = [NSPort port];
        [[NSRunLoop currentRunLoop]addPort:port forMode:NSDefaultRunLoopMode];
// 调用 runMode: beforeDate: 方法开启runloop
        [[NSRunLoop currentRunLoop]runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        NSLog(@"---------- end -----------");
    }];
    [self.thread start];
}

//运行结果;
2019-12-08 18:37:07.633864+0800 线程保活[2027:259432] ---------- start -----------
2019-12-08 18:37:08.609064+0800 线程保活[2027:257252] 123
2019-12-08 18:37:08.609123+0800 线程保活[2027:259432] 线程真正需要执行的任务
2019-12-08 18:37:08.609421+0800 线程保活[2027:259432] ---------- end -----------

从运行结果看到,执行完任务后RunLoop立马就结束了,但是我们的目的是保住线程的命,控制它的生命周期,所以我们需要一个开关来控制runMode: beforeDate:方法的调用次数:

@interface ViewController ()

@property (nonatomic,strong)MYThread *thread;

@property (nonatomic,assign)BOOL isStop;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
//    self.thread = [[MYThread alloc]initWithTarget:self selector:@selector(saveThreadLife) object:nil];

    __weak typeof(self) weakSelf = self;
    self.isStop = NO;
    self.thread = [[MYThread alloc]initWithBlock:^{
        NSLog(@"---------- start -----------");
        // 往 RunLoop 中添加 Source0 , Source1 , Timer , Source 等任务
        NSPort *port = [[NSPort alloc]init];
        [[NSRunLoop currentRunLoop]addPort:port forMode:NSDefaultRunLoopMode];
        while (!weakSelf.isStop) {
           [[NSRunLoop currentRunLoop]runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        }
        NSLog(@"---------- end -----------");
    }];
    [self.thread start];
}
//开始执行任务按钮
- (IBAction)startExecuteTask:(id)sender {
    
    [self performSelector:@selector(threadTask) onThread:self.thread withObject:nil waitUntilDone:NO];
    
    NSLog(@"123");
}

//线程执行任务的函数
- (void)threadTask{
    NSLog(@"线程真正需要执行的任务");
}
// 停止按钮的方法
- (IBAction)stopAction {
    [self performSelector:@selector(stopRunLoop) onThread:self.thread withObject:nil waitUntilDone:NO];
}

//停止runloop
- (void)stopRunLoop{
    self.isStop = YES;
    CFRunLoopStop(CFRunLoopGetCurrent());
    NSLog(@"%s",__func__);
}

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

我们按照以上写法,定义一个isStop的变量,初始值设置为NO,然后用一个while循环判断如果isStopNO就循环执行runMode : beforeDate :方法,直到我们在stopRunLoop中将isStop设置为YES位置.这样就能保证RunLoop在执行完任务后不会立马就销毁.我们运行试一下:

正常

但是我们如果不点击停止按钮,直接Back会发现有时候会崩溃:

崩溃

这是为什么呢?这就是我们上面讲的waitUntilDone造成的.我们在[self performSelector:@selector(stopRunLoop) onThread:self.thread withObject:nil waitUntilDone:NO];中把waitUntilDone设置为NO,就表示在子线程执行的stopRunLoop函数和在主线程执行的- (IBAction)stopAction函数是同时执行的.一旦- (IBAction)stopAction函数先执行完,那么ViewControllerdealloc函数也会立马执行完毕,ViewController就会释放.这时候再去执行stopRunLoop就会报坏内存访问,因为ViewController已经释放了.为什么会崩溃到[[NSRunLoop currentRunLoop]runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];这一行呢?这是因为performSelector:onThread:的本质就是通过子线程的runloop去调用具体的SEL,而runloop在调用stopRunLoop的时候会用到ViewController,而ViewController又释放了,所以会崩溃到这一行.
所以,我们把

- (IBAction)stopAction {
    [self performSelector:@selector(stopRunLoop) onThread:self.thread withObject:nil waitUntilDone:YES];
}

中的waitUntilDone设置为YES就好了.
这样的确是不会发生崩溃了,但是还有另外一个问题,我们点击返回退出控制器后ViewController释放了,但是没有看到线程释放:

线程没有释放

我们在while循环中打一个断点:

weakSelf 为 null

从上图可以看出的确是调用了stopRunLoop并且执行了CFRunLoopStop(CFRunLoopGetCurrent());.但是CFRunLoopStop(CFRunLoopGetCurrent());只是停止了RunLoop循环中的一次,等到下次循环时候判断!weakSelf.isStop竟然还是为YES,而从打印的weakSelf能看出来此时weakSelf已经为null了,我们再向一个null对象发送消息得到还是null,再取反!null结果就是YES,所以会符合条件再次进入RunLoop循环.那我们可不可以使用__strong typeof(self)strongSelf = weakSelf;再强引用一下weakSelf呢?如果这样的话,发现更加连ViewController都无法释放了,因为他们之前形成了循环引用:
循环引用

所以我们还是要用weakSlef,只不过要多加一个判断:

        while (weakSelf && !weakSelf.isStop) {
            NSLog(@"weakSelf-----%@",weakSelf);
           [[NSRunLoop currentRunLoop]runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        }

判断weakSelf是否为nil,如果不为nil,再判断weakSelf.isStop是否为NO.ok,我们来试一下.

崩溃

运行后,点击停止再退出ViewController就崩溃了.这是因为我们点击停止后执行了stopRunLoop方法,在这个方法内部调用了CFRunLoopStop(CFRunLoopGetCurrent());,把子线程的runloop停止掉了.runloop停止掉后它的任务就执行完了,线程的生命周期已经结束了,这时候它已经不能再执行任务了.而我们点击返回按钮,又让子线程去执行stopRunLoop任务就会报错.我们可以打印self.thread.executing会发现为NO.所以,我们需要加上判断,如果self.thread == nilreturn,不执行任务:

// 停止按钮的方法
- (IBAction)stopAction {
    if (!self.thread) return;
    [self performSelector:@selector(stopRunLoop) onThread:self.thread withObject:nil waitUntilDone:YES];
}


//停止runloop
- (void)stopRunLoop{
    self.isStop = YES;
    CFRunLoopStop(CFRunLoopGetCurrent());
    self.thread = nil;
    NSLog(@"%s",__func__);
}

这样就完美了.

你可能感兴趣的:(RunLoop 二 : RunLoop在实际中的应用)