Runloop的概念和總結(jié)

RunLoop 的概念
一般來講,一個(gè)線程一次只能執(zhí)行一個(gè)任務(wù),執(zhí)行完成后線程就會(huì)退出。如果我們需要一個(gè)機(jī)制,讓線程能隨時(shí)處理事件但并不退出,通常的代碼邏輯是這樣的:

function loop() {
    initialize();
    do {
        var message = get_next_message();
        process_message(message);
    } while (message != quit);
}

這種模型通常被稱作 Event Loop。 Event Loop 在很多系統(tǒng)和框架里都有實(shí)現(xiàn),比如 Node.js 的事件處理,比如 Windows 程序的消息循環(huán),再比如 OSX/iOS 里的 RunLoop。實(shí)現(xiàn)這種模型的關(guān)鍵點(diǎn)在于:如何管理事件/消息,如何讓線程在沒有處理消息時(shí)休眠以避免資源占用、在有消息到來時(shí)立刻被喚醒。

所以,RunLoop 實(shí)際上就是一個(gè)對(duì)象,這個(gè)對(duì)象管理了其需要處理的事件和消息,并提供了一個(gè)入口函數(shù)來執(zhí)行上面 Event Loop 的邏輯。線程執(zhí)行了這個(gè)函數(shù)后,就會(huì)一直處于這個(gè)函數(shù)內(nèi)部 "接受消息->等待->處理" 的循環(huán)中,直到這個(gè)循環(huán)結(jié)束(比如傳入 quit 的消息),函數(shù)返回。

OSX/iOS 系統(tǒng)中,提供了兩個(gè)這樣的對(duì)象:NSRunLoop 和 CFRunLoopRef。
CFRunLoopRef 是在 CoreFoundation 框架內(nèi)的,它提供了純 C 函數(shù)的 API,所有這些 API 都是線程安全的。
NSRunLoop 是基于 CFRunLoopRef 的封裝,提供了面向?qū)ο蟮?API,但是這些 API 不是線程安全的。

CFRunLoopRef 的代碼是開源的,你可以在這里 http://opensource.apple.com/tarballs/CF/ 下載到整個(gè) CoreFoundation 的源碼來查看。

(Update: Swift 開源后,蘋果又維護(hù)了一個(gè)跨平臺(tái)的 CoreFoundation 版本:https://github.com/apple/swift-corelibs-foundation/,這個(gè)版本的源碼可能和現(xiàn)有 iOS 系統(tǒng)中的實(shí)現(xiàn)略不一樣,但更容易編譯,而且已經(jīng)適配了 Linux/Windows。)

RunLoop 與線程的關(guān)系
首先,iOS 開發(fā)中能遇到兩個(gè)線程對(duì)象: pthread_t 和 NSThread。過去蘋果有份文檔標(biāo)明了 NSThread 只是 pthread_t 的封裝,但那份文檔已經(jīng)失效了,現(xiàn)在它們也有可能都是直接包裝自最底層的 mach thread。蘋果并沒有提供這兩個(gè)對(duì)象相互轉(zhuǎn)換的接口,但不管怎么樣,可以肯定的是 pthread_t 和 NSThread 是一一對(duì)應(yīng)的。比如,你可以通過 pthread_main_thread_np() 或 [NSThread mainThread] 來獲取主線程;也可以通過 pthread_self() 或 [NSThread currentThread] 來獲取當(dāng)前線程。CFRunLoop 是基于 pthread 來管理的。

蘋果不允許直接創(chuàng)建 RunLoop,它只提供了兩個(gè)自動(dòng)獲取的函數(shù):CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。 這兩個(gè)函數(shù)內(nèi)部的邏輯大概是下面這樣:

/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 訪問 loopsDic 時(shí)的鎖
static CFSpinLock_t loopsLock;

/// 獲取一個(gè) pthread 對(duì)應(yīng)的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
OSSpinLockLock(&loopsLock);

if (!loopsDic) {
    // 第一次進(jìn)入時(shí),初始化全局Dic,并先為主線程創(chuàng)建一個(gè) RunLoop。
    loopsDic = CFDictionaryCreateMutable();
    CFRunLoopRef mainLoop = _CFRunLoopCreate();
    CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
}

/// 直接從 Dictionary 里獲取。
CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));

