”大師,近日我研讀線程操作之法,發(fā)現(xiàn)了一個問題。我的子線程做完了任務(wù)之后就銷毀了,后續(xù)再來任務(wù),我需要重新開一個子線程,但為什么我手中的這部iPhone18點開一個APP卻能一直運轉(zhuǎn)呢?“
”哦,少俠是說這個問題啊,簡單。為什么能一直運轉(zhuǎn)呢?因為電池電量沒用完?!?/p>
”......“
“哈哈哈,少俠如此好學(xué),老夫就授予你RunLoop使用大法和RunLoop內(nèi)功心法,融匯貫通這兩種秘籍,iPhone18的運行機理會在你眼中變的越來越清晰?!?/p>
RunLoop使用大法
日常開發(fā)中主要使用在一下幾個方面:
NSTimer相關(guān)使用- 子線程?;?/li>
Perform Selector的使用- 更深層次操作
- NSTimer在主線程時,列表滑動時也可正常使用。
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(beginUpdateUI) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
創(chuàng)建NSTimer時,默認(rèn)是運行在主線程的NSRunLoopDefaultMode中,當(dāng)UIScrollView滾動時,會切換到UITrackingRunLoopMode,此時定時器會停止調(diào)用。
為了讓定時器正確調(diào)用,可以手動將RunLoop加入到NSRunLoopCommonModes中,此時定時器可以正確調(diào)用。
- 在子線程中使用NSTimer。
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(beginUpdateUI) userInfo:nil repeats:YES];
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
[runloop addTimer:timer forMode:NSDefaultRunLoopMode];
[runloop run];
});
子線程中的RunLoop在第一次獲取時創(chuàng)建,把timer加入到對應(yīng)的Mode之后,子線程中的timer正常工作。注意:如果想要取消子線程中的timer,也要在對應(yīng)的子線程中取消。
- 子線程?;睢FNetworking2.x中有一個示范性的應(yīng)用。
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}
主線程的RunLoop是默認(rèn)開啟的,子線程的RunLoop默認(rèn)是不開啟的,所以子線程執(zhí)行完一個任務(wù)之后,就會被銷毀。如果想讓子線程一直存活,就需要創(chuàng)建RunLoop。NSRunLoop *runLoop = [NSRunLoop currentRunLoop];,如果只是這樣,會發(fā)現(xiàn)子線程依然是做完任務(wù)就退出了。為什么出現(xiàn)這樣的情況呢?因為RunLoop想要運行在一個mode下,需要mode中有Source、Timer、Observer,當(dāng)這些都沒有時RunLoop會退出。所以添加了[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];,相當(dāng)于添加了一個Source,這樣子線程的RunLoop就能一直運行了,后臺線程?;畛晒Α?/p>
-
Perform Selector的使用
- (void)viewDidLoad {
[super viewDidLoad];
[self performSelector:@selector(onMainThread) withObject:nil afterDelay:3];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[[NSThread currentThread] setName:@"gaga"];
[self performSelector:@selector(onOtherThread) withObject:nil afterDelay:3];
});
}
- (void)onMainThread {
NSLog(@"%@", [NSThread currentThread]);
NSLog(@"%@", NSStringFromSelector(_cmd));
}
- (void)onOtherThread {
NSLog(@"%@", [NSThread currentThread]);
NSLog(@"%@", NSStringFromSelector(_cmd));
}
在主線程和子線程中分別調(diào)用performSelector:withObject:afterDelay:,發(fā)現(xiàn)主線程的正常執(zhí)行,但是子線程中調(diào)用的不能執(zhí)行。
performSelector:withObject:afterDelay延時操作相當(dāng)于創(chuàng)建一個Timer添加到當(dāng)前線程的RunLoop中,因為主線程的RunLoop一直存在,所以可以正常執(zhí)行。而子線程的RunLoop在第一次獲取時才會創(chuàng)建,因為我們沒有手動獲取子線程的RunLoop,所以不能正常執(zhí)行。
將子線程修改為如下即可正常執(zhí)行
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[[NSThread currentThread] setName:@"gaga"];
[self performSelector:@selector(onOtherThread) withObject:nil afterDelay:3];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop run];
});
打印子線程的RunLoop可以看到Timer,證實了延時操作會加入Timer的想法。
- (void)onOtherThread {
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
NSLog(@"%@", runLoop);
}
結(jié)果:
timers = <CFArray 0x600001fa6d60 [0x10da31ae8]>{type = mutable-small, count = 0, values = ()},
currently 589343081 (174839084728530) / soft deadline in: 1.84465692e+10 sec (@ -1) / hard deadline in: 1.84465692e+10 sec (@ -1)
“少俠,想一想如果不用延時,直接在主線程和子線程使用performSelector:withObject:,會正常調(diào)用嗎?”
“額...”
"答案是會正常調(diào)用,因為直接使用performSelector:withObject:,相當(dāng)于是個方法調(diào)用,不涉及Runloop。"
- 其他操作
當(dāng)然還有其他的應(yīng)用,比如在子線程中執(zhí)行performSelector這個方法時,如果不創(chuàng)建RunLoop會發(fā)現(xiàn)方法無法調(diào)用。這個留給少俠,自己摸索了。
還有一種應(yīng)用在iOS性能優(yōu)化(中級+): 異步繪制有所體現(xiàn),添加Observer,在RunLoop運行中的合適時機執(zhí)行想要操作的代碼。
“少俠可看好了,以上這些使用方法涵蓋了RunLoop大部分的應(yīng)用層使用?!?/p>
“大師,這們武功修煉起來,確實讓人經(jīng)絡(luò)通暢,身心舒適?!?/p>
“但這些只是招式,少俠如果想要更好的了解iPhone的運行機理,還需修煉對應(yīng)的內(nèi)功心法方方能發(fā)揮出本門武功的最大威力。”
RunLoop內(nèi)功心法
線程與runloop一一對應(yīng),其關(guān)系保存在一個全局的Dictionary里。線程剛創(chuàng)建時沒有RunLoop,如果你不主動獲取,那它就一直不會有。
RunLoop的創(chuàng)建是發(fā)生在第一次獲取時,RunLoop的銷毀是發(fā)生在線程結(jié)束時。RunLoop不能直接創(chuàng)建,只能獲取。
提供了兩個自動獲取的函數(shù):CFRunLoopGetMain()和CFRunLoopGetCurrent(),且只能在一個線程的內(nèi)部獲取其RunLoop(主線程除外)。以上這些關(guān)系,可以通過觀察CF源碼中,CFRunLoop.c 文件里,
_CFRunLoopGet0這個方法查看。

RunLoop主要涉及這幾個類
CFRunLoopRef CFRunLoopModeRef CFRunLoopSourceRef CFRunLoopTimerRef CFRunLoopObserverRef,一個RunLoop包含多個Mode,每個Mode又包含多個Source/Timer/Observer。每次調(diào)用RunLoop的主函數(shù)時,只能指定其中一個Mode,這個Mode被稱作CurrentMode,如果需要切換Mode,只能退出Loop,再重新指定一個Mode進(jìn)入。這樣做主要是為了分隔開不同組的Source/Timer/Observer,讓其互不影響。如果一個mode中一個item都沒有,RunLoop會直接退出,不進(jìn)入循環(huán)。
附一張RunLoop運行圖
