iOS RunLoop理解

RunLoop概念

一個(gè)APP之所以能在程序運(yùn)行起來(lái)不停止,就是RunLoop的原因,RunLoop就像一個(gè)死循環(huán),等待處理外部手機(jī)操作,網(wǎng)絡(luò)請(qǐng)求以及內(nèi)部通訊等命令,其實(shí)RunLoop是管理線程的一種機(jī)制,這種機(jī)制不僅在iOS上有,在Node.js中的EventLoop,Android中的Looper,都有類似的模式。

RunLoop作用

一個(gè)RunLoop是一個(gè)事件處理環(huán),系統(tǒng)利用這個(gè)事件處理環(huán)來(lái)安排事務(wù),協(xié)調(diào)輸入的各種事件。RunLoop的目的是讓你的線程在有工作的時(shí)候忙碌,沒(méi)有工作的時(shí)候休眠。
RunLoop 實(shí)際上就是一個(gè)對(duì)象,這個(gè)對(duì)象管理了其需要處理的事件和消息,并提供了一個(gè)入口函數(shù)來(lái)執(zhí)行上面 Event Loop 的邏輯。線程執(zhí)行了這個(gè)函數(shù)后,就會(huì)一直處于這個(gè)函數(shù)內(nèi)部 “接受消息->等待->處理” 的循環(huán)中,直到這個(gè)循環(huán)結(jié)束(比如傳入 quit 的消息),函數(shù)返回。

RunLoop和線程

RunLoop是為了線程而生,沒(méi)有線程,它就沒(méi)有存在的必要。RunLoop是線程的基礎(chǔ)架構(gòu)部分。線程和RunLoop之間是以鍵值對(duì)的形式一一對(duì)應(yīng)的,其中key是thread,value是RunLoop。

RunLoop中API

CocoaTouch和CoreFundation都提供了Runloop對(duì)象方便配置和管理線程的RunLoop。OSX/iOS 系統(tǒng)中,提供了兩個(gè)這樣的對(duì)象:NSRunLoop 和 CFRunLoopRef。
CFRunLoopRef 是在 CoreFoundation 框架內(nèi)的,它提供了純 C 函數(shù)的 API,所有這些 API 都是線程安全的。
NSRunLoop 是基于 CFRunLoopRef 的封裝,提供了面向?qū)ο蟮?API,但是這些 API 不是線程安全的。
CocoaTouch層面提供的API比較簡(jiǎn)單:
每個(gè)線程,包括程序的主線程都有與之相應(yīng)的RunLoop對(duì)象。
主線程的RunLoop默認(rèn)是啟動(dòng)的,通過(guò)[NSRunLoop mainRunLoop]獲得。子線程的RunLoop如果要啟動(dòng)需要手動(dòng)調(diào)用。在任何一個(gè)CocoaTouch程序的線程中,都可以通過(guò)NSRunLoop *runloop = [NSRunLoop currentRunLoop]來(lái)獲取到當(dāng)前線程的RunLoop。開(kāi)啟子線程RunLoop,可以使用[[NSRunLoop currentRunLoop]runUntilDate:[NSDate distantFuture]];
那么,開(kāi)啟的RunLoop什么時(shí)候銷毀呢?答案是當(dāng)該線程銷毀時(shí),該線程的RunLoop肯定被銷毀或者RunLoop的mode為空的時(shí)候銷毀,那么怎么判斷mode為空呢?答案是該mode中沒(méi)有observer,source,timer事件就為空。

CoreFundation層面提供的API相對(duì)更多一點(diǎn):
在 CoreFoundation 里面關(guān)于 RunLoop 有5個(gè)類:
1.CFRunLoopRef
它只提供了兩個(gè)自動(dòng)獲取的函數(shù):CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。

2.CFRunLoopModeRef
一個(gè) RunLoop 包含若干個(gè) Mode,每個(gè) Mode 又包含若干個(gè) Source/Timer/Observer。但是每次RunLoop運(yùn)行時(shí),只能指定其中一個(gè)Mode,這個(gè)Mode被稱作 CurrentMode。如果需要切換 Mode,只能退出 Loop,再重新指定一個(gè) Mode 進(jìn)入。這樣做主要是為了分隔開(kāi)不同組的 Source/Timer/Observer,讓其互不影響。

3.CFRunLoopSourceRef
處理事件源。Source有兩個(gè)版本:Source0 和 Source1。分別處理不同事件,Source0處理外部交互事件,Source1處理內(nèi)部通信等事件。

4.CFRunLoopTimerRef
處理Timer事件。