if (!loop) {
    /// 取不到時(shí),創(chuàng)建一個(gè)
    loop = _CFRunLoopCreate();
    CFDictionarySetValue(loopsDic, thread, loop);
    /// 注冊一個(gè)回調(diào),當(dāng)線程銷毀時(shí),順便也銷毀其對(duì)應(yīng)的 RunLoop。
    _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
}

    OSSpinLockUnLock(&loopsLock);
    return loop;
}

CFRunLoopRef CFRunLoopGetMain() {
    return _CFRunLoopGet(pthread_main_thread_np());
}

CFRunLoopRef CFRunLoopGetCurrent() {
    return _CFRunLoopGet(pthread_self());
}

從上面的代碼可以看出,線程和 RunLoop 之間是一一對(duì)應(yīng)的,其關(guān)系是保存在一個(gè)全局的 Dictionary 里。線程剛創(chuàng)建時(shí)并沒有 RunLoop,如果你不主動(dòng)獲取,那它一直都不會(huì)有。RunLoop 的創(chuàng)建是發(fā)生在第一次獲取時(shí),RunLoop 的銷毀是發(fā)生在線程結(jié)束時(shí)。你只能在一個(gè)線程的內(nèi)部獲取其 RunLoop(主線程除外)。

http://blog.ibireme.com/2015/05/18/runloop/

runloop是iOS系統(tǒng)對(duì)事件接受和分發(fā)機(jī)制的一個(gè)實(shí)現(xiàn),是線程的基本架構(gòu)部分。一個(gè)runloop就是一個(gè)事件處理循環(huán),用來不停的調(diào)配工作以及處理輸入事件。 使用runloop的目的是使你的線程在有工作的時(shí)候工作,沒有的時(shí)候休眠,以達(dá)到節(jié)省cpu的目的。runloop的管理并不完全是自動(dòng),當(dāng)我們創(chuàng)建一個(gè)子線程時(shí),我們必須在適當(dāng)?shù)臅r(shí)候啟動(dòng)Runloop并正確響應(yīng)事件。 子線程不需要顯式的創(chuàng)建RunLoop,每個(gè)線程,包括程序的主線程都有與之對(duì)應(yīng)的RunLoop對(duì)象,但是自己創(chuàng)建的線程需要手動(dòng)運(yùn)行RunLoop的運(yùn)行方法。不過程序啟動(dòng)時(shí),主線程會(huì)自動(dòng)創(chuàng)建并運(yùn)行RunLoop。

在沒有手加Autorelease Pool的情況下,Autorelease對(duì)象是在當(dāng)前的runloop迭代結(jié)束時(shí)釋放的,而它能夠釋放的原因是系統(tǒng)在每個(gè)runloop迭代中都加入了自動(dòng)釋放池Push和Pop.

什么是Runloop

Runloop,顧名思義就是運(yùn)行的循環(huán)。簡單理解就是多線程機(jī)制中的基礎(chǔ),它能夠接收外部事件的輸入,并且在有事件的時(shí)候保持運(yùn)行,在沒有事件的時(shí)候進(jìn)入休眠。并且它對(duì)于線程的消息處理機(jī)制進(jìn)行了很好的封裝。Runloop的作用就是要減少cpu做無謂的空轉(zhuǎn),cpu可在空閑的時(shí)候休眠,以節(jié)約電量。

對(duì)于線程來說,每一個(gè)線程都有一個(gè)runloop對(duì)象,是否能向某個(gè)線程的runloop發(fā)送事件取決于你是否啟動(dòng)了這個(gè)runloop,系統(tǒng)會(huì)默認(rèn)在你的程序啟動(dòng)的時(shí)候運(yùn)行主線程上的runloop,但是你自定義創(chuàng)建出來的線程可以不需要運(yùn)行runloop,一些第三方框架,例如AFNetworking,就有在自己的線程上維護(hù)一個(gè)runloop對(duì)象。

