八、 停止 NSRunLoop 运行
上章提到了 ,只有控制器释放了。线程没有被释放。这是因为 代码 卡在了 [[NSRunLoop currentRunLoop] run];
这句代码.
-
任务执行完成后,线程会销毁。但是 有 run 方法的话。代表系统一直在执行run 方法。所以任务并没有执行完成 。
-
也就是任务没有执行结束,self.thread 线程并不会销毁。
-
[[NSRunLoop currentRunLoop] run];
会让线程一直运行。这就会引出问题。 -
self.thread
线程属于控制器的一个属性。控制器死亡,那线程也应该死亡。除非self.thread
线程全项目都在使用。别的控制器在这个线程中也可以做事情。 -
如果希望能够控制 NSRunLoop 的声明周期,比如:想让NSRunLoop 死,那 NSRunLoop 就死。这样就需要 修改 一下代码。
-
让线程活下来,调用 start 方法即可。但是如果是死亡呢?
-
想让线程跟随控制器的生命周期。那就需要在 dealloc 方法中写 让 线程 死亡的方法。 这样就可以让 runLoop 死亡。就可以打印 initWithBlock 方法中的
NSLog(@"---end---");
.就可以说线程已经死亡了。 -
下面的代码正确么?
* 在ViewController控制器中的- (void) dealloc
方法 写CFRunLoopStop(CFRunLoopGetCurrent());
。
* 是错误的写法。因为ViewController 的 dealloc 方法默认是在主线程里面调用的。所有下方图片的写法是在停止主线程的RunLoop。而不是停止 self.thread 线程的RunLoop。
-
可以在新创建一个方法,比如
- (void) stop
方法,在这个方法中写CFRunLoopStop(CFRunLoopGetCurrent());
。
// 用于停止子线程的RunLoop
- (void)stop {
// 停止RunLoop
CFRunLoopStop(CFRunLoopGetCurrent());
NSLog(@"%s %@", __func__, [NSThread currentThread]);
}
- 让 stop 方法 在 self.thread 线程中调用即可。
- (void)dealloc {
NSLog(@"%s", __func__);
// 在子线程调用stop
[self performSelector:@selector(stop) onThread:self.thread withObject:nil waitUntilDone:NO];
}
-
运行代码。可以看到 调用了 stop 方法。可以看到里面的 NSLog 打印了。但是 runLoop 并没有停止。因为 没有打印 initWithBlock 中的 NSLog(@"— end —"); 这句代码。
-
那可能会 想 是不是 因为控制器已经要销毁了。你在快销毁的时候才执行是不是来不及调用 ?
-
针对这个问题。修改下界面。
-
在 橘色界面创建一个 button ,在button 的点击方法写:
- (IBAction)stop { // button 点击方法
// 在子线程调用stop
// 这个方法是在主线程 调用的。
// 而 stopThread 方法是在子线程调用的
[self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:NO];
}
// 用于停止子线程的RunLoop
- (void)stopThread { // button 点击方法 中调用的方法
// 停止RunLoop
CFRunLoopStop(CFRunLoopGetCurrent());
NSLog(@"%s %@", __func__, [NSThread currentThread]);
}
- 运行代码。可以看到 调用了 stopThread 方法。因为里面的 NSLog 已经打印了。但是 runLoop 并没有停止。因为 没有打印 initWithBlock方法中的 NSLog(@"— end —"); 这句代码。
九、RunLoop 中的 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
- 解释第一句:在 NSDefaultRunLoopMode 模式下跑起来,并重复调用
runMode:beforeDate:
方法。 - 相当于 run 方法的底层一直在重复调用
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate: nil ]
方法 - 解释第二句:换句话说:run方法的作用就是开启一个无限的循环(也就是不会死掉的循环)。相当于写了一个死循环 while (1).
- 而 在 self.thread 线程调用的
CFRunLoopStop(CFRunLoopGetCurrent());
方法,不是停止 run 方法。而是 停止run方法里面的一次循环(当前的runLoop)。 - 因为 run 方法 不能死亡,所以最好还是 自己实现一个 循环。
- NSRunLoop 的 run 方法是无法停止的,它专门用于开启一个永不销毁的线程(NSRunLoop)
十、自己实现 循环
(一)、创建 runloop
-
现在是要把 RunLoop 跑起来,可以使用这句代码:
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate: [NSDate dateWithTimeIntervalSinceNow:5] ]
-
[NSDate dateWithTimeIntervalSinceNow:5] 代表从当前时间在加上5秒。例如:当前时间是 9点16分30秒,这句话就是在 9点16分35秒的时候过时。
-
如果RunLoop 开始休眠,休眠到 35秒的时候,RunLoop 会自动退出。
-
当我们希望runloop 不要退出,那就给
beforeDate
传一个不会过期的时间.[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
. -
[NSDate distantFuture] : 遥远的未来。
(二)、有问题的外循环while(1)
while (1) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
-
现在的代码是这样的。
-
需要改的地方是 while(1).
-
while括号里面不要用1。如果用1 ,那么当调用
- (void)stopThread
方法停掉 当前 runloop 。就又会 重新开启一个线程。 -
那如果把 while(1){… … } 循环去掉。只保留 它里面的代码。可不可以 ?代码如下:
-
运行程序。可以看到线程启动打印的 initWithBlock 里面的 ---- begin ----
-
点击 橘色界面的橘色区域。可以看到,执行了
[ViewController test]
方法。在3线程。但执行玩这个方法后,就调用了[[WYTread alloc] initWithBlock:
方法中的NSLog(@"%@----end----");
-
可以看到,如果不加 外循环 while() 循环。那么 runloop只能使用一次。使用完后直接退出。
(三)、如何添加外循环
- 添加一个标记
@property (assign, nonatomic, getter=isStoped) BOOL stopped;
- 设置标记。在
viewDidLoad
方法中写self.stopped = NO;
。 - 在
- (void)stopThread
方法中,设置标记
// 设置标记为YES
self.stopped = YES;
(四)、全部代码
#import "ViewController.h"
#import "WYTread.h"
@interface ViewController ()
@property (strong, nonatomic) WYTread *thread;
@property (assign, nonatomic, getter=isStoped) BOOL stopped;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
self.stopped = NO;
self.thread = [[WYTread alloc] initWithBlock:^{
NSLog(@"%@----begin----", [NSThread currentThread]);
// 往RunLoop里面添加Source\Timer\Observer
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
while (!weakSelf.isStoped) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
NSLog(@"%@----end----", [NSThread currentThread]);
}];
[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]);
}
- (IBAction)stop:(UIButton *)sender {
// 在子线程调用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]);
}
- (void)dealloc {
NSLog(@"%s", __func__);
}
@end
十一、EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
崩溃信息
-
上面的代码有一个问题。RunLoop是进入到ViewController 后就自动创建的。那最好在这个控制器销毁的时候让runloop也销毁。
-
也就是 当进入到橘色界面自动开启了 RunLoop 以后,不点击停止,直接点击 back 返回按钮。 也可以让创建的 runloop 销毁。
-
但现在的问题是,不能够 自动让 runloop 销毁。需要点击停止按钮。
-
如何实现 让runloop 在控制器销毁的时候也跟着销毁呢?
-
如果想让 点击 back 返回按钮时 停止。可以在
- (void)dealloc
中 调用- (IBAction)stop
方法。 -
运行程序。进入到橘色界面启动了 RunLoop 以后,不点击停止,直接点击 back 返回按钮。 程序会崩溃。
- 崩溃信息是:
Thread 8: EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
意思是:坏内存访问。
- 崩溃信息是:
-
为什么会出现 坏内存访问 错误 ?
- 当执行
- (void)dealloc
时,意味着控制器正在销毁当中,控制器即将死亡。 - 这个时候调用
[self stop:nil];
就会执行[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
这句代码: - 这句代码就会去 子线程(self.thread子线程),去执行
- (void)stopThread
方法中的代码
- (void)stopThread {
// 设置标记为YES
self.stopped = YES;
// 停止RunLoop
CFRunLoopStop(CFRunLoopGetCurrent());
NSLog(@"%s %@", __func__, [NSThread currentThread]);
}
- 当执行
- (void)stopThread
方法中的代码,按理说应该停止 runloop。 那为什么没有停止 runloop 方法,还程序崩溃了? - 这是由于
[self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:NO];
方法中的waitUntilDone
为 NO 导致的。 - waitUntilDone = NO 的含义是 :不等子线程执行完 stopThread 这个方法。
- 例如:下面的代码 .控制器会同时执行这两个代码。
- (IBAction)stop:(UIButton *)sender {
// 在子线程调用stop
[self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:NO];
NSLog(@"123");
}
- 如果 waitUntilDone = Yes。代表会到 self.thread 线程中执行
stopThread
方法中的代码。然后在回到 stop 方法中执行NSLog(@"123");
这句话。 然后 stop 方法才算运行完毕。 - 当 waitUntilDone = NO 时, 执行完 performSelector:onThread:withObject:waitUntilDone 方法后,就会调用 dealloc 方法。调用 dealloc 方法就代表 控制器已经销毁了。
- 与此同时 self.thread 会通过 self 去 调用 stopThread 方法。 self 代表控制器。但是这个时候的控制器已经销毁了(因为调用了 dealloc)。
- 控制器已经销毁了。还用 控制器去执行
performSelector:onThread:withObject:waitUntilDone
方法。并且还设置 stopped 属性为 YES , 停止 runloop 等操作。肯定会出现报错。 - 报错信息的坏内存访问,是指控制器已经坏掉了。
- 解决办法是 waitUntilDone = Yes
- waitUntilDone = Yes 代表子线程的代码执行完毕后,stop 方法才会往下走。
- 运行程序。程序不崩溃。但又有新的问题,runloop 没有停掉。因为没有打印
[[WYTread alloc] initWithBlock
大括号中的 end。
十二、weakSelf 问题
-
已经设置了 stopped 属性 为 yes,为什么 还会 再次开启 runloop?
-
我们在
while (!weakSelf.isStoped)
中打印一下 weakSelf .结果为 null . -
也就是说 当 调用了 dealloc 后, weakfSelf 为null,也就是 NO。
-
while(!weakSelf.isStoped)
-
while(!NO)
-
while(YES)
-
所以还是可以进入到 循环里面。
-
这个时候需要修改下 循环语句的判断条件即可。
-
while (weakSelf && !weakSelf.isStoped).
-
当 weakSelf 为null 时,第一个就为 NO,这语句就不会再次判断 后面的结果。
-
现在控制器的执行顺序如下图
-
runLoop 不结束的原因
-
是不是可以用强指针引用 weakSelf? 代码如下:
-
运行程序。发现情况更糟糕,连控制器都不销毁了。
-
这是因为 产生了循环引用。
-
上面的办法不行,需要修改while循环中的判断
while (weakSelf && !weakSelf.isStoped)
全部代买
//
// ViewController.m
// RunLoop源码
//
// Created by study on 2018/10/19.
// Copyright © 2018年 WY. All rights reserved.
//
#import "ViewController.h"
#import "WYTread.h"
@interface ViewController ()
@property (strong, nonatomic) WYTread *thread;
@property (assign, nonatomic, getter=isStoped) BOOL stopped;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
self.stopped = NO;
self.thread = [[WYTread alloc] initWithBlock:^{
__strong typeof (weakSelf) strongSelf = weakSelf;
NSLog(@"%@----begin----", [NSThread currentThread]);
// 往RunLoop里面添加Source\Timer\Observer
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
while (strongSelf && !strongSelf.isStoped) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
NSLog(@"%@----end----", [NSThread currentThread]);
}];
[self.thread start];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
if (!self.thread) { return; }
[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}
// 子线程需要执行的任务
- (void)test {
NSLog(@"%s %@", __func__, [NSThread currentThread]);
}
- (IBAction)stop:(UIButton *)sender {
if (!self.thread) { return; }
// 在子线程调用stop
[self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:YES];
}
// 用于停止子线程的RunLoop
- (void)stopThread {
// 设置标记为YES
self.stopped = YES;
// 停止RunLoop
CFRunLoopStop(CFRunLoopGetCurrent());
NSLog(@"%s %@", __func__, [NSThread currentThread]);
// 清空线程
self.thread = nil;
}
- (void)dealloc {
NSLog(@"%s", __func__);
[self stop:nil];
}
@end