5.CFRunLoopObserverRef
觀察者,監(jiān)聽(tīng)RunLoop不同狀態(tài)。

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry         = (1UL << 0), // 即將進(jìn)入Loop
    kCFRunLoopBeforeTimers  = (1UL << 1), // 即將處理 Timer
    kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Source
    kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進(jìn)入休眠
    kCFRunLoopAfterWaiting  = (1UL << 6), // 剛從休眠中喚醒
    kCFRunLoopExit          = (1UL << 7), // 即將退出Loop
};
-(void)observerRunLoop
{
    //監(jiān)聽(tīng)kCFRunLoopDefaultMode下的RunLoop狀態(tài)
    CFRunLoopMode mode =kCFRunLoopDefaultMode;
    
    CFRunLoopObserverRef observer=CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), 0, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        switch (activity) {
            case kCFRunLoopEntry:
                SDLog(@"RunLoop啟動(dòng)");
                break;
            case kCFRunLoopBeforeWaiting:
                SDLog(@"RunLoop即將休眠");
                break;
            case kCFRunLoopAfterWaiting:
                SDLog(@"RunLoop被喚醒");
                break;
            case kCFRunLoopBeforeTimers:
                SDLog(@"RunLoop即將處理Timers");
                break;
            case kCFRunLoopBeforeSources:
                SDLog(@"RunLoop即將處理Sources");
                break;
            case kCFRunLoopExit:
                SDLog(@"RunLoop退出");
                break;
                
            default:
                break;
        }
    });
    
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, mode);
}

RunLoop關(guān)于mode的應(yīng)用

RunLoop包含5中mode(模式),每種模式接受不同的事件源。
a. kCFRunLoopDefaultMode: App的默認(rèn) Mode,通常主線程是在這個(gè) Mode 下運(yùn)行的。
b. UITrackingRunLoopMode: 界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動(dòng),保證界面滑動(dòng)時(shí)不受其他 Mode 影響。
c. UIInitializationRunLoopMode: 在剛啟動(dòng) App 時(shí)第進(jìn)入的第一個(gè) Mode,啟動(dòng)完成后就不再使用。
d.GSEventReceiveRunLoopMode: 接受系統(tǒng)事件的內(nèi)部 Mode,通常用不到。
e. kCFRunLoopCommonModes: 這是一個(gè)占位的 Mode,沒(méi)有實(shí)際作用。

每種mode中又包含三種item(soures,observer,timer),如果一個(gè)Mode中一個(gè)item都沒(méi)有,則這個(gè)RunLoop會(huì)直接退出。我們的RunLoop要想工作,必須要讓它存在一個(gè)Item(source,observer或者timer),主線程之所以能夠一直存在,并且隨時(shí)準(zhǔn)備被喚醒就是因?yàn)橄到y(tǒng)為其添加了很多item。

mode:最常見(jiàn)的兩種模式,默認(rèn)模式(空閑)NSDefaultRunLoopMode,UI模式UITrackingRunLoopMode,比如UI相關(guān)事件(滑動(dòng),點(diǎn)擊等),就是主線程RunLoop在UITrackingRunLoopMode模式下進(jìn)行監(jiān)視處理的。

1.例如:performSelector方法
//performSelector默認(rèn)是在當(dāng)前RunLoop的默認(rèn)模式下執(zhí)行方法
[self performSelector:@selector(test) withObject:self];
//可以通過(guò)performSelector指定RunLoop模式的方式解決RunLoop問(wèn)題
    [self performSelector:@selector(test) withObject:self afterDelay:3 inModes:@[NSRunLoopCommonModes]];
2.例如:處理滑動(dòng)時(shí)間和定時(shí)器沖突的問(wèn)題

主線程調(diào)用timer,添加到NSDefaultRunLoopMode的RunLoop中,此時(shí)滑動(dòng)scrollview,那么timer將停止打印,若添加到UITrackingRunLoopMode的RunLoop中,滑動(dòng)scrollview,那么timer可以打印,但是不滑動(dòng)時(shí)不打印。原因就是滑動(dòng)時(shí)主線程RunLoop在UITrackingRunLoopMode模式下運(yùn)行,timer如果放到該模式下就能檢測(cè)到,如果放到NSDefaultRunLoopMode模式下就檢測(cè)不到。如果想在兩種模式下都檢測(cè)到,就都需要添加,當(dāng)然,iOS為我們提供了復(fù)合的Mode-NSRunLoopCommonModes,包含上述兩種模式。
解決方法就是添加到NSRunLoopCommonModes。