在 Core Foundation 里面關(guān)于 RunLoop 有5個(gè)類:

CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef

他們的關(guān)系可以從NSRunloop對(duì)象的結(jié)構(gòu)定義中得出。首先,runloop對(duì)象在Cocoa和Core Foundation中都有實(shí)現(xiàn),但是他們做了很好的橋接,你可以直接調(diào)用

CFRunLoopRef runLoopRef = currentThreadRunLoop.getCFRunLoop;

來獲取一個(gè)CoreFoundation中的runloop對(duì)象。然后,當(dāng)你在查看NSRunloop的結(jié)構(gòu)的時(shí)候,你應(yīng)該能看到:

<CFRunLoop 0x7fd360f5af30 [0x1090a1180]>{wakeup port = 0x4507, stopped = false, ignoreWakeUps = true, 
current mode = (none),
common modes = <CFBasicHash 0x7fd360f5a470 [0x1090a1180]>{type = mutable set, count = 1,
entries =>
2 : <CFString 0x10907d080 [0x1090a1180]>{contents = "kCFRunLoopDefaultMode"}},
common mode items = (null),
modes = <CFBasicHash 0x7fd360f5b2b0 [0x1090a1180]>{type = mutable set, count = 1,
entries =>
2 : <CFRunLoopMode 0x7fd360f5aff0 [0x1090a1180]>{name = kCFRunLoopDefaultMode, port set = 0x4703, timer port = 0x4803, 
sources0 = (null),
sources1 = (null),
observers = <CFArray 0x7fd360f5b1a0 [0x1090a1180]>{type = mutable-small, count = 1, values = (
0 : <CFRunLoopObserver 0x7fd360f5c7f0 [0x1090a1180]>{valid = Yes, activities = 0xfffffff, repeats = Yes, order = 0, callout = currentRunLoopObserver (0x10855b340), context = <CFRunLoopObserver context 0x7fd361213d70>}
)},
timers = <CFArray 0x7fd360e020d0 [0x1090a1180]>{type = mutable-small, count = 1, values = (
0 : <CFRunLoopTimer 0x7fd360e01f90 [0x1090a1180]>{valid = Yes, firing = No, interval = 1, tolerance = 0, next fire date = 463742311 (-2.53606331 @ 23607719248079), callout = (NSTimer) [SCCustomThread handleTimerTask] (0x1086416f1 / 0x10855b560) (/Users/useruser/Library/Developer/CoreSimulator/Devices/424D3C6E-8DC0-418B-A2EC-8EDF89507348/data/Containers/Bundle/Application/4D07AF38-9BFC-4617-BAE0-4CB0D7966CC8/runloopTest.app/runloopTest), context = <CFRunLoopTimer context 0x7fd360e01f70>}
)},
currently 463742313 (23610255156065) / soft deadline in: 1.84467441e+10 sec (@ 23607719248079) / hard deadline in: 1.84467441e+10 sec (@ 23607719248079)
},}}

可以看到一個(gè)runloop對(duì)象包含各種Mode——currentMode,common mode,modes等等,這里的示例我只指定了一個(gè)defaultMode。每個(gè)mode對(duì)應(yīng)了source,observers和timers。

也許你會(huì)注意到 source 包括了source0和source1兩個(gè)版本。

Source0 只包含了一個(gè)回調(diào)(函數(shù)指針),它并不能主動(dòng)觸發(fā)事件。使用時(shí),你需要先調(diào)用 CFRunLoopSourceSignal(source),將這個(gè) Source 標(biāo)記為待處理,然后手動(dòng)調(diào)用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop,讓其處理這個(gè)事件。
Source1 包含了一個(gè) mach_port 和一個(gè)回調(diào)(函數(shù)指針),被用于通過內(nèi)核和其他線程相互發(fā)送消息。這種 Source 能主動(dòng)喚醒 RunLoop 的線程。
CFRunLoopObserver類型的對(duì)象也可以稱之為觀察者。每個(gè)觀察者都包含了一個(gè)回調(diào),當(dāng)runloop的狀態(tài)發(fā)生變化時(shí),你可以通過回調(diào)來知道當(dāng)前的狀態(tài)。

