定義
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是線程安全的。
未完待續...