RunLoop六:在实际开发中的应用 之 控制线程生命周期(线程保活) 二

八、 停止 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。
    RunLoop六:在实际开发中的应用 之 控制线程生命周期(线程保活) 二_第1张图片

  • 可以在新创建一个方法,比如 - (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){… … } 循环去掉。只保留 它里面的代码。可不可以 ?代码如下:
    RunLoop六:在实际开发中的应用 之 控制线程生命周期(线程保活) 二_第2张图片

  • 运行程序。可以看到线程启动打印的 initWithBlock 里面的 ---- begin ----
    在这里插入图片描述

  • 点击 橘色界面的橘色区域。可以看到,执行了 [ViewController test]方法。在3线程。但执行玩这个方法后,就调用了[[WYTread alloc] initWithBlock: 方法中的 NSLog(@"%@----end----");

  • 可以看到,如果不加 外循环 while() 循环。那么 runloop只能使用一次。使用完后直接退出。

(三)、如何添加外循环

  • 添加一个标记@property (assign, nonatomic, getter=isStoped) BOOL stopped;
  • 设置标记。在viewDidLoad 方法中写 self.stopped = NO;RunLoop六:在实际开发中的应用 之 控制线程生命周期(线程保活) 二_第3张图片
  • - (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 *)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) 意思是:坏内存访问。
      崩溃信息
  • 为什么会出现 坏内存访问 错误 ?

  1. 当执行 - (void)dealloc时,意味着控制器正在销毁当中,控制器即将死亡。
  2. 这个时候调用 [self stop:nil]; 就会执行 [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO]; 这句代码:
  3. 这句代码就会去 子线程(self.thread子线程),去执行- (void)stopThread方法中的代码
- (void)stopThread {
    // 设置标记为YES
    self.stopped = YES;
    
    // 停止RunLoop
    CFRunLoopStop(CFRunLoopGetCurrent());
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
}
  1. 当执行 - (void)stopThread 方法中的代码,按理说应该停止 runloop。 那为什么没有停止 runloop 方法,还程序崩溃了?
  2. 这是由于 [self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:NO]; 方法中的waitUntilDone 为 NO 导致的。
  3. waitUntilDone = NO 的含义是 :不等子线程执行完 stopThread 这个方法。
  4. 例如:下面的代码 .控制器会同时执行这两个代码。
- (IBAction)stop:(UIButton *)sender {
    // 在子线程调用stop
    [self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:NO];
    NSLog(@"123");
}
  1. 如果 waitUntilDone = Yes。代表会到 self.thread 线程中执行 stopThread方法中的代码。然后在回到 stop 方法中执行 NSLog(@"123");这句话。 然后 stop 方法才算运行完毕。
  2. 当 waitUntilDone = NO 时, 执行完 performSelector:onThread:withObject:waitUntilDone 方法后,就会调用 dealloc 方法。调用 dealloc 方法就代表 控制器已经销毁了。
  3. 与此同时 self.thread 会通过 self 去 调用 stopThread 方法。 self 代表控制器。但是这个时候的控制器已经销毁了(因为调用了 dealloc)。
  4. 控制器已经销毁了。还用 控制器去执行 performSelector:onThread:withObject:waitUntilDone 方法。并且还设置 stopped 属性为 YES , 停止 runloop 等操作。肯定会出现报错。
  5. 报错信息的坏内存访问,是指控制器已经坏掉了。
  6. 解决办法是 waitUntilDone = Yes
  7. waitUntilDone = Yes 代表子线程的代码执行完毕后,stop 方法才会往下走。
  8. 运行程序。程序不崩溃。但又有新的问题,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六:在实际开发中的应用 之 控制线程生命周期(线程保活) 二_第4张图片

  • runLoop 不结束的原因
    RunLoop六:在实际开发中的应用 之 控制线程生命周期(线程保活) 二_第5张图片

  • 是不是可以用强指针引用 weakSelf? 代码如下:
    RunLoop六:在实际开发中的应用 之 控制线程生命周期(线程保活) 二_第6张图片

  • 运行程序。发现情况更糟糕,连控制器都不销毁了。

  • 这是因为 产生了循环引用。
    RunLoop六:在实际开发中的应用 之 控制线程生命周期(线程保活) 二_第7张图片

  • 上面的办法不行,需要修改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 *)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

你可能感兴趣的:(iOS底层)