NSTimer * timer=[NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
    }];
    
 [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
    
 [timer setFireDate:[NSDate distantPast]];

還有種解決辦法,就是子線程執(zhí)行timer,或者使用dispatch_source_set_timer在全局并行隊(duì)列執(zhí)行。

dispatch_async(dispatch_get_global_queue(0, 0), ^{
        
        timer=[NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
            
            
        }];
        
        [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
        
        [timer setFireDate:[NSDate distantPast]];
        
       //注意要開(kāi)啟子線程的RunLoop,因?yàn)樽泳€程RunLoop默認(rèn)關(guān)閉

        [[NSRunLoop currentRunLoop]runUntilDate:[NSDate distantFuture]];
    });
timer=dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
    dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
    dispatch_source_set_event_handler(timer, ^{
        
    });
    dispatch_resume(timer);
3.開(kāi)啟常駐子線程

NSRunLoop提供了添加item的API,也可以通過(guò)添加item讓子線程RunLoop活下來(lái)。

[NSRunLoop currentRunLoop]addTimer:(nonnull NSTimer *) forMode:(nonnull NSRunLoopMode)

[[NSRunLoop currentRunLoop]addPort:[[NSPort alloc]init] forMode:NSDefaultRunLoopMode];

[NSRunLoop currentRunLoop]addObserver:(nonnull NSObject *) forKeyPath:(nonnull NSString *) options:(NSKeyValueObservingOptions) context:(nullable void *):

例如:讓一個(gè)子線程處理處理完一個(gè)任務(wù)之后,再處理另一個(gè)任務(wù)。

/*單純使用線程間通訊是做不到的,因?yàn)樽泳€程一旦執(zhí)行完任務(wù)就銷毀了啊,無(wú)法再被喚醒,除非使用該子線程常駐不被銷毀。
*/
[self performSelector:@selector(test) onThread:子線程 withObject:nil waitUntilDone:YES];
/*這樣就可以考慮在子線程中開(kāi)啟該子線程RunLoop,并讓RunLoop做任務(wù)讓該子線程保持存活,那么做什么任務(wù)呢,根據(jù)之前的知識(shí),可以是source事件,可以是timer事件,也可以是observer,推薦使用基于端口的source0事件。
*/
    NSRunLoop *runloop=[NSRunLoop currentRunLoop];
    [runloop addPort:[NSPort port]forMode:NSDefaultRunLoopMode];
    [runloop run];

RunLoop的內(nèi)部邏輯

RunLoop_1.png

RunLoop循環(huán)內(nèi)部會(huì)不斷創(chuàng)建和銷毀自動(dòng)釋放池處理一些垃圾數(shù)據(jù)(使用過(guò)的變量等)
自動(dòng)釋放池第一次創(chuàng)建:當(dāng)RunLoop啟動(dòng)時(shí)
自動(dòng)釋放池最后一次銷毀:當(dāng)RunLoop銷毀時(shí)
自動(dòng)釋放池其他時(shí)間創(chuàng)建和銷毀:當(dāng)RunLoop即將進(jìn)入休眠的時(shí)候,釋放之前的自動(dòng)釋放池(回收數(shù)據(jù)),創(chuàng)建新的自動(dòng)釋放池。

RunLoop在常用SDK中應(yīng)用場(chǎng)景

AFNetworking(相當(dāng)于一個(gè)線程常駐的方式)
這個(gè)類是基于 NSURLConnection 構(gòu)建的,其希望能在后臺(tái)線程接收 Delegate 回調(diào)。為此 AFNetworking 單獨(dú)創(chuàng)建了一個(gè)線程,并在這個(gè)線程中啟動(dòng)了一個(gè) RunLoop:

+ (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 啟動(dòng)前內(nèi)部必須要有至少一個(gè) Timer/Observer/Source,所以 AFNetworking 在 [runLoop run] 之前先創(chuàng)建了一個(gè)新的 NSMachPort 添加進(jìn)去了。通常情況下,調(diào)用者需要持有這個(gè) NSMachPort (mach_port) 并在外部線程通過(guò)這個(gè) port 發(fā)送消息到 loop 內(nèi);但此處添加 port 只是為了讓 RunLoop 不至于退出,并沒(méi)有用于實(shí)際的發(fā)送消息。

據(jù)說(shuō)使用NSURLConnection的老前輩都需要通過(guò)RunLoop調(diào)試子線程的網(wǎng)絡(luò)回調(diào)。

PerformSelecter
當(dāng)調(diào)用 NSObject 的 performSelecter:afterDelay: 后,實(shí)際上其內(nèi)部會(huì)創(chuàng)建一個(gè) Timer 并添加到當(dāng)前線程的 RunLoop 中。所以如果當(dāng)前線程沒(méi)有 RunLoop,則這個(gè)方法會(huì)失效。
當(dāng)調(diào)用 performSelector:onThread: 時(shí),實(shí)際上其會(huì)創(chuàng)建一個(gè) Timer 加到對(duì)應(yīng)的線程去,同樣的,如果對(duì)應(yīng)線程沒(méi)有 RunLoop 該方法也會(huì)失效。

AsyncDisplayKit
ASDK 仿照 QuartzCore/UIKit 框架的模式,實(shí)現(xiàn)了一套類似的界面更新的機(jī)制:即在主線程的 RunLoop 中添加一個(gè) Observer,監(jiān)聽(tīng)了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件,在收到回調(diào)時(shí),遍歷所有之前放入隊(duì)列的待處理的任務(wù),然后一一執(zhí)行。

參考文章:https://blog.ibireme.com/2015/05/18/runloop/

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

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

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