iOS-NSRunLoop

定義


RunLoop是一種消息處理機制,它通過不斷循環等待的方式被動接收外部信號,然后處理對應事件。當事件都處理完畢時,它處于一種偽掛起狀態,不會消耗系統資源。

RunLoop與線程是一對一的關系,但這并不意味著線程啟動時RunLoop對象就創建了。整個App主線程的RunLoop從運行時就創建,可采用mainRunLoop方法獲得,但子線程的RunLoop會在你調用currentRunLoop方法時創建,并且一個線程只會創建一個RunLoop對象。

讓我們看一段簡單的但不太恰當的代碼,為什么不恰當后面說。

- (void)viewDidLoad {
    [super viewDidLoad];
    NSThread * thread = [[NSThread alloc] initWithTarget:self selector:@selector(initThread) object:nil];
    [thread start];
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [self performSelector:@selector(doSomething) onThread:thread withObject:nil waitUntilDone:NO];
    });
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [self performSelector:@selector(doSomething2) onThread:thread withObject:nil waitUntilDone:NO];
    });
}

- (void)initThread
{
    @autoreleasepool {
        NSLog(@"初始化線程");
        NSRunLoop * runloop = [NSRunLoop currentRunLoop];
        // 讓runloop監聽一個端口消息,這樣runloop就會處于一個有事干的循環,你可以嘗試注釋掉這行看看結果。
        [runloop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runloop run];
        NSLog(@"線程結束");
    }
}

- (void)doSomething
{
    NSLog(@"第一件事");
}

- (void)doSomething2
{
    NSLog(@"第二件事");
}

此時系統輸出:

2017-05-10 22:00:49.169 Test[6704:412836] 初始化線程
2017-05-10 22:00:50.268 Test[6704:412836] 第一件事
2017-05-10 22:00:51.365 Test[6704:412836] 第二件事

是的,發現沒有,「線程結束」這幾個字不會輸出,因為RunLoop使線程處于循環待命的狀態。

這邊提一個事兒,很多文章在介紹不同線程間的通訊時會使用NSMachPort來做示例,但親測在iOS無效,后來查了一些資料發現這種在iOS5之后就不頂用了。

使用


上面的例子中線程監聽了某個端口,并且直接使用[runLoop run]運行會導致一個結果——持有該線程對象的類(比如控制器)無法釋放,因為線程停不下來,即使remove了這個端口監聽,使用CFRunLoopStop也沒用。這也是我比較疑惑的一點,曉得原因的朋友抽空幫我解答一下。

讓我們看下NSRunLoop提供的接口:

// 停不下來的循環
- (void)run; 
// 在到達指定日期后停止循環,期間可以處理多個事件,無法臨時停止
- (void)runUntilDate:(NSDate *)limitDate;
// 在到達指定日期之前處理了一件事就停止,或者到達指定日期后都沒事干就停止,可以用CFRunLoopStop臨時停止
- (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;

根據這幾個接口的性質我們來做第一次改造

- (void)viewDidLoad {
    [super viewDidLoad];
    NSThread * thread = [[NSThread alloc] initWithTarget:self selector:@selector(initThread) object:nil];
    [thread start];
    
    // 立刻執行第一件事,可以嘗試注釋掉,看看有什么不同
    [self performSelector:@selector(doSomething) onThread:thread withObject:nil waitUntilDone:NO];
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [self performSelector:@selector(doSomething2) onThread:thread withObject:nil waitUntilDone:NO];
    });
}

- (void)initThread
{
    @autoreleasepool {
        NSLog(@"初始化線程");
        NSRunLoop * runloop = [NSRunLoop currentRunLoop];
        [runloop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:5]];
        NSLog(@"線程結束");
    }
}
....

得到系統輸出如下,線程在5秒內處理了我指定的2件事,并在5秒后結束了線程,線程被釋放。

2017-05-10 23:03:51.313 Test[10246:642945] 初始化線程
2017-05-10 23:03:51.314 Test[10246:642945] 第一件事
2017-05-10 23:03:52.407 Test[10246:642945] 第二件事
2017-05-10 23:03:56.314 Test[10246:642945] 線程結束

注釋掉處理的第一件事doSomething,不管是采用3個API中的哪一個都會發現系統直接輸出線程結束,第二件事也不會觸發:

2017-05-10 23:07:46.040 Test[10450:657177] 初始化線程
2017-05-10 23:07:46.040 Test[10450:657177] 線程結束

看了下CFRunLoop的具體流程代碼,我認為應該是因為在調用run的api之后,它直接循環了一次,然后覺得已經沒事干了所以就直接停止了loop,而不是等待5秒后才結束。因此在線程start后需要立即執行一個事件使得runloop可以進入掛起待命。

__CFRunLoopModeIsEmpty(runloop, currentMode) 

當你希望有一條線程持續處于待命狀態,并且不會因為無限循環導致類對象無法釋放,那么我們來進行第二次改造:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.stop = NO;
    
    NSThread * thread = [[NSThread alloc] initWithTarget:self selector:@selector(initThread) object:nil];
    [thread start];
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [self performSelector:@selector(doSomething) onThread:thread withObject:nil waitUntilDone:NO];
    });
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [self performSelector:@selector(doSomething2) onThread:thread withObject:nil waitUntilDone:NO];
    });
}

- (void)viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];
    self.stop = YES;
}

- (void)initThread
{
    @autoreleasepool {
        NSLog(@"初始化線程");
        do {
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
        } while (!self.stop);
        NSLog(@"線程結束");
    }
}
....

在這次的例子中,我們使用了do...while循環來控制loop。每一次loop超時時間未0.1秒,即0.1秒時間內沒接收到事件就結束本次loop,如果接收到事件,那么處理完事件時也會結束本次loop。

采用這種方式的好處是,我們可以改變loop的運行模式mode,并且每個loop不會持續太長時間,當stop=YES時,線程能立刻釋放(如果沒有被某個耗時事件卡住的話)。

由于NSRunLoop不是線程安全的,因此在設計的時候可以使用鎖控制,或者采用CFRunLoop的寫法,CFRunLoop是線程安全的。


未完待續...

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容