在你的程序中,runloop的過程實(shí)際上是一個(gè)無限循環(huán)的循環(huán)體,這個(gè)循環(huán)體是由你的程序來運(yùn)行的。主線程的runloop由于系統(tǒng)已經(jīng)實(shí)現(xiàn)并且沒有它程序就不能運(yùn)行,因此不需要我們手動(dòng)去運(yùn)行這個(gè)runloop。然而如果我們需要在自定義的線程中使用到runloop,我們則需要用一個(gè)do…while循環(huán)來驅(qū)動(dòng)它。而runloop對(duì)象負(fù)責(zé)不斷地在循環(huán)體中運(yùn)行傳進(jìn)來的事件,然后將事件發(fā)給相應(yīng)的響應(yīng)。

如果你打開你的程序的main.m,你就會(huì)發(fā)現(xiàn)其實(shí)主線程的runloop就是在main函數(shù)中進(jìn)行的,并且系統(tǒng)已經(jīng)為你生成好了autoreleasepool,因此你也無需操心主線程上的內(nèi)存釋放到底是在什么時(shí)候執(zhí)行了:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

根據(jù)響應(yīng)源的不同,runloop也被分成了許多種不同的模式,這就是被Cocoa和Core Foundation都封裝了的runloopMode。主要是這么幾種:

NSDefaultRunLoopMode: 大多數(shù)工作中默認(rèn)的運(yùn)行方式。
NSConnectionReplyMode: 使用這個(gè)Mode去監(jiān)聽NSConnection對(duì)象的狀態(tài)。
NSModalPanelRunLoopMode: 使用這個(gè)Mode在Model Panel情況下去區(qū)分事件(OS X開發(fā)中會(huì)遇到)。
NSEventTrackingRunLoopMode: 使用這個(gè)Mode去跟蹤來自用戶交互的事件(比如UITableView上下滑動(dòng))。
NSRunLoopCommonModes: 這是一個(gè)偽模式,其為一組run loop mode的集合。如果將Input source加入此模式,意味著關(guān)聯(lián)Input source到Common Modes中包含的所有模式下。在iOS系統(tǒng)中NSRunLoopCommonMode包含NSDefaultRunLoopMode、NSTaskDeathCheckMode、NSEventTrackingRunLoopMode.可使用CFRunLoopAddCommonMode方法向Common Modes中添加自定義mode。

在文首的情況中,我們可以根據(jù)蘋果官方文檔的定義知道,當(dāng)你在滑動(dòng)頁面的時(shí)候,主線程的runloop自動(dòng)進(jìn)入了NSEventTrackingRunLoopMode,而你的timer只是運(yùn)行在DefaultMode下,所以不能響應(yīng)。那么最簡單的辦法就是將你的timer添加在其他的mode下,像這樣即可:

