iOS多線程編程Part 1/3 - NSThread & Run Loop
前言
多線程的價值無需贅述,對於App性能和用戶體驗都有着至關重要的意義,在iOS開發中,Apple提供了不同的技術支持多線程編程,除了跨平臺的pthread之外,還提供了NSThread、NSOperationQueue、GCD等多線程技術,從本篇Blog開始介紹這幾種多線程技術的細節。
對於pthread這種跨平臺的多線程技術,這本Programming with POSIX Threads做了詳細介紹,不再提及。
NSThread
使用NSThead創建線程有很多方法:
- +detachNewThreadSelector:toTarget:withObject:類方法直接生成一個子線程
<span class="line-number" style="margin: 0px; padding: 0px;">1</span> |
|
- 創建一個NSThread類實例,然後調用start方法。
<span class="line-number" style="margin: 0px; padding: 0px;">1</span> <span class="line-number" style="margin: 0px; padding: 0px;">2</span> |
|
- 調用NSObject的
+performSelectorInBackground:withObject:
方法生成子線程。
<span class="line-number" style="margin: 0px; padding: 0px;">1</span> |
|
- 創建一個NSThread子類,然後調用子類實例的start方法,。
創建線程也是有開銷的,iOS下主要成本包括構造內核數據結構(大約1KB)、棧空間(子線程512KB、主線程1MB,不過可以使用方法-setStackSize:
自己設置,注意必須是4K的倍數,而且最小是16K),創建線程大約需要90毫秒的創建時間。
第二種和第四種方法創建的線程有個好處是擁有線程的對象,因此可以使用performSelector:onThread:withObject:waitUntilDone:
在該線程上執行方法,這是一種非常方便的線程間通訊的方法(相對於設置麻煩的NSPort用於通訊),所要執行的方法可以直接添加到目標線程的Runloop中執行。Apple建議使用這個接口運行的方法不要是耗時或者頻繁的操作,以免子線程的負載過重。
第三種方法其實與第一種方法是一樣的,都會直接生成一個子線程。
上面四種方法生成的子線程都是detached狀態,即主線程結束時這些線程都會被直接殺死;如果要生成joinable狀態的子線程,只能使用pthread接口啦。
如果需要,可以設置線程的優先級(-setThreadPriority:
);如果要在線程中保存一些狀態信息,還可以使用到-threadDictionary
得到一個NSMutableDictionary,以key-value的方式保存信息用於線程內讀寫。
NSThread的入口方法
要寫一個有效的子線程入口方法需要注意很多問題,示例代碼:
<span class="line-number" style="margin: 0px; padding: 0px;">1</span> <span class="line-number" style="margin: 0px; padding: 0px;">2</span> <span class="line-number" style="margin: 0px; padding: 0px;">3</span> <span class="line-number" style="margin: 0px; padding: 0px;">4</span> <span class="line-number" style="margin: 0px; padding: 0px;">5</span> <span class="line-number" style="margin: 0px; padding: 0px;">6</span> <span class="line-number" style="margin: 0px; padding: 0px;">7</span> <span class="line-number" style="margin: 0px; padding: 0px;">8</span> <span class="line-number" style="margin: 0px; padding: 0px;">9</span> <span class="line-number" style="margin: 0px; padding: 0px;">10</span> <span class="line-number" style="margin: 0px; padding: 0px;">11</span> <span class="line-number" style="margin: 0px; padding: 0px;">12</span> <span class="line-number" style="margin: 0px; padding: 0px;">13</span> <span class="line-number" style="margin: 0px; padding: 0px;">14</span> <span class="line-number" style="margin: 0px; padding: 0px;">15</span> <span class="line-number" style="margin: 0px; padding: 0px;">16</span> <span class="line-number" style="margin: 0px; padding: 0px;">17</span> <span class="line-number" style="margin: 0px; padding: 0px;">18</span> <span class="line-number" style="margin: 0px; padding: 0px;">19</span> <span class="line-number" style="margin: 0px; padding: 0px;">20</span> <span class="line-number" style="margin: 0px; padding: 0px;">21</span> <span class="line-number" style="margin: 0px; padding: 0px;">22</span> <span class="line-number" style="margin: 0px; padding: 0px;">23</span> <span class="line-number" style="margin: 0px; padding: 0px;">24</span> <span class="line-number" style="margin: 0px; padding: 0px;">25</span> |
|
- 必須創建一個NSAutoreleasePool,因爲子線程不會自動創建。同時要注意這個pool因爲是最外層pool,如果線程中要進行長時間的操作生成大量autoreleased的對象,則只有在該子線程退出時纔會回收,因此如果線程中會大量創建autoreleased對象,那麼需要創建額外的NSAutoreleasePool,可以在NSRunloop每次迭代時創建和銷燬一個NSAutoreleasePool。
- 如果你的子線程會拋出異常,最好在子線程中設置一個異常處理函數,因爲如果子線程無法處理拋出的異常,會導致程序直接Crash關閉。
- (可選)設置Run Loop,如果子線程只是做個一次性的操作,那麼無需設置Run Loop;如果子線程進入一個循環需要不斷處理一些事件,那麼設置一個Run Loop是最好的處理方式,如果需要Timer,那麼Run Loop就是必須的。
- 如果需要在子線程運行的時候讓子線程結束操作,子線程每次Run Loop迭代中檢查相應的標誌位來判斷是否還需要繼續執行,可以使用threadDictionary以及設置Input Source的方式來通知這個子線程。那麼什麼是Run Loop呢?這是涉及NSThread及線程相關的編程時無法迴避的一個問題。
Run Loop
Run Loop本身並不具備併發執行的功能,但是和多線程開發息息相關,而且概念令人迷惑,相關的介紹資料也很少,它的主要的特性如下:
- 每個線程都有一個Run Loop,主線程的Run Loop會在App運行時自動運行,子線程中需要手動運行。
- 每個Run Loop都會以一個模式mode來運行,可以使用NSRunLoop的
- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate
方法運行在某個特定模式mode。 - Run Loop的處理兩大類事件源:Timer Source和Input Source(包括performSelector***方法簇、Port或者自定義Input Source),每個事件源都會綁定在Run Loop的某個特定模式mode上,而且只有RunLoop在這個模式運行的時候纔會觸發該Timer和Input Source。
- 如果沒有任何事件源添加到Run Loop上,Run Loop就會立刻exit。
Run Loop接口
要操作Run Loop,Foundation層和Core Foundation層都有對應的接口可以操作Run Loop。
Foundation層對應的是NSRunLoop:
Core Foundation層對應的是CFRunLoopRef:
兩組接口差不多,不過功能上還是有許多區別的,例如CF層可以添加自定義Input Source事件源(CFRunLoopSourceRef)和Run Loop觀察者Observer(CFRunLoopObserverRef),很多類似功能的接口特性也是不一樣的。
Run Loop運行
Run Loop如何運行呢?在上一節NSThread的入口函數中使用了一種NSRunLoop的使用場景,再看一例:
<span class="line-number" style="margin: 0px; padding: 0px;">1</span> <span class="line-number" style="margin: 0px; padding: 0px;">2</span> <span class="line-number" style="margin: 0px; padding: 0px;">3</span> <span class="line-number" style="margin: 0px; padding: 0px;">4</span> <span class="line-number" style="margin: 0px; padding: 0px;">5</span> <span class="line-number" style="margin: 0px; padding: 0px;">6</span> <span class="line-number" style="margin: 0px; padding: 0px;">7</span> <span class="line-number" style="margin: 0px; padding: 0px;">8</span> <span class="line-number" style="margin: 0px; padding: 0px;">9</span> <span class="line-number" style="margin: 0px; padding: 0px;">10</span> <span class="line-number" style="margin: 0px; padding: 0px;">11</span> <span class="line-number" style="margin: 0px; padding: 0px;">12</span> <span class="line-number" style="margin: 0px; padding: 0px;">13</span> <span class="line-number" style="margin: 0px; padding: 0px;">14</span> <span class="line-number" style="margin: 0px; padding: 0px;">15</span> <span class="line-number" style="margin: 0px; padding: 0px;">16</span> <span class="line-number" style="margin: 0px; padding: 0px;">17</span> <span class="line-number" style="margin: 0px; padding: 0px;">18</span> <span class="line-number" style="margin: 0px; padding: 0px;">19</span> <span class="line-number" style="margin: 0px; padding: 0px;">20</span> <span class="line-number" style="margin: 0px; padding: 0px;">21</span> <span class="line-number" style="margin: 0px; padding: 0px;">22</span> <span class="line-number" style="margin: 0px; padding: 0px;">23</span> <span class="line-number" style="margin: 0px; padding: 0px;">24</span> <span class="line-number" style="margin: 0px; padding: 0px;">25</span> |
|
我們看到入口方法裏創建了一個NSTimer,並且以NSDefaultRunLoopMode模式加入到當前子線程的NSRunLoop中。進入循環後肯定會執行-doOtherTask
方式法一次,然後再以NSDefaultRunLoopMode模式運行NSRunLoop,如果一次Timer事件觸發處理後,這個Run
Loop會返回嗎?答案是不會,Why?
NSRunLoop的底層是由CFRunLoopRef實現的,你可以想象成一個循環或者類似Linux下select或者epoll,當沒有事件觸發時,你調用的Run Loop運行方法不會立刻返回,它會持續監聽其他事件源,如果需要Run Loop會讓子線程進入sleep等待狀態而不是空轉,只有當Timer Source或者Input Source事件發生時,子線程纔會被喚醒,然後處理觸發的事件,然而由於Timer source比較特殊,Timer Source事件發生處理後,Run Loop運行方法-
(BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;
也不會返回;而其他非Timer事件的觸發處理會讓這個Run Loop退出並返回YES。當Run Loop運行在一個特定模式時,如果該模式下沒有事件源,運行Run Loop會立刻返回NO。
NSRunLoop的運行接口:
<span class="line-number" style="margin: 0px; padding: 0px;">1</span> <span class="line-number" style="margin: 0px; padding: 0px;">2</span> <span class="line-number" style="margin: 0px; padding: 0px;">3</span> <span class="line-number" style="margin: 0px; padding: 0px;">4</span> <span class="line-number" style="margin: 0px; padding: 0px;">5</span> <span class="line-number" style="margin: 0px; padding: 0px;">6</span> <span class="line-number" style="margin: 0px; padding: 0px;">7</span> <span class="line-number" style="margin: 0px; padding: 0px;">8</span> |
|
CFRunLoopRef的運行接口:
<span class="line-number" style="margin: 0px; padding: 0px;">1</span> <span class="line-number" style="margin: 0px; padding: 0px;">2</span> <span class="line-number" style="margin: 0px; padding: 0px;">3</span> <span class="line-number" style="margin: 0px; padding: 0px;">4</span> <span class="line-number" style="margin: 0px; padding: 0px;">5</span> <span class="line-number" style="margin: 0px; padding: 0px;">6</span> <span class="line-number" style="margin: 0px; padding: 0px;">7</span> <span class="line-number" style="margin: 0px; padding: 0px;">8</span> <span class="line-number" style="margin: 0px; padding: 0px;">9</span> <span class="line-number" style="margin: 0px; padding: 0px;">10</span> <span class="line-number" style="margin: 0px; padding: 0px;">11</span> |
|
詳細講解下NSRunLoop的三個運行接口:
- (void)run;
無條件運行
不建議使用,因爲這個接口會導致Run Loop永久性的運行在NSDefaultRunLoopMode模式,即使使用CFRunLoopStop(runloopRef);
也無法停止Run
Loop的運行,那麼這個子線程就無法停止,只能永久運行下去。
- (void)runUntilDate:(NSDate *)limitDate;
有一個超時時間限制
比上面的接口好點,有個超時時間,可以控制每次Run Loop的運行時間,也是運行在NSDefaultRunLoopMode模式。這個方法運行Run Loop一段時間會退出給你檢查運行條件的機會,如果需要可以再次運行Run Loop。注意CFRunLoopStop(runloopRef);
也無法停止Run
Loop的運行,因此最好自己設置一個合理的Run Loop運行時間。示例:
<span class="line-number" style="margin: 0px; padding: 0px;">1</span> <span class="line-number" style="margin: 0px; padding: 0px;">2</span> <span class="line-number" style="margin: 0px; padding: 0px;">3</span> <span class="line-number" style="margin: 0px; padding: 0px;">4</span> <span class="line-number" style="margin: 0px; padding: 0px;">5</span> <span class="line-number" style="margin: 0px; padding: 0px;">6</span> |
|
- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;
有一個超時時間限制,而且設置運行模式
這個接口在非Timer事件觸發、顯式的用CFRunLoopStop停止Run Loop、到達limitDate後會退出返回。如果僅是Timer事件觸發並不會讓Run Loop退出返回;如果是PerfromSelector***事件或者其他Input Source事件觸發處理後,Run Loop會退出返回YES。示例:
<span class="line-number" style="margin: 0px; padding: 0px;">1</span> <span class="line-number" style="margin: 0px; padding: 0px;">2</span> <span class="line-number" style="margin: 0px; padding: 0px;">3</span> <span class="line-number" style="margin: 0px; padding: 0px;">4</span> <span class="line-number" style="margin: 0px; padding: 0px;">5</span> <span class="line-number" style="margin: 0px; padding: 0px;">6</span> |
|
那麼如何知道一個Run Loop是因爲什麼原因exit退出的呢?NSRunLoop中沒有接口可以知道,而需要通過Core Foundation的接口來運行CFRunLoopRef,NSRunLoop其實就是CFRunLoopRef的二次封裝。使用CFRunLoop的接口(C的接口)來運行Run Loop,有兩個接口:
void CFRunLoopRun(void);
運行在默認的kCFRunLoopDefaultMode模式下,直到使用CFRunLoopStop接口停止這個Run Loop,或者Run Loop的所有事件源都被刪除。
SInt32 CFRunLoopRunInMode(CFStringRef mode, CFTimeInterval seconds, Boolean returnAfterSourceHandled);
第一個參數是指RunLoop運行的模式(例如kCFRunLoopDefaultMode或者kCFRunLoopCommonModes),第二個參數是運行時間,第三個參數是是否在處理事件後讓Run Loop退出返回。 示例:
<span class="line-number" style="margin: 0px; padding: 0px;">1</span> <span class="line-number" style="margin: 0px; padding: 0px;">2</span> <span class="line-number" style="margin: 0px; padding: 0px;">3</span> <span class="line-number" style="margin: 0px; padding: 0px;">4</span> <span class="line-number" style="margin: 0px; padding: 0px;">5</span> <span class="line-number" style="margin: 0px; padding: 0px;">6</span> <span class="line-number" style="margin: 0px; padding: 0px;">7</span> <span class="line-number" style="margin: 0px; padding: 0px;">8</span> <span class="line-number" style="margin: 0px; padding: 0px;">9</span> <span class="line-number" style="margin: 0px; padding: 0px;">10</span> <span class="line-number" style="margin: 0px; padding: 0px;">11</span> |
|
如果Run Loop退出返回後,返回值是SInt32類型(signed long),表明Run Loop返回的原因,目前有四種:
<span class="line-number" style="margin: 0px; padding: 0px;">1</span> <span class="line-number" style="margin: 0px; padding: 0px;">2</span> <span class="line-number" style="margin: 0px; padding: 0px;">3</span> <span class="line-number" style="margin: 0px; padding: 0px;">4</span> <span class="line-number" style="margin: 0px; padding: 0px;">5</span> <span class="line-number" style="margin: 0px; padding: 0px;">6</span> |
|
注意:Run Loop是可以嵌套調用的(就像NSAutoreleasePool),例如一個Run Loop運行過程中一個事件觸發後,那麼在觸發方法裏可以再運行當前子線程的Run Loop,然後由這個Run Loop等待其他事件觸發。不過這種嵌套Run Loop調用方式我用的比較少。
以上Run Loop運行方法參考本文最後的Sample Code自行嘗試。
Run Loop的運行模式Mode
iOS下Run Loop的主要運行模式mode有:
1) NSDefaultRunLoopMode: 默認的運行模式,除了NSConnection對象的事件。
2) NSRunLoopCommonModes: 是一組常用的模式集合,將一個input source關聯到這個模式集合上,等於將input source關聯到這個模式集合中的所有模式上。在iOS系統中NSRunLoopCommonMode包含NSDefaultRunLoopMode、NSTaskDeathCheckMode、UITrackingRunLoopMode,我有個timer要關聯到這些模式上,一個個註冊很麻煩,我可以用CFRunLoopAddCommonMode([[NSRunLoop
currentRunLoop] getCFRunLoop],(__bridge CFStringRef) NSEventTrackingRunLoopMode)
將NSEventTrackingRunLoopMode或者其他模式添加到這個NSRunLoopCommonModes模式中,然後只需要將Timer關聯到NSRunLoopCommonModes,即可以實現Run Loop運行在這個模式集合中任何一個模式時,這個Timer都可以被觸發。默認情況下NSRunLoopCommonModes包含了NSDefaultRunLoopMode和UITrackingRunLoopMode。注意:讓Run
Loop運行在NSRunLoopCommonModes下是沒有意義的,因爲一個時刻Run Loop只能運行在一個特定模式下,而不可能是個模式集合。
3) UITrackingRunLoopMode: 用於跟蹤觸摸事件觸發的模式(例如UIScrollView上下滾動),主線程當觸摸事件觸發時會設置爲這個模式,可以用來在控件事件觸發過程中設置Timer。
4) GSEventReceiveRunLoopMode: 用於接受系統事件,屬於內部的Run Loop模式。
5) 自定義Mode:可以設置自定義的運行模式Mode,你也可以用CFRunLoopAddCommonMode添加到NSRunLoopCommonModes中。
Run Loop運行時只能以一種固定的模式運行,只會監控這個模式下添加的Timer Source和Input Source,如果這個模式下沒有相應的事件源,Run Loop的運行也會立刻返回的。注意Run Loop不能在運行在NSRunLoopCommonModes模式,因爲NSRunLoopCommonModes其實是個模式集合,而不是一個具體的模式,我可以在添加事件源的時候使用NSRunLoopCommonModes,只要Run Loop運行在NSRunLoopCommonModes中任何一個模式,這個事件源都可以被觸發。
Run Loop的事件源
歸根結底,Run Loop就是個處理事件的Loop,可以添加Timer和其他Input Source等各種事件源,如果事件源沒有發生時,Run Loop就可能讓線程進入asleep狀態,而事件源發生時就會喚醒休眠的(asleep)的子線程來處理事件。Run Loop的事件源事件源分兩類:Timer Source和Input Source(包括-performSelector:***API調用簇,Port Input Source、自定義Input Source)。
從上圖可以看出Run Loop就是處理事件的一個循環,不同的是Timer Source事件處理後不會使Run Loop結束,而Input Source事件處理後會讓Run Loop退出。因此你需要自己的一個Loop去不斷運行Run Loop來處理事件,就像本文開頭的示例那樣。
細分下Run Loop的事件源:
1) Timer Souce就是創建Timer添加到Run Loop中,沒啥好說的,Cocoa或者Core Foundation都有相應接口實現。需要注意的是scheduledTimerWith****
開頭生成的Timer會自動幫你以默認NSDefaultRunLoopMode模式加載到當前的Run
Loop中,而其他接口生成的Timer則需要你手動使用-addTimer:forMode
添加到Run
Loop中。需要額外注意的是Timer的觸發不會讓Run Loop返回。(Timer sources deliver events to their handler routines but do not cause the run loop to exit.) 具體實驗可以看下面的Sample Code。
2) Input Source中的-performSelector:***API調用簇方法,有以下這些接口:
<span class="line-number" style="margin: 0px; padding: 0px;">1</span> <span class="line-number" style="margin: 0px; padding: 0px;">2</span> <span class="line-number" style="margin: 0px; padding: 0px;">3</span> <span class="line-number" style="margin: 0px; padding: 0px;">4</span> <span class="line-number" style="margin: 0px; padding: 0px;">5</span> <span class="line-number" style="margin: 0px; padding: 0px;">6</span> <span class="line-number" style="margin: 0px; padding: 0px;">7</span> <span class="line-number" style="margin: 0px; padding: 0px;">8</span> <span class="line-number" style="margin: 0px; padding: 0px;">9</span> <span class="line-number" style="margin: 0px; padding: 0px;">10</span> <span class="line-number" style="margin: 0px; padding: 0px;">11</span> |
|
這些API最後兩個是取消當前線程中調用,其他API是在主線程或者當前線程下的Run Loop中執行指定的@selector。
3) Port Input Source:概念上也比較簡單,可以用NSMachPort作爲線程之間的通訊通道。例如在主線程創建子線程時傳入一個NSPort對象,這樣主線程就可以和這個子線程通訊啦,如果要實現雙向通訊,那麼子線程也需要回傳給主線程一個NSPort。
NSPort的子類除了NSMachPort,還可以使用NSMessagePort或者Core Foundation中的CFMessagePortRef。
注意:雖然有這麼棒的方式實現線程間通訊方式,但是估計是由於危及iOS的Sandbox沙盒環境,所以這些API都是私有接口,如果你用到NSPortMessage,XCode會提示'NSPortMessage'
for instance message is a forward declaration
。
4) 自定義Input Source:
向Run Loop添加自定義Input Source只能使用Core Foundation的接口:CFRunLoopSourceCreate
創建一個source,CFRunLoopAddSource
向Run
Loop中添加source,CFRunLoopRemoveSource
從Run
Loop中刪除source,CFRunLoopSourceSignal
通知source,CFRunLoopWakeUp
喚醒Run
Loop。
Apple官方文檔提供了一個自定義Input Source使用模式。
主線程持有包含子線程的Run Loop和Source的context對象,還有一個用於保存需要運行操作的數據buffer。主線程需要子線程幹活時,首先將需要的操作數據添加到數據buffer,然後通知source,喚醒子線程Run Loop(因爲子線程可能正在sleep狀態,CFRunLoopWakeUp
喚醒Run
Loop可以通知線程醒來幹活),由於子線程也持有這個source和數據buffer,因此在觸發喚醒時可以使用這個數據buffer的數據來執行相關操作(需要注意數據buffer訪問時的同步)。
具體實現參見本文最後的Sample Code。
Run Loop的Observer
Core Foundation層的接口可以定義一個Run Loop的觀察者在Run Loop進入以下某個狀態時得到通知:
- Run loop的進入
- Run loop處理一個Timer的時刻
- Run loop處理一個Input Source的時刻
- Run loop進入睡眠的時刻
- Run loop被喚醒的時刻,但在喚醒它的事件被處理之前
- Run loop的終止
Observer的創建以及添加到Run Loop中需要使用Core Foundation的接口:
<span class="line-number" style="margin: 0px; padding: 0px;">1</span> <span class="line-number" style="margin: 0px; padding: 0px;">2</span> <span class="line-number" style="margin: 0px; padding: 0px;">3</span> <span class="line-number" style="margin: 0px; padding: 0px;">4</span> <span class="line-number" style="margin: 0px; padding: 0px;">5</span> <span class="line-number" style="margin: 0px; padding: 0px;">6</span> <span class="line-number" style="margin: 0px; padding: 0px;">7</span> |
|
首先創建Observer的context,然後調用Core Foundation方法CFRunLoopObserverCreate創建Observer,再加入到當前線程的Run Loop中,注意CFRunLoopObserverCreate方法的第二個參數是Observer觀察類型,有如下幾種:
<span class="line-number" style="margin: 0px; padding: 0px;">1</span> <span class="line-number" style="margin: 0px; padding: 0px;">2</span> <span class="line-number" style="margin: 0px; padding: 0px;">3</span> <span class="line-number" style="margin: 0px; padding: 0px;">4</span> <span class="line-number" style="margin: 0px; padding: 0px;">5</span> <span class="line-number" style="margin: 0px; padding: 0px;">6</span> <span class="line-number" style="margin: 0px; padding: 0px;">7</span> <span class="line-number" style="margin: 0px; padding: 0px;">8</span> <span class="line-number" style="margin: 0px; padding: 0px;">9</span> <span class="line-number" style="margin: 0px; padding: 0px;">10</span> |
|
對應Run Loop的各種事件,kCFRunLoopAllActivities比較特殊,可以觀察所有事件。具體樣例代碼請參考Sample Code。
總結
Run Loop就是一個處理事件源的循環,你可以控制這個Run Loop運行多久,如果當前沒有事件發生,Run Loop會讓這個線程進入睡眠狀態(避免再浪費CPU時間),如果有事件發生,Run Loop就處理這個事件。Run Loop處理事件和發送給Observer通知的流程如下:
- 1) 進入Run Loop運行,此時會通知觀察者進入Run Loop;
- 2) 如果有Timer即將觸發時,通知觀察者;
- 3) 如果有非Port的Input Sourc即將e觸發時,通知觀察者;
- 4)觸發非Port的Input Source事件源;
- 5)如果基於Port的Input Source事件源即將觸發時,立即處理該事件,跳轉到步驟9;
- 6)通知觀察者當前線程將進入休眠狀態;
- 7)將線程進入休眠狀態直到有以下事件發生:基於Port的Input Source被觸發、Timer被觸發、Run Loop運行時間到了過期時間、Run Loop被喚醒。
- 8) 通知觀察者線程將要被喚醒。
- 9) 處理被觸發的事件:
- 如果是用戶自定義的Timer,處理Timer事件後重新啓動Run Loop進入步驟2;
- 如果線程被喚醒又沒有到過期時間,則進入步驟2;
- 如果是其他Input Source事件源有事件發生,直接處理這個事件;
- 10)到達此步驟說明Run Loop運行時間到期,或者是非Timer的Input Source事件被處理後,Run Loop將要退出,退出前通知觀察者線程已退出。
什麼時候需要用到Run Loop?官方文檔的建議是:
- 需要使用Port或者自定義Input Source與其他線程進行通訊。
- 需要在線程中使用Timer。
- 需要在線程上使用performSelector*****方法。
- 需要讓線程執行週期性的工作。
我個人在開發中遇到的需要使用Run Loop的情況有:
- 使用自定義Input Source和其他線程通信
- 子線程中使用了定時器
- 使用任何performSelector*****到子線程中運行方法
- 使用子線程去執行週期性任務
- NSURLConnection在子線程中發起異步請求
Sample Code
RunLoop剛開始用確實坑很多,理解概念最好的方式還是動手寫代碼,寫了個例子放在GitHub上(工程NSThreadExample),歡迎大家討論。
Apple官方也有一個基於Run Loop的異步網絡請求示例程序SimpleURLConnections。