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];
真实的模式只有NSDefaultRunLoopMode
和UITrackingRunLoopMode
两种.NSRunLoopCommonModes
这种模式并不存在,它只是一个标记,表示timer
可以在设置了common
标记下的模式下运行.换句话说就是timer
可以在runloop
中的_commonModels
集合中装的模式下运行,事实上这个集合就装了NSDefaultRunLoopMode
和UITrackingRunLoopMode
两个模式.而能在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:
最后一个参数需要注意:
如果
waitUntiDone
为YES
,就表示子线程调用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
waitUntiDone
为NO
表示,不需要等子线程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]
很奇怪,控制器都释放了,按理说控制器内部的所有东西都应该释放了呀,我们在ViewController
的dealloc
方法中把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
循环判断如果isStop
为NO
就循环执行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
函数先执行完,那么ViewController
的dealloc
函数也会立马执行完毕,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
循环中打一个断点:
从上图可以看出的确是调用了
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 == nil
就return
,不执行任务:
// 停止按钮的方法
- (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__);
}
这样就完美了.