[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

需要注意的是CommonModes其實(shí)并不是一種Mode,而是一個(gè)集合。因此runloop并不能在CommonModes下運(yùn)行,相反,你可以將需要輸入的事件源添加為這個(gè)mode,這樣無論runloop運(yùn)行在哪個(gè)mode下都可以響應(yīng)這個(gè)輸入事件,否則這個(gè)事件將不會(huì)得到響應(yīng)。

Input Source
輸入源包括三種,端口,自定義輸入源和performSelector的消息。根據(jù)上面的圖我們可以看出,在runloop接收到消息并執(zhí)行了指定方法的時(shí)候,它會(huì)執(zhí)行runUntilDate:這個(gè)方法來退出當(dāng)前循環(huán)。

端口源是基于Mach port的,其他進(jìn)程或線程可以通過端口來發(fā)送消息。這里的知識(shí)點(diǎn)需要深入到Mach,就已經(jīng)比較晦澀難懂了……這里你只需要知道你可以用Cocoa封裝的NSPort對(duì)象來進(jìn)行線程之間的通信,而這種通信方式所產(chǎn)生的事件就是通過端口源來傳入runloop的。關(guān)于Mach port的更深層介紹可以看這篇。http://segmentfault.com/a/1190000002400329

自定義輸入源。Core Foundation提供了CFRunLoopSourceRef類型的相關(guān)函數(shù),可以用來創(chuàng)建自定義輸入源。

performSelector輸入源:

//在主線程的Run Loop下執(zhí)行指定的 @selector 方法
  performSelectorOnMainThread:withObject:waitUntilDone:
  performSelectorOnMainThread:withObject:waitUntilDone:modes:

//在當(dāng)前線程的Run Loop下執(zhí)行指定的 @selector 方法
  performSelector:onThread:withObject:waitUntilDone:
  performSelector:onThread:withObject:waitUntilDone:modes:

//在當(dāng)前線程的Run Loop下延遲加載指定的 @selector 方法
  performSelector:withObject:afterDelay:
  performSelector:withObject:afterDelay:inModes:

//取消當(dāng)前線程的調(diào)用
  cancelPreviousPerformRequestsWithTarget:
  cancelPreviousPerformRequestsWithTarget:selector:object:

runloop生命周期
每一次runloop其實(shí)都是一次循環(huán),runloop會(huì)在循環(huán)中執(zhí)行runUntilDate: 或者runMode: beforeDate: 來開始每一個(gè)循環(huán)。而每一個(gè)循環(huán)又分為下面幾個(gè)階段,也就是runloop的生命周期:

kCFRunLoopEntry 進(jìn)入循環(huán)
kCFRunLoopBeforeTimers 先接收timer的事件
kCFRunLoopBeforeSources 接收來自input source的事件
kCFRunLoopBeforeWaiting 如果沒有事件,則準(zhǔn)備進(jìn)入休眠模式,在這里,如果沒有事件傳入,runloop會(huì)運(yùn)行直到循環(huán)中給定的日期,如果你給的是distantFuture,那么這個(gè)runloop會(huì)無限等待下去
kCFRunLoopAfterWaiting 從休眠中醒來,直接回到kCFRunLoopBeforeTimers狀態(tài)
kCFRunLoopExit 退出循環(huán)

這些狀態(tài)也是一個(gè)枚舉類型,系統(tǒng)是這么定義的,你可以使用observer來觀測到這些狀態(tài):

/* Run Loop Observer Activities */
   typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
      kCFRunLoopEntry = (1UL << 0),
      kCFRunLoopBeforeTimers = (1UL << 1),
      kCFRunLoopBeforeSources = (1UL << 2),
      kCFRunLoopBeforeWaiting = (1UL << 5),
      kCFRunLoopAfterWaiting = (1UL << 6),
      kCFRunLoopExit = (1UL << 7),
      kCFRunLoopAllActivities = 0x0FFFFFFFU
   };

我們下面做一個(gè)測試,在demo中我們定義了一個(gè)新的線程類,這樣我們可以自己啟動(dòng)和維護(hù)它的runloop對(duì)象。

- (void)main
{
    @autoreleasepool {
        NSLog(@"Thread Enter");
        [[NSThread currentThread] setName:@"This is a test thread"];
        NSRunLoop *currentThreadRunLoop = [NSRunLoop currentRunLoop];
        // 或者
        // CFRunLoopRef currentThreadRunLoop = CFRunLoopGetCurrent();
    
        CFRunLoopObserverContext context = {0, (__bridge void *)(self), NULL, NULL, NULL};
        CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &currentRunLoopObserver, &context);
    
        if (observer) {
            CFRunLoopRef runLoopRef = currentThreadRunLoop.getCFRunLoop;
            CFRunLoopAddObserver(runLoopRef, observer, kCFRunLoopDefaultMode);
        }
    
        // 創(chuàng)建一個(gè)Timer,重復(fù)調(diào)用來驅(qū)動(dòng)Run Loop
        //[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(handleTimerTask) userInfo:nil repeats:YES];
        do {
            [currentThreadRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:3]];
        } while (1);
    }
}

輸入源或者timer對(duì)于runloop來說是必要條件,如果沒有添加任何輸入源,則runloop根本不會(huì)啟動(dòng),所以上面的代碼中添加timer的操作,實(shí)際上是添加了一個(gè)默認(rèn)的事件輸入源,能讓runloop保持運(yùn)行。但是實(shí)際上,當(dāng)你創(chuàng)建好一個(gè)runloop對(duì)象后,任何輸入的事件都可以觸發(fā)runloop的啟動(dòng)。

例如下面的:

[self performSelector:@selector(selectorTest) onThread:self.runLoopThread withObject:nil waitUntilDone:NO];

記住,如果你需要自己來啟動(dòng)和維護(hù)runloop的話,核心就在于一個(gè)do…while循環(huán),你可以為runloop的跳出設(shè)置一個(gè)條件,也可以讓runloop無限進(jìn)行下去。在runloop沒有接收到事件進(jìn)入休眠狀態(tài)之后,如果調(diào)用performSelector,runloop的狀態(tài)變化如下:

Current thread Run Loop activity: kCFRunLoopAfterWaiting
Current thread Run Loop activity: kCFRunLoopBeforeTimers
Current thread Run Loop activity: kCFRunLoopBeforeSources
fuck
fuck_1
Current thread Run Loop activity: kCFRunLoopExit
Current thread Run Loop activity: kCFRunLoopEntry
Current thread Run Loop activity: kCFRunLoopBeforeTimers
Current thread Run Loop activity: kCFRunLoopBeforeSources
Current thread Run Loop activity: kCFRunLoopExit
Current thread Run Loop activity: kCFRunLoopEntry
Current thread Run Loop activity: kCFRunLoopBeforeTimers
Current thread Run Loop activity: kCFRunLoopBeforeSources
Current thread Run Loop activity: kCFRunLoopBeforeWaiting

在這里我連續(xù)調(diào)用了兩次performSelector,可以看到runloop也經(jīng)歷了兩個(gè)循環(huán),而如果只調(diào)用一次的話,不會(huì)有多出來的那次runloop(你可以自己嘗試一下),這是否說明每一次performSelector執(zhí)行完畢之后都會(huì)立即結(jié)束當(dāng)前runloop開始新的,蘋果的官方文檔里有一句話:

The run loop processes all queued perform selector calls each time through the loop, rather than processing one during each loop iteration

應(yīng)該意思是并不是像上面看到的結(jié)果那樣每一次循環(huán)執(zhí)行一次,而是有一個(gè)待執(zhí)行的操作隊(duì)列。如果我同時(shí)執(zhí)行四次performSelector,像這樣:

[self performSelector:@selector(selectorTest) onThread:self.runLoopThread withObject:nil waitUntilDone:NO];
[self performSelector:@selector(selectorTest_1) onThread:self.runLoopThread withObject:nil waitUntilDone:NO];
[self performSelector:@selector(selectorTest_2) onThread:self.runLoopThread withObject:nil waitUntilDone:NO];
[self performSelector:@selector(selectorTest_2) onThread:self.runLoopThread withObject:nil waitUntilDone:NO];

實(shí)際上得到的結(jié)果和上面是一樣的,然而當(dāng)我將他們的waitUntilDone參數(shù)都設(shè)置為YES之后,我們可以看到不一樣的地方:

Thread Enter
Current thread Run Loop activity: kCFRunLoopEntry
Current thread Run Loop activity: kCFRunLoopBeforeTimers
Current thread Run Loop activity: kCFRunLoopBeforeSources
fuck
Current thread Run Loop activity: kCFRunLoopExit
Current thread Run Loop activity: kCFRunLoopEntry
Current thread Run Loop activity: kCFRunLoopBeforeTimers
Current thread Run Loop activity: kCFRunLoopBeforeSources
fuck_1
Current thread Run Loop activity: kCFRunLoopExit
Current thread Run Loop activity: kCFRunLoopEntry
Current thread Run Loop activity: kCFRunLoopBeforeTimers
Current thread Run Loop activity: kCFRunLoopBeforeSources
fuck_2
Current thread Run Loop activity: kCFRunLoopExit
Current thread Run Loop activity: kCFRunLoopEntry
Current thread Run Loop activity: kCFRunLoopBeforeTimers
Current thread Run Loop activity: kCFRunLoopBeforeSources
fuck_2
Current thread Run Loop activity: kCFRunLoopExit
Current thread Run Loop activity: kCFRunLoopEntry
Current thread Run Loop activity: kCFRunLoopBeforeTimers
Current thread Run Loop activity: kCFRunLoopBeforeSources
Current thread Run Loop activity: kCFRunLoopBeforeWaiting

你可以看到每一個(gè)performSelector操作都單獨(dú)執(zhí)行了一個(gè)runloop,從蘋果的文檔中我們可以找到這個(gè)方法的定義:

performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:
Performs the specified selector on any thread for which you have an NSThread object. These methods give you the option of blocking the current thread until the selector is performed.

也就是說,waitUntilDone意味著這個(gè)操作是否會(huì)在當(dāng)前線程阻塞其他的輸入源,如果等于True,則每一次runloop循環(huán)只會(huì)處理這一個(gè)selector的調(diào)用,如果為False,則隊(duì)列中后面等待著的selector調(diào)用都會(huì)在同一次runloop循環(huán)中執(zhí)行。至于上文的執(zhí)行了兩個(gè)runloop循環(huán)的現(xiàn)象,我猜測應(yīng)該是當(dāng)runloop從休眠模式被喚醒的時(shí)候,當(dāng)前循環(huán)執(zhí)行完喚醒的操作后就會(huì)立即結(jié)束,釋放掉之前可能累積下來的內(nèi)存,然后開始新的循環(huán),將隊(duì)列中的其他輸入逐個(gè)放進(jìn)runloop循環(huán)中執(zhí)行。

來自http://sergiochan.xyz/2015/10/22/runloop初窺/

NSRunLoop的原理

以下內(nèi)容來自RyanJIN http://www.itdecent.cn/p/ebb3e42049fd的簡書。
關(guān)于NSRunLoop推薦看一下來自百度工程師孫源的分享視頻:http://v.youku.com/v_show/id_XODgxODkzODI0.html
RunLoop就是跑圈, 保證程序一直在執(zhí)行. App運(yùn)行起來之后, 即使你什么都不做, 放在那兒它也不會(huì)退出, 而是一直在"跑圈", 這就是RunLoop干的事. 主線程會(huì)自動(dòng)創(chuàng)建一個(gè)RunLoop來保證程序一直運(yùn)行. 但子線程默認(rèn)不創(chuàng)建NSRunLoop, 所以子線程的任務(wù)一旦返回, 線程就over了.

上面的并發(fā)operation當(dāng)start函數(shù)返回后子線程就退出了, 當(dāng)NSURLConnection的delegate回調(diào)時(shí), 線程已經(jīng)木有了, 所以你也就收不到回調(diào)了. 為了保證子線程持續(xù)live(等待connection回調(diào)), 你需要在子線程中加入RunLoop, 來保證它不會(huì)被kill掉.

RunLoop在某一時(shí)刻只能在一種模式下運(yùn)行, 更換模式時(shí)需要暫停當(dāng)前的Loop, 然后重啟新的Loop. RunLoop主要有下面幾個(gè)模式:

NSDefalutRunLoopMode : 默認(rèn)Mode, 通常主線程在這個(gè)模式下運(yùn)行
UITrackingRunLoopMode : 滑動(dòng)ScrollView是會(huì)切換到這個(gè)模式
NSRunLoopCommonModes: 包括上面兩個(gè)模式

這邊需要特別注意的是, 在滑動(dòng)ScrollView的情況下, 系統(tǒng)會(huì)自動(dòng)把RunLoop模式切換成UITrackingRunLoopMode來保證ScrollView的流暢性.

[NSTimer scheduledTimerWithTimeInterval:1.f
                             target:self
                           selector:@selector(timerAction:)   
                           userInfo:nil
                            reports:YES];

當(dāng)你在滑動(dòng)ScrollView的時(shí)候上面的timer會(huì)失效, 原因是Timer是默認(rèn)加在NSDefalutRunLoopMode上的, 而滑動(dòng)ScrollView后系統(tǒng)把RunLoop切換為UITrackingRunLoopMode, 所以timer就不會(huì)執(zhí)行了. 解決方法是把該Timer加到NSRunLoopCommonModes下, 這樣即使滑動(dòng)ScrollView也不會(huì)影響timer了.

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
另外還有一個(gè)trick是當(dāng)tableview的cell從網(wǎng)絡(luò)異步加載圖片, 加載完成后在主線程刷新顯示圖片, 這時(shí)滑動(dòng)tableview會(huì)造成卡頓. 通常的思路是tableview滑動(dòng)的時(shí)候延遲加載圖片, 等停止滑動(dòng)時(shí)再顯示圖片. 這里我們可以通過RunLoop來實(shí)現(xiàn).

[self.cellImageView performSelector:@sector(setImage:)
                     withObject:downloadedImage
                     afterDelay:0
                        inModes:@[NSDefaultRunLoopMode]];

當(dāng)NSRunLoop為NSDefaultRunLoopMode的時(shí)候tableview肯定停止滑動(dòng)了, why? 因?yàn)槿绻€在滑動(dòng)中, RunLoop的mode應(yīng)該是UITrackingRunLoopMode.

呼叫NSURLConnection的異步回調(diào)
現(xiàn)在解決方案已經(jīng)很清晰了, 就是利用RunLoop來監(jiān)督線程, 讓它一直等待delegate的回調(diào). 上面已經(jīng)說到Main Thread是默認(rèn)創(chuàng)建了一個(gè)RunLoop的, 所以我們的Option 1是讓start函數(shù)在主線程運(yùn)行(即使[operation start]是在子線程調(diào)用的).

- (void)start 
{
     if (![NSThread isMainThread]) {
          [self performSelectorOnMainThread:@selector(start)
                           withObject:nil
                        waitUntilDone:NO];
          return;
    }
// set up NSURLConnection...
}

或者這樣:

- (void)start
{
       [[NSOperationQueue mainQueue] addOperationWithBlock:^{
       self.connection = [NSURLConnection connectionWithRequest:self.request delegate:self];
       }];
}

這樣我們可以簡單直接的使用main run loop, 因?yàn)閿?shù)據(jù)delivery是非??斓? 然后我們就可以將處理incoming data的操作放到子線程去...

Option 2是讓operation的start函數(shù)在子線程運(yùn)行, 但是我們?yōu)樗鼊?chuàng)建一個(gè)RunLoop. 然后把URL connection schedule到上面去. 我們先來瞅瞅AFNetworking是怎么做滴:

+ (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;
 }

 - (void)start 
 {
      [self.lock lock];
      if ([self isCancelled]) {
           [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
      } else if ([self isReady]) {
           self.state = AFOperationExecutingState;
           [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
      }
      [self.lock unlock];
 }

AFNetworking創(chuàng)建了一個(gè)新的子線程(在子線程中調(diào)用NSRunLoop *runloop = [NSRunLoop currentRunLoop]; 獲取RunLoop對(duì)象的時(shí)候, 就會(huì)創(chuàng)建RunLoop), 然后把它加到RunLoop里面來保證它一直運(yùn)行.

這邊我們可以簡單的判斷下當(dāng)前start()的線程是子線程還是主線程, 如果是子線程則調(diào)用[NSRunLoop currentRunLoop]創(chuàng)新RunLoop, 否則就直接調(diào)用[NSRunLoop mainRunLoop], 當(dāng)然在主線程下就沒必要調(diào)用[runLoop run]了, 因?yàn)樗緛砭褪且恢眗un的.
P.S. 我們還可以使用CFRunLoop來啟動(dòng)和停止RunLoop, 像下面這樣:

[self.connection scheduleInRunLoop:[NSRunLoop currentRunLoop]
                       forMode:NSRunLoopCommonModes];
CFRunLoopRun();

等到該Operation結(jié)束的時(shí)候, 一定要記得調(diào)用CFRunLoopStop()停止當(dāng)前線程的RunLoop, 讓當(dāng)前線程在operation finished之后可以退出.
多線程。

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

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

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