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)
意思是:壞記憶體訪問。
- 崩潰資訊是:
-
為什麼會出現 壞記憶體訪問 錯誤 ?
- 當執行
- (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