iOS RunLoop的使用及底層原理

”大師,近日我研讀線程操作之法,發(fā)現(xiàn)了一個問題。我的子線程做完了任務(wù)之后就銷毀了,后續(xù)再來任務(wù),我需要重新開一個子線程,但為什么我手中的這部iPhone18點開一個APP卻能一直運轉(zhuǎn)呢?“

”哦,少俠是說這個問題啊,簡單。為什么能一直運轉(zhuǎn)呢?因為電池電量沒用完?!?/p>

”......“

“哈哈哈,少俠如此好學(xué),老夫就授予你RunLoop使用大法和RunLoop內(nèi)功心法,融匯貫通這兩種秘籍,iPhone18的運行機理會在你眼中變的越來越清晰?!?/p>

RunLoop使用大法

日常開發(fā)中主要使用在一下幾個方面:

  1. NSTimer相關(guān)使用
  2. 子線程?;?/li>
  3. Perform Selector的使用
  4. 更深層次操作
  1. 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)用。

  1. 在子線程中使用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)的子線程中取消。

  1. 子線程?;睢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>

  1. 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。"

  1. 其他操作

當(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

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運行圖

RunLoop運行
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容