1. 程式人生 > >RunLoop六:在實際開發中的應用 之 控制執行緒生命週期(執行緒保活) 二

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。
    在這裡插入圖片描述

  • 可以在新建立一個方法,比如 - (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) 意思是:壞記憶體訪問。
      崩潰資訊
  • 為什麼會出現 壞記憶體訪問 錯誤 ?

  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 不結束的原因
    在這裡插入圖片描述

  • 是不是可以用強指標引用 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