iOS —— NSRunLoop / NSPart

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 detachNewThreadSelector:@selector(threadRoutine:) toTarget:self withObject:nil];
  • 創建一個NSThread類實例,然後調用start方法。
<span class="line-number" style="margin: 0px; padding: 0px;">1</span>
<span class="line-number" style="margin: 0px; padding: 0px;">2</span>
NSThread* aThread = [[NSThread alloc] initWithTarget:self selector:@selector(threadRoutine:) object:nil];
[aThread start];
  • 調用NSObject的+performSelectorInBackground:withObject:方法生成子線程。
<span class="line-number" style="margin: 0px; padding: 0px;">1</span>
[myObj performSelectorInBackground:@selector(threadRoutine:) withObject:nil];
  • 創建一個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>
- (void)threadRoutine
{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  
  BOOL moreWorkToDo = YES;
    BOOL exitNow = NO;
    NSRunLoop* runLoop = [NSRunLoop currentRunLoop];

    NSMutableDictionary* threadDict = [[NSThread currentThread] threadDictionary];
    [threadDict setValue:[NSNumber numberWithBool:exitNow] forKey:@"ThreadShouldExitNow"];

  //添加事件源
    [self myInstallCustomInputSource];

    while (moreWorkToDo && !exitNow)
    {
        //執行線程真正的工作方法,如果完成了可以設置moreWorkToDo爲False

        [runLoop runUntilDate:[NSDate date]];

        exitNow = [[threadDict valueForKey:@"ThreadShouldExitNow"] boolValue];
    }

    [pool release];
}
  • 必須創建一個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>
- (void)main
{
    @autoreleasepool {
        NSLog(@"starting thread.......");
        NSTimer *timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(doTimerTask) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
        [timer release];
        while (! self.isCancelled) {
            [self doOtherTask];
            BOOL ret = [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
            NSLog(@"after runloop counting.........: %d", ret);
        }
        NSLog(@"finishing thread.........");
    }
}

- (void)doTimerTask
{
    NSLog(@"do timer task");
}

- (void)doOtherTask
{
    NSLog(@"do other task");
}

我們看到入口方法裏創建了一個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>
//運行 NSRunLoop,運行模式爲默認的NSDefaultRunLoopMode模式,沒有超時限制
- (void)run;

//運行 NSRunLoop: 參數爲運行模式、時間期限,返回值爲YES表示是處理事件後返回的,NO表示是超時或者停止運行導致返回的
- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;

//運行 NSRunLoop: 參數爲運時間期限,運行模式爲默認的NSDefaultRunLoopMode模式 
-(void)runUntilDate:(NSDate *)limitDate;

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>
//運行 CFRunLoopRef
void CFRunLoopRun();

//運行 CFRunLoopRef: 參數爲運行模式、時間和是否在處理Input Source後退出標誌,返回值是exit原因
SInt32 CFRunLoopRunInMode (mode, seconds, returnAfterSourceHandled);

//停止運行 CFRunLoopRef
void CFRunLoopStop( CFRunLoopRef rl );

//喚醒 CFRunLoopRef
void CFRunLoopWakeUp ( CFRunLoopRef rl );

詳細講解下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>
while (!Done)
{
    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate
                dateWithTimeIntervalSinceNow:10]];
    NSLog(@"exiting runloop.........:");
}
  • - (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>
while (!Done)
{
    BOOL ret = [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
                                        beforeDate:[NSDate distantFuture]];
    NSLog(@"exiting runloop.........: %d", ret);
}

那麼如何知道一個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>
while (!self.isCancelled)
{
    [self doOtherTask];

    SInt32 result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 2, YES);
    if (result == kCFRunLoopRunStopped)
    {
        [self cancel];
    }
    NSLog(@"exit run loop.........: %ld", result);
}

如果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>
enum {
    kCFRunLoopRunFinished = 1, //Run Loop結束,沒有Timer或者其他Input Source
    kCFRunLoopRunStopped = 2, //Run Loop被停止,使用CFRunLoopStop停止Run Loop
    kCFRunLoopRunTimedOut = 3, //Run Loop超時
    kCFRunLoopRunHandledSource = 4 ////Run Loop處理完事件,注意Timer事件的觸發是不會讓Run Loop退出返回的,即使CFRunLoopRunInMode的第三個參數是YES也不行
};

注意: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>
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:

performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:

performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:

cancelPreviousPerformRequestsWithTarget:
cancelPreviousPerformRequestsWithTarget:selector:object:

這些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>
CFRunLoopObserverContext  context = {0, (__bridge void *)(self), NULL, NULL, NULL};
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopBeforeTimers, YES, 0, &myRunLoopObserver, &context);
if (observer)
{
  CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer,
                                 kCFRunLoopCommonModes);
}

首先創建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 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
};

對應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

參考資料

Threading Programming Guide

NSRunLoop Class Reference

CFRunLoop Reference

CFRunLoopObserver Reference

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章