Runloop的概念和總結

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

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

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

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

OSX/iOS 系統中,提供了兩個這樣的對象:NSRunLoop 和 CFRunLoopRef。
CFRunLoopRef 是在 CoreFoundation 框架內的,它提供了純 C 函數的 API,所有這些 API 都是線程安全的。
NSRunLoop 是基于 CFRunLoopRef 的封裝,提供了面向對象的 API,但是這些 API 不是線程安全的。

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

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

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

蘋果不允許直接創建 RunLoop,它只提供了兩個自動獲取的函數:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。 這兩個函數內部的邏輯大概是下面這樣:

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

/// 獲取一個 pthread 對應的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
OSSpinLockLock(&loopsLock);

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

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

if (!loop) {
    /// 取不到時,創建一個
    loop = _CFRunLoopCreate();
    CFDictionarySetValue(loopsDic, thread, loop);
    /// 注冊一個回調,當線程銷毀時,順便也銷毀其對應的 RunLoop。
    _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
}

    OSSpinLockUnLock(&loopsLock);
    return loop;
}

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

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

從上面的代碼可以看出,線程和 RunLoop 之間是一一對應的,其關系是保存在一個全局的 Dictionary 里。線程剛創建時并沒有 RunLoop,如果你不主動獲取,那它一直都不會有。RunLoop 的創建是發生在第一次獲取時,RunLoop 的銷毀是發生在線程結束時。你只能在一個線程的內部獲取其 RunLoop(主線程除外)。

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

runloop是iOS系統對事件接受和分發機制的一個實現,是線程的基本架構部分。一個runloop就是一個事件處理循環,用來不停的調配工作以及處理輸入事件。 使用runloop的目的是使你的線程在有工作的時候工作,沒有的時候休眠,以達到節省cpu的目的。runloop的管理并不完全是自動,當我們創建一個子線程時,我們必須在適當的時候啟動Runloop并正確響應事件。 子線程不需要顯式的創建RunLoop,每個線程,包括程序的主線程都有與之對應的RunLoop對象,但是自己創建的線程需要手動運行RunLoop的運行方法。不過程序啟動時,主線程會自動創建并運行RunLoop。

在沒有手加Autorelease Pool的情況下,Autorelease對象是在當前的runloop迭代結束時釋放的,而它能夠釋放的原因是系統在每個runloop迭代中都加入了自動釋放池Push和Pop.

什么是Runloop

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

對于線程來說,每一個線程都有一個runloop對象,是否能向某個線程的runloop發送事件取決于你是否啟動了這個runloop,系統會默認在你的程序啟動的時候運行主線程上的runloop,但是你自定義創建出來的線程可以不需要運行runloop,一些第三方框架,例如AFNetworking,就有在自己的線程上維護一個runloop對象。

在 Core Foundation 里面關于 RunLoop 有5個類:

CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef

他們的關系可以從NSRunloop對象的結構定義中得出。首先,runloop對象在Cocoa和Core Foundation中都有實現,但是他們做了很好的橋接,你可以直接調用

CFRunLoopRef runLoopRef = currentThreadRunLoop.getCFRunLoop;

來獲取一個CoreFoundation中的runloop對象。然后,當你在查看NSRunloop的結構的時候,你應該能看到:

<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)
},}}

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

也許你會注意到 source 包括了source0和source1兩個版本。

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

在你的程序中,runloop的過程實際上是一個無限循環的循環體,這個循環體是由你的程序來運行的。主線程的runloop由于系統已經實現并且沒有它程序就不能運行,因此不需要我們手動去運行這個runloop。然而如果我們需要在自定義的線程中使用到runloop,我們則需要用一個do…while循環來驅動它。而runloop對象負責不斷地在循環體中運行傳進來的事件,然后將事件發給相應的響應。

如果你打開你的程序的main.m,你就會發現其實主線程的runloop就是在main函數中進行的,并且系統已經為你生成好了autoreleasepool,因此你也無需操心主線程上的內存釋放到底是在什么時候執行了:

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

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

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

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

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

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

Input Source
輸入源包括三種,端口,自定義輸入源和performSelector的消息。根據上面的圖我們可以看出,在runloop接收到消息并執行了指定方法的時候,它會執行runUntilDate:這個方法來退出當前循環。

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

自定義輸入源。Core Foundation提供了CFRunLoopSourceRef類型的相關函數,可以用來創建自定義輸入源。

performSelector輸入源:

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

//在當前線程的Run Loop下執行指定的 @selector 方法
  performSelector:onThread:withObject:waitUntilDone:
  performSelector:onThread:withObject:waitUntilDone:modes:

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

//取消當前線程的調用
  cancelPreviousPerformRequestsWithTarget:
  cancelPreviousPerformRequestsWithTarget:selector:object:

runloop生命周期
每一次runloop其實都是一次循環,runloop會在循環中執行runUntilDate: 或者runMode: beforeDate: 來開始每一個循環。而每一個循環又分為下面幾個階段,也就是runloop的生命周期:

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

這些狀態也是一個枚舉類型,系統是這么定義的,你可以使用observer來觀測到這些狀態:

/* 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
   };

我們下面做一個測試,在demo中我們定義了一個新的線程類,這樣我們可以自己啟動和維護它的runloop對象。

- (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);
        }
    
        // 創建一個Timer,重復調用來驅動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對于runloop來說是必要條件,如果沒有添加任何輸入源,則runloop根本不會啟動,所以上面的代碼中添加timer的操作,實際上是添加了一個默認的事件輸入源,能讓runloop保持運行。但是實際上,當你創建好一個runloop對象后,任何輸入的事件都可以觸發runloop的啟動。

例如下面的:

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

記住,如果你需要自己來啟動和維護runloop的話,核心就在于一個do…while循環,你可以為runloop的跳出設置一個條件,也可以讓runloop無限進行下去。在runloop沒有接收到事件進入休眠狀態之后,如果調用performSelector,runloop的狀態變化如下:

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

在這里我連續調用了兩次performSelector,可以看到runloop也經歷了兩個循環,而如果只調用一次的話,不會有多出來的那次runloop(你可以自己嘗試一下),這是否說明每一次performSelector執行完畢之后都會立即結束當前runloop開始新的,蘋果的官方文檔里有一句話:

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

應該意思是并不是像上面看到的結果那樣每一次循環執行一次,而是有一個待執行的操作隊列。如果我同時執行四次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];

實際上得到的結果和上面是一樣的,然而當我將他們的waitUntilDone參數都設置為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

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

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意味著這個操作是否會在當前線程阻塞其他的輸入源,如果等于True,則每一次runloop循環只會處理這一個selector的調用,如果為False,則隊列中后面等待著的selector調用都會在同一次runloop循環中執行。至于上文的執行了兩個runloop循環的現象,我猜測應該是當runloop從休眠模式被喚醒的時候,當前循環執行完喚醒的操作后就會立即結束,釋放掉之前可能累積下來的內存,然后開始新的循環,將隊列中的其他輸入逐個放進runloop循環中執行。

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

NSRunLoop的原理

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

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

RunLoop在某一時刻只能在一種模式下運行, 更換模式時需要暫停當前的Loop, 然后重啟新的Loop. RunLoop主要有下面幾個模式:

NSDefalutRunLoopMode : 默認Mode, 通常主線程在這個模式下運行
UITrackingRunLoopMode : 滑動ScrollView是會切換到這個模式
NSRunLoopCommonModes: 包括上面兩個模式

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

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

當你在滑動ScrollView的時候上面的timer會失效, 原因是Timer是默認加在NSDefalutRunLoopMode上的, 而滑動ScrollView后系統把RunLoop切換為UITrackingRunLoopMode, 所以timer就不會執行了. 解決方法是把該Timer加到NSRunLoopCommonModes下, 這樣即使滑動ScrollView也不會影響timer了.

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

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

當NSRunLoop為NSDefaultRunLoopMode的時候tableview肯定停止滑動了, why? 因為如果還在滑動中, RunLoop的mode應該是UITrackingRunLoopMode.

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

- (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, 因為數據delivery是非常快滴. 然后我們就可以將處理incoming data的操作放到子線程去...

Option 2是讓operation的start函數在子線程運行, 但是我們為它創建一個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創建了一個新的子線程(在子線程中調用NSRunLoop *runloop = [NSRunLoop currentRunLoop]; 獲取RunLoop對象的時候, 就會創建RunLoop), 然后把它加到RunLoop里面來保證它一直運行.

這邊我們可以簡單的判斷下當前start()的線程是子線程還是主線程, 如果是子線程則調用[NSRunLoop currentRunLoop]創新RunLoop, 否則就直接調用[NSRunLoop mainRunLoop], 當然在主線程下就沒必要調用[runLoop run]了, 因為它本來就是一直run的.
P.S. 我們還可以使用CFRunLoop來啟動和停止RunLoop, 像下面這樣:

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

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容