iOS RunLoop 探究

Junyiii發表於2017-04-23

RunLoop常見用法

AFN
AFN2.x中把網路請求全部都放在一個子執行緒中進行。由於子執行緒執行完任務後就會自動銷燬,所以在子執行緒中執行了一個Runloop保證執行緒不會被銷燬掉。(執行緒的建立和銷燬耗費的資源雖然很少,但是大量網路請求導致大量建立和銷燬所耗費的資源還是十分可觀的)

#pragma mark AFN
+ (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)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];

        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}複製程式碼

用CFRunloop也可建立一個Runloop

    CFRunLoopSourceContext context = {0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL};
    //A perform callback for the run loop source. This callback is called when the source has fired.
    //Availability
    context.perform = fire;

    CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);

    CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
    NSLog(@"自定義RunLoopRun");
    CFRunLoopRun();複製程式碼

執行緒與RunLoop的一些概念

執行緒安全
  • CFRunLoopRef 是執行緒安全的
  • NSRunLoop 非執行緒安全
執行緒執行
  • 子執行緒執行完任務自動銷燬
  • runloop實際是一個迴圈,並不會自動停止
  • 新增runloop後,run,如果要銷燬這個執行緒,必須要停止runloop。
  • 如果當前執行緒沒有Runloop就

關於AFN 3.x

  • 由於NSUrlSession參考了AF的2.x的優點,自己維護了一個執行緒池,做Request執行緒的排程與管理,所以在AF3.x中,沒有了常駐執行緒,都是用的時候run,結束的時候stop。

執行緒間的通訊

  • iOS執行緒間的通訊,實際上是各種輸入源,觸發Runloop去處理對應的事件。

在什麼情況下使用RunLoop

僅當在為你的程式建立輔助執行緒的時候,你才需要顯式執行一個run loop。
Run loop在你要和執行緒有更多的互動時才需要,比如以下情況:
>

  • 使用埠或自定義輸入源來和其他執行緒通訊
  • 使用執行緒的定時器
  • Cocoa中使用任何performSelector…的方法
  • 使執行緒週期性工作

RunLoop 詳細介紹

iOS RunLoop 探究

iOS RunLoop 探究

Run Loop的處理兩大類事件源:Timer Source和Input Source(包括performSelector* 方法簇、Port或者自定義Input Source),每個事件源都會繫結在Run Loop的某個特定模式mode上,而且只有RunLoop在這個模式執行的時候才會觸發該Timer和Input Source。

  • Runloop 處理兩大類事件源 1.Timer Source 2.Input Source
  • 如果沒有任何事件源新增到Run Loop上,Run Loop就會立刻exit
Input Source:傳遞非同步事件,通常訊息來源於其他執行緒或程式。
  • 基於埠的輸入源
    • Cocoa和Cocoa Foundation 內建支援使用埠相關的物件和函式來建立的機遇埠的源。
    • Cocoa中只要簡單的建立埠物件,將埠新增到Runloop即可。埠物件會自己處理建立和配置輸入源。
    • 在Core Fundation中,你必須人工建立埠和他的Runloop源。我們可以使用埠相關的函式(CFMachPortRef,CFMessagePortRef,CFSocketRef)來建立合適的物件。

Example:

void createPortSource()
{

    CFMessagePortRef port = CFMessagePortCreateLocal(kCFAllocatorDefault, CFSTR("com.someport"),myCallbackFunc, NULL, NULL);
    CFRunLoopSourceRef source =  CFMessagePortCreateRunLoopSource(kCFAllocatorDefault, port, 0);
    CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
    while (pageStillLoading) {
        NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
        CFRunLoopRun();
        [pool release];
    }

    CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
    CFRelease(source);
}複製程式碼
  • 自定義輸入源
    • 自定義輸入源需要人工從其他執行緒傳送。
    • 使用Core Fundation中的CFRunLoopSourceRef型別相關的函式來建立。
    • 需要定義訊息傳遞機制
      Example:
      void createCustomSource()
      {
      CFRunLoopSourceContext context = {0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL};
      CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
      CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
      while (pageStillLoading) {
        NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
        CFRunLoopRun();
        [pool release];
      }
      CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
      CFRelease(source);
      }複製程式碼
  • Cocoa的Selector源

    • 除了基於埠的源,Cocoa定義了自定義輸入源,允許你在任何執行緒執行selector方法
    • 當在其他執行緒上面執行selector時,目標執行緒須有一個活動的run loop。對於你建立的執行緒,這意味著執行緒在你顯式的啟動run loop之前是不會執行selector方法的,而是一直處於休眠狀態。(導致Crash
    • 和基於埠的源一樣。執行selector請求會在目標執行緒上序列化,減緩多執行緒上允許多個方法容易引起的同步問題。
  • 定時源

    • 定時源在預設的時間點同步方式傳遞訊息,這些訊息都會發生在特定時間或者重複的時間間隔。定時源直接傳遞訊息給處理例程,不會立即退出run loop.
    • 定時器與runloop中的特定模式有關。
      Example:
      //方法一:
      NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:4.0
                                                     target:self
                                                   selector:@selector(backgroundThreadFire:) userInfo:nil
                                                    repeats:YES];
      [[NSRunLoop currentRunLoop] addTimer:timerforMode:NSDefaultRunLoopMode];
      //方法二:
      [NSTimer scheduledTimerWithTimeInterval:10
                                        target:self
                                       selector:@selector(backgroundThreadFire:)
                                       userInfo:nil
                                       repeats:YES];複製程式碼

事件源按照函式呼叫棧的分類

  • 基於埠的輸入源
    • 在runloop中,被定義名為souce1。Cocoa和Core Foundation內建支援使用埠相關的物件和函式來建立的基於埠的源。例如,在Cocoa裡面你從來不需要直接建立輸入源。你只要簡單的建立埠物件,並使用NSPort的方法把該埠新增到run loop。埠物件會自己處理建立和配置輸入源。
  • 非基於port,自定義輸入源
RunLoop 觀察者

源是在合適的同步或非同步事件發生時觸發,而run loop觀察者則是在run loop本身執行的特定時候觸發。你可以使用run loop觀察者來為處理某一特定事件或是進入休眠的執行緒做準備。你可以將run loop觀察者和以下事件關聯:

  1. Runloop入口
  2. Runloop何時處理一個定時器
  3. Runloop何時處理一個輸入源
  4. Runloop何時進入睡眠狀態
  5. Runloop何時被喚醒,但在喚醒之前要處理的事件
  6. Runloop終止
RunLoop事件佇列

每次執行run loop,你執行緒的run loop對會自動處理之前未處理的訊息,並通知相關的觀察者。具體的順序如下:

  1. 通知觀察者run loop已經啟動
  2. 通知觀察者任何即將要開始的定時器
  3. 通知觀察者任何即將啟動的非基於埠的源
  4. 啟動任何準備好的非基於埠的源
  5. 如果基於埠的源準備好並處於等待狀態,立即啟動;並進入步驟9。
  6. 通知觀察者執行緒進入休眠
  7. 將執行緒置於休眠直到任一下面的事件發生:
    • 某一事件到達基於埠的源
    • 定時器啟動
    • Run loop設定的時間已經超時
    • run loop被顯式喚醒
  8. 通知觀察者執行緒將被喚醒。
  9. 處理未處理的事件
    • 如果使用者定義的定時器啟動,處理定時器事件並重啟run loop。進入步驟2
    • 如果輸入源啟動,傳遞相應的訊息
    • 如果run loop被顯式喚醒而且時間還沒超時,重啟run loop。進入步驟2
  10. 通知觀察者run loop結束。

因為定時器和輸入源的觀察者是在相應的事件發生之前傳遞訊息,所以通知的時間和實際事件發生的時間之間可能存在誤差。如果需要精確時間控制,你可以使用休眠和喚醒通知來幫助你校對實際發生事件的時間。

因為當你執行run loop時定時器和其它週期性事件經常需要被傳遞,撤銷run loop也會終止訊息傳遞。典型的例子就是滑鼠路徑追蹤。因為你的程式碼直接獲取到訊息而不是經由程式傳遞,因此活躍的定時器不會開始直到滑鼠追蹤結束並將控制權交給程式。

Run loop可以由run loop物件顯式喚醒。其它訊息也可以喚醒run loop。例如,新增新的非基於埠的源會喚醒run loop從而可以立即處理輸入源而不需要等待其他事件發生後再處理。

從這個事件佇列中可以看出:

①如果是事件到達,訊息會被傳遞給相應的處理程式來處理, runloop處理完當次事件後,run loop會退出,而不管之前預定的時間到了沒有。你可以重新啟動run loop來等待下一事件。

②如果執行緒中有需要處理的源,但是響應的事件沒有到來的時候,執行緒就會休眠等待相應事件的發生。這就是為什麼run loop可以做到讓執行緒有工作的時候忙於工作,而沒工作的時候處於休眠狀態。

什麼時候使用run loop

僅當在為你的程式建立輔助執行緒的時候,你才需要顯式執行一個run loop。Run loop是程式主執行緒基礎設施的關鍵部分。所以,Cocoa和Carbon程式提供了程式碼執行主程式的迴圈並自動啟動run loop。IOS程式中UIApplication的run方法(或Mac OS X中的NSApplication)作為程式啟動步驟的一部分,它在程式正常啟動的時候就會啟動程式的主迴圈。類似的,RunApplicationEventLoop函式為Carbon程式啟動主迴圈。如果你使用xcode提供的模板建立你的程式,那你永遠不需要自己去顯式的呼叫這些例程。

對於輔助執行緒,你需要判斷一個run loop是否是必須的。如果是必須的,那麼你要自己配置並啟動它。你不需要在任何情況下都去啟動一個執行緒的run loop。比如,你使用執行緒來處理一個預先定義的長時間執行的任務時,你應該避免啟動run loop。
如果你決定在程式中使用run loop,那麼它的配置和啟動都很簡單。和所有執行緒程式設計一樣,你需要計劃好在輔助執行緒退出執行緒的情形。讓執行緒自然退出往往比強制關閉它更好。

CFRunLoop 對外介面

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef

CFRunLoopSourceRef

  • Source0 只包含了一個回撥(函式指標),它並不能主動觸發事件。使用時,你需要先呼叫CFRunLoopSourceSignal(source),將這個Source標記為待處理時間,然後手動呼叫CFRunLoopWeakUp(runloop)來喚醒RunLoop,讓其處理這個事件。
  • Source1 包含了一個mach_port和一個回撥(函式指標),被用於通過核心和其他執行緒相互傳送訊息。這種Source能主動喚醒RunLoop的執行緒。

CFRunLoopTimerRef 是基於時間的觸發器
CFRunLoopObserverRef 是觀察者

Runloop 使用

Run Loop執行介面

  • 要操作Run Loop,Foundation層和Core Foundation層都有對應的介面可以操作Run Loop:
    Foundation層對應的是NSRunLoop,Core Foundation層對應的是CFRunLoopRef;
    兩組介面差不多,不過功能上還是有許多區別的:

  • 例如CF層可以新增自定義Input Source事件源、(CFRunLoopSourceRef)Run Loop觀察者Observer(CFRunLoopObserverRef),很多類似功能的介面特性也是不一樣的。
    NSRunLoop的執行介面:

//執行 NSRunLoop,執行模式為預設的NSDefaultRunLoopMode模式,沒有超時限制
- (void)run;
//執行 NSRunLoop: 引數為運時間期限,執行模式為預設的NSDefaultRunLoopMode模式 
- (void)runUntilDate:(NSDate *)limitDate;
//執行 NSRunLoop: 引數為執行模式、時間期限,返回值為YES表示是處理事件後返回的,NO表示是超時或者停止執行導致返回的
- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;複製程式碼
模式問題

Run Loop執行時只能以一種固定的模式執行,如果我們需要它切換模式,只有停掉它,再重新開啟它。
執行時它只會監控這個模式下新增的Timer Source和Input Source,如果這個模式下沒有相應的事件源,Run Loop的執行也會立刻返回的。注意Run Loop不能在執行在NSRunLoopCommonModes模式,因為NSRunLoopCommonModes其實是個模式集合,而不是一個具體的模式,我可以在新增事件源的時候使用NSRunLoopCommonModes,只要Run Loop執行在NSRunLoopCommonModes中任何一個模式,這個事件源都可以被觸發。

相關文章