看蘋果官方文檔怎么說RunLoop

這幾天研究了一下iOS的Runloop,看了不少的文章,收獲不少,但是疑問也挺多。所以我就試著去翻譯了并分析總結了一下蘋果的Runloop文檔,注意:并不是CFRunloop的源碼,而是這篇總體概述了Runloop的文檔

這里先給出一些有關RunLoop的官方文檔及一些好的文章的鏈接:

官方文檔:

Core Foundation框架源碼

CFRoopLoop源碼

蘋果對于Runloop的概括說明文檔

CFRunLoop的官方參考文檔

NSRunLoop的官方參考文檔

入門可以看:

《iOS RunLoop入門小結》

《RunLoop入門 看我就夠了》

大神文章:

《深入理解RunLoop》

《iOS刨根問底-深入理解RunLoop》

《iOS程序啟動與運轉——RunLoop個人小結》

RunLoop怎么用:

《RunLoop已入門?不來應用一下?》

RunLoop問題集:

《RunLoop問題集》

《iOS基礎面試題之RunLoop篇》


提前說明,這篇文章你可以對照著英文文檔來看,目錄結構是一樣的。但并不是逐句翻譯的,會有省略,并加上了我自己的總結分析。

本文目錄

  • Run Loops(RunLoop)
  • Anatomy of a Run Loop(RunLoop的剖析)
  • Run Loop Modes(RunLoop的Mode)
  • Input Sources
      1. Port-Based Sources(基于端口的輸入源)
      1. Custom Input Sources(自定義輸入源)
      1. Cocoa Perform Selector Sources(PerformSelector源)
  • Timer Sources(定時器源)
  • Run Loop Observers(RunLoop的觀察者Observer)
  • The Run Loop Sequence of Events(RunLoop的內部運行邏輯)
  • 關于CFRunLoop(這個為個人補充)
  • 關于CFRunLoopMode(這個為個人補充)
  • When Would You Use a Run Loop?(什么時候使用RunLoop?)
  • Using Run Loop Objects (如何使用RunLoop對象)
      1. Getting a Run Loop Object(獲取RunLoop對象)
      1. Configuring the Run Loop(配置RunLoop)
      1. Starting the Run Loop(啟動RunLoop)
      1. Exiting the Run Loop(退出RunLoop)
      1. Thread Safety and Run Loop Objects(線程安全和RunLoop對象)
  • Configuring Run Loop Sources(配置RunLoop的Source)
      1. Defining a Custom Input Source(定義自定義輸入源,即Source0)
      1. Configuring Timer Sources(配置定時器源,即Timer)
      1. Configuring a Port-Based Input Source(配置基于端口的輸入源,即Source1)

Run Loops(RunLoop)

RunLoop是與線程相關的基礎架構中的一部分。RunLoop是一個事件處理環,它可以用來安排工作并協調傳入的事件。RunLoop的目的是在有任務的時候保持線程的運行,在沒有任務的時候使線程休眠。

RunLoop的使用管理不是完全自動的。你必須在合適的時機使用線程代碼啟動RunLoop,并對接收的事件進行反應處理。Cocoa(NSRunLoop)和Core Foundation(CFRunLoop)都提供了RunLoop的對象。

每一個線程都對應著一個RunLoop。在應用程序啟動的時候,主線程的RunLoop會自動啟動,而輔助線程(即子線程)是需要你手動去獲取的

這里有CFRunLoopNSRunLoop的參考文檔。

Anatomy of a Run Loop(RunLoop的剖析)

RunLoop與它的名字的意思相像,它是一個環,線程進入這個環并且可以對收到的事件進行運行和處理。

image

如上圖所示:RunLoop從兩種不同類型的源接收事件,一個是輸入源(Input Source),另一個是定時器源(Timer Source)。Input Source 提供異步事件,通常是來自另一個線程或不同應用程序的消息Timer Source提供同步事件,是發生在預定時間的或重復間隔里的

除了處理輸入的源,RunLoop還會生成有關RunLoop的通知,已經注冊了的Observer可以接收這些通知并使用它們在線程上執行其它的處理。你可以通過Core Foundation在你的線程上使用RunLoop Observer。

分析說明:

  • RunLoop有兩種源:Input sources和Timer sources,Input sources里面又分了幾種。RunLoop還包含了Observer。
  • 看圖,Input Source里面有三種:Port、Custom、performSelector,其實只有兩種:基于端口的輸入源(Port)和自定義的輸入源(Custom),因為performSelector是蘋果自定義的輸入源(它比較特殊)。
  • 關于CFRunLoop的里面的Source又分為了Source1和Source0,我們下面會說。
下面如無特別說明,Source就是指Input sources,Timer就是指Timer sources。

Run Loop Modes(RunLoop的Mode)

一個RunLoop Mode是多個輸入源(Input Source)、多個定時器(Timer)、多個觀察者(Observer)的集合。每次你運行一個RunLoop,你都得顯式或隱式地指定要運行的Mode。在RunLoop的運行過程中,僅監測該Mode下的源(sources)并允許其傳遞事件,并且只有該Mode下的Observer才有作用。在其它Mode下的源(sources)會掛起任何新的事件,直到RunLoop以在該Mode下運行。

分析說明:

  • 每個線程只能有一個對應的RunLoop,RunLoop必須手動去開啟才能存在,但是主線程對應的RunLoop是在應用啟動的時候自動就開啟了,所以只需要你主動去開啟子線程的RunLoop不用管主線程的RunLoop。關于RunLoop的創建下面會說。現在先了解這一點。

分析說明:
一般我們常用的Mode有三種:

  1. kCFRunLoopDefaultMode(CFRunLoop)/NSDefaultRunLoopMode(NSRunLoop)

默認模式,在RunLoop沒有指定Mode的時候,默認就跑在DefaultMode下。一般情況下App都是運行在這個mode下的

  1. CFStringRef)UITrackingRunLoopMode(CFRunLoop)/UITrackingRunLoopMode(NSRunLoop)

一般作用于ScrollView滾動的時候的模式,保證滑動的時候不受其他事件影響。

  1. kCFRunLoopCommonModes(CFRunLoop)/NSRunLoopCommonModes(NSRunLoop)
    這個并不是某種具體的Mode,而是一種模式組合,在主線程中默認包含了NSDefaultRunLoopMode和 UITrackingRunLoopMode。子線程中只包含NSDefaultRunLoopMode。

注意:
在選擇RunLoop的runMode時不可以填這種模式否則會導致RunLoop運行不成功
在添加事件源的時候填寫這個模式就相當于向組合中所有包含的Mode中注冊了這個事件源
③你也可以通過調用CFRunLoopAddCommonMode()方法將自定義Mode放到kCFRunLoopCommonModes組合。

分析說明:

  • 注意,一個RunLoop里會有多個Mode(這點后面會說明);
  • 一個Mode下有多個Source、Timer、Observer;
  • RunLoop的運行必須指定一個Mode,不管是顯式或隱式的指定;
  • RunLoop在一個Mode下運行,該Mode里的Source、Timer、Observer才會有效,其它Mode里的Source、Timer、Observer就不能有效果了。所以說Mode其實是為了把不同的Source、Timer、Observer分開來;
  • 其它沒有運行的Mode會掛起的新來的事件,只有當RunLoop運行到該Mode下時,該Mode的新事件才會被處理。

你可以通過Mode的名字來識別和使用這些Mode。Cocoa 和 Core Foundation都定義了一個默認的Mode和其它一些常用的Mode。你也可以使用任何名稱自定義一個Mode,但必須確保這個自定義的Mode里有一個或多個Input Source、Timer Source、Observer。

你可以在不同Mode下運行RunLoop,以此過濾掉不需要的來源中的事件。

分析說明:

  • 為什么要用多個Mode,就是為了不同的Mode里面的Source、Timer、Observer互不影響。
  • 典型的例子就是NSTimer在平常管用,因為主線程平常是在NSDefaultRunLoopMode(kCFRunLoopDefaultMode)的默認模式下,但在scrollview滑動的時候NSTimer就不管用了,因為scrollview滑動的時候,RunLoop是運行在UITrackingRunLoopMode模式下的。所以要想NSTimer在滑動的時候也管用,就要將NSTimer添加進NSDefaultRunLoopMode和UITrackingRunLoopMode這兩個Mode下。
  • NSTimer是一種Timer Source。
  • 這里說明一下,RunLoop必須有Timer或Source才能運行,否則會退出,即使只有Observer也不行。

Input Sources

Input Sources以異步方式向線程傳遞事件。事件的來源取決于Input Sources的類型,通常為兩個類型中的一個:1. 基于端口(Port)的輸入源,它監視應用程序的Mach端口;2. 自定義(Custom)輸入源,監視自定義事件源。

就RunLoop來說,Input Sources是哪一種類型并無所謂。兩種類型的源的唯一區別是,基于端口(Port)的輸入源自動從內核發出信號,而自定義(Custom)輸入源必須手動地從另一個線程發出信號。

分析說明:

  • 這里我也一知半解,關于Mach端口可以去看這篇大神文章里的說明。至于Input Source 和Source1和Source0的關系在后面會說到。

1. Port-Based Sources(基于端口的輸入源)

在Cocoa和Core Foundation里,提供了與端口(Port)相關的的對象和函數,你可以使用它們來創建基于端口的輸入源。

比如,在Cocoa里,你根本不必直接去創建一個端口輸入源,你只需要創建一個端口對象,并使用NSPort的方法將這個端口對象添加到RunLoop中,端口對象就會自動為你處理輸入源的創建和配置。

在Core Foundation中,您必須手動創建端口及其RunLoop源。

有關如何設置和配置基于端口的自定義源的示例,請參閱配置基于端口的輸入源

分析說明:

  • 基于端口的輸入源:就是Source1。具體后面說明。

2. Custom Input Sources(自定義輸入源)

要創建自定義輸入源,必須使用 Core Foundation 里的CFRunLoopSourceRef的相關函數。

有關如何創建自定義輸入源的示例,請參閱定義自定義輸入源。有關自定義輸入源的參考信息,另請參閱CFRunLoopSource參考

3. Cocoa Perform Selector Sources(PerformSelector源)

除了基于端口的輸入源(Port-Based Sources),Cocoa還定義了一個自定義輸入源,允許你在任何線程去執行一個selector。

與基于端口的源相同的是,Perform Selector請求在目標線程上被執行,從而緩解了一個線程上運行多個方法時可能會發生的同步問題。與基于端口的源不同的是,一個Perform Selector Source會在執行完后從這個RunLoop中被移除

想要在目標線程上執行一個selector,目標線程必須有一個活動的RunLoop。主線程在應用程序啟動時,已經具備了一個RunLoop;而子線程必須去你自己手動獲取RunLoop,子線程的RunLoop才會存在。

分析說明:

  • 關于RunLoop是如何獲取和創建的,可以去看這篇文章

RunLoop通過一次循環處理所有排隊的Perform Selector,而不是每次循環只處理一個

在其它線程上執行selector的方法如下

//在主線程的下一個RunLoop的循環里,去執行selector。這兩個方法可以選擇是否阻塞當前線程直到這個selector被執行完畢。
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:
 
//在任意線程中(前提是你有這個線程的對象)執行selector。這兩個方法可以選擇是否阻塞當前線程直到這個selector被執行完畢。
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:
 
//在RunLoop的下一個循環周期和可選的延遲之后,在當前線程執行selector。因為它必須等到下一個循環去執行selector,所以這些方法提供了來自當前執行代碼的自動迷你延遲。多個selector按照排隊順序執行。
performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:
 
//這個是針對performSelector:withObject:afterDelay: or performSelector:withObject:afterDelay:inModes: method使用的,用來取消發送到當前線程的消息。
cancelPreviousPerformRequestsWithTarget:
cancelPreviousPerformRequestsWithTarget:selector:object:

分析說明:

  • Perform Selector也是自定義輸入源。
  • Perform Selector它是比較特殊的,也是屬于Source0(非端口輸入源)。

Timer Sources(定時器源)

定時器源在預設的時間將時間同步傳遞給你的線程。定時器是線程通知它自己干事情的一種方式

雖然定時器是基于時間的通知,但是它并不是一種實時機制。與輸入源類似,Timer與Mode相關聯,如果Timer不在RunLoop當前所運行的Mode中,它就不會被觸發(分析說明:這一點上面提過了)。還有,如果RunLoop正在執行一段程序,而這時定時器的時間到了,它也不會被觸發,它會等下一次的時間點去觸發。

分析說明:

  • 假如一個定時器的觸發時間是5點0秒,5點10秒,5點20秒...那么到了5點0秒的時候,假如RunLoop正在執行一大段代碼,那么定時器不會被觸發,它只能等5點10秒這次了。如果5點10這次也錯過了,那么就等5點20秒了。

Run Loop Observers(RunLoop的觀察者Observer)

Observer在RunLoop的特殊位置觸發。可以使用observer來準備線程以處理事件,或在線程進入休眠狀態之前準備線程。

observer觸發的位置(可以去看下面的那張圖):

  1. 即將進入RunLoop,通知observer;
  2. 即將處理Timer,通知observer;
  3. 即將處理Source(非端口的輸入源),通知observer;
  4. 線程即將休眠,通知observer;
  5. 線程剛被喚醒,但在它處理喚醒它的事件之前,通知observer;
  6. 線程退出了RunLoop,通知observer;

你可以使用Core Foundation添加Observer到應用程序里,需使用CFRunLoopObserverRef類型。

與Timer類似,Observer可以使用一次或重復使用。一次性Observer在觸發后將其自身從RunLoop中移除,而重復的Observer會繼續存在于RunLoop中,您可以指定Observer在創建時運行一次還是重復運行

The Run Loop Sequence of Events(RunLoop的內部運行邏輯)

每一次運行RunLoop,線程對應的RunLoop就會處理掛起的事件,并通知觀察者。它執行的順序如下

  1. 通知Observer即將進入RunLoop
  2. 通知Observer即將處理Timer
  3. 通知Observer即將處理Source0(非端口的輸入源)
  4. 處理Source0(非端口的輸入源)
  5. 如果有Source1(基于端口的輸入源)準備就緒并等待被觸發,立即處理該事件,并跳到步驟9
  6. 通知Observer即將休眠
  7. 線程休眠,直到發生以下事件之一:
    • 一個事件到達Source1(基于端口的輸入源)
    • 一個定時器(Timer)觸發
    • RunLoop超時
    • RunLoop被手動喚醒(例如添加一個Source0非端口的輸入源)
  8. 通知Observer線程剛剛喚醒
  9. 處理待處理的事件
    • 如果用戶定義的Timer觸發了,則處理這個定時器事件并重新啟動RunLoop循環,跳到步驟2
    • 如果輸入源觸發了,則傳遞事件
    • 如果RunLoop被手動喚醒,但尚未超時,重新啟動RunLoop循環,跳到步驟2
  10. 通知Observer RunLoop已經退出。

可以使用RunLoop對象顯式喚醒RunLoop,其它事件也可能導致RunLoop被喚醒,例如添加一個Source0(非端口的輸入源)會喚醒RunLoop,以便立即處理輸入源,而不是等到其它事件發生

****注意:下圖有錯誤,最左邊應該改為Source1(port),且缺少一個超時喚醒;10應該改為通知Observer,RunLoop已經退出,而不是即將退出****。

image

以下是CFRunLoop的一些分析,文檔中并沒有,屬于補充,幫助更好的理解。這里也會說明Source1和Source0。

關于CFRunLoop

在Core Foundation中,CFRunLoop的結構大致如下:

struct __CFRunLoop {
    CFMutableSetRef _commonModes;     // Set
    CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
    CFRunLoopModeRef _currentMode;    // Current RunLoop Mode
    CFMutableSetRef _modes;           // Set
    ...
};

這里我們可以看到CFRunLoop里:

  • CFMutableSetRef _modes:說明一個RunLoop中有多個Mode。
  • 還有一個叫currentMode的,這就是RunLoop當前所運行的Mode,正如上文所說的,RunLoop只能指定一個Mode來運行。(補充,記住,RunLoop要想切換Mode,只能退出RunLoop,再指定一個Mode重新運行。)
  • commonModes:一個Mode可以將自己標記為“common”屬性(通過使用其 Mode的Name添加到RunLoop的“commonModes”中)。主線程的 RunLoop 里有兩個預置的Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。這兩個Mode都已經被標記為Common”屬性。kCFRunLoopCommonModes/NSRunLoopCommonModes包含這兩個Mode。
  • commonModeItems:Source/Observer/Timer都是item,你可以將source/timer/observer放入到RunLoop的commonModeItems中。每當 RunLoop 的內容發生變化時,RunLoop 都會自動將commonModeItems里的 Source/Observer/Timer同步到具有 “Common” 標記的所有Mode里。所以說,NSTimer有兩種方式去解決滑動時不運行,一種方式是上面所說,將NSTimer對象加入到kCFRunLoopDefaultMode和UITrackingRunLoopMode(或者NSRunLoopCommonModes中)中;另一種方式就是將Timer加入到頂層的RunLoop的 “commonModeItems”中。”commonModeItems” 會被RunLoop自動更新到所有具有“Common”屬性的Mode里去。

關于CFRunLoopMode

CFRunLoopMode的結構大致如下:

struct __CFRunLoopMode {
    CFStringRef _name;            // Mode Name, 例如 @"kCFRunLoopDefaultMode"
    CFMutableSetRef _sources0;    // Set
    CFMutableSetRef _sources1;    // Set
    CFMutableArrayRef _observers; // Array
    CFMutableArrayRef _timers;    // Array
    ...
};

看CFRunLoopMode的結構發現,Mode里面有Source(在set集合里)、Observer(array數組里)、Timer(在array數組里)。

而根據前面的文檔,我們知道RunLoop的源有Timer Source和Input Source,RunLoop還包括了Observer。這里我們對應一下,Timer Source就是CFRunLoop的Timer,Input Source就是CFRunLoop的Source,Observer是CFRunLoop的Observer。

蘋果的文檔里又將Input Source分為了基于端口的輸入源和自定義輸入源,我們再看CFRunLoop的結構,發現里面的Source分為了Source0和Source1,那么Source0和Source1怎么區分呢?

我去找了
CFRunLoopSource的文檔來看:

CFRunLoopSource是RunLoop的輸入源的抽象,輸入源通常是異步事件。輸入源在CFRunLoop里包括:CFMachPort, CFMessagePort, and CFSocket。

CFRunLoopSource有兩類:

  • 版本0(Version 0),即Source0,這樣命名是因為它的上下文結構的版本字段為0。Source0在應用里必須手動管理(注意了,Source0是手動去觸發的)。當一個Source0準備觸發的時候,必須使用CFRunLoopSourceSignal通知RunLoop這個Source準備觸發了。CFSocket是Source0。

  • 版本1(Version 1),即Source1。Source1是由RunLoop和內核管理的。當消息到達Mach端口的時候,Source1會自動發出信號。CFMachPort和CFMessagePort是Source1。

總結一下,在前文中的Input Source又分了基于端口的輸入源和自定義輸入源:這里基于端口的輸入源就為Source1,而自定義輸入源應該就是Source0(自定義輸入源需要手動從另一個線程觸發)。至于performSelector比較特殊,也應該是屬于Source0(非端口)的,至于內部到底是怎么實現的,我就不清楚了。

總的來說:輸入源就分為基于端口的輸入源Source1和非端口的輸入源Source0
蘋果文檔里稍微說了一下怎么配置Source的,在文章最后。


When Would You Use a Run Loop?(什么時候使用RunLoop?)

  1. 你需要顯式運行RunLoop的唯一一種情況是,為應用程序創建輔助線程(這里的輔助線程就是指子線程)。因為應用程序的主線程的RunLoop會在應用創建的時候自動啟動,所以不需要你去管主線程的RunLoop。

  2. 對于輔助線程,你需要確定是否需要RunLoop,如果是,則自行去配置并啟動它。通常在所有的情況下,你都不需要去啟動一個線程的RunLoop。比如你要使用一個線程去執行一個長時間運行且預定義的任務,你可以避免去使用RunLoop。

  3. RunLoop適用于這種情況:當你希望與線程進行更多的交互時。比如,如果你計劃執行以下任何操作,則需要啟動RunLoop:

    • 使用端口或自定義的輸入源與其他線程通信
    • 在線程上使用定時器
    • 在Cocoa框架下,使用任何的performSelector方法
    • 保持線程以執行定期的任務

如果您確實選擇使用RunLoop,則配置和設置非常簡單。與線程的編程一樣,你應該確定在適當的時機退出RunLoop。并且,最好通過退出而不是強制終止來結束一個線程。

分析說明:

  • 主線程對應的RunLoop是在應用創建的時候自動開啟的,而子線程的RunLoop需要你手動去獲取,你不去獲取,子線程的RunLoop就不存在。
  • 也就是說,當你在子線程使用NSTimer的時候或者你對子線程使用performSelector系列方法時,必須先去將子線程的RunLoop開啟了。
  • performSelector需要RunLoop才能有用。
  • 這里的performSelector是指上面列出的方法,應該不包括performSelectorInBackGround:withObject:,這個方法是開啟一個新的子線程去執行任務。

Using Run Loop Objects (如何使用RunLoop對象)

  • 一個RunLoop對象提供了添加Input Source、Timer Source、Observer這些主要接口。
  • 一個線程只有一個與之關聯的RunLoop對象。
  • 在Cocoa中,RunLoop對象是NSRunLoop類的實例;在底層應用中,它是CFRunLoopRef類型的指針。

1. Getting a Run Loop Object(獲取RunLoop對象)

獲取RunLoop,可以使用下面的方式:

//獲得當前線程的RunLoop
[NSRunLoop currentRunLoop];
 
//主線程的RunLoop
[NSRunLoop mainRunLoop];
 
//CFRunLoop方法,獲得當前現成的RunLoop
CFRunLoopRef CFRunLoopGetCurrent(void);
 
//CFRunLoop方法,獲得主線程的RunLoop
CFRunLoopRef CFRunLoopGetMain(void);

也可以使用NSRunLoop的實例方法:- (CFRunLoopRef)getCFRunLoop; 返回一個CFRunLoopRef類型的RunLoop。

2. Configuring the Run Loop(配置RunLoop)

當你在子線程運行一個RunLoop的時候,你至少得添加一個Source或Timer給它,否則當你運行它時,它會立即退出,即使有Observer也不行,必須有Source或Timer

除了配置源(Input Source和Timer)給RunLoop,你也可以配置Observer并使用它來監測RunLoop的不同執行階段。你可以使用 CFRunLoopObserverRef 類型和 CFRunLoopAddObserver 函數去添加一個Observer到RunLoop。注意,Observer只能使用Core Foundation(CFRunLoop)的相關方法來創建,即使在Cocoa中也是這樣,也就是說,NSRunLoop沒有創建Observer的相關方法

下面是一個例子:創建Observer并添加到RunLoop中,Observer用來監視RunLoop的所有活動。(例子不用深究)

 
- (void)threadMain
{
    // 應用程序采用垃圾回收機制,所以不需要autorelease pool
   
    //獲取當前的RunLoop
    NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];
 
    // 創建一個Observer,kCFRunLoopAllActivities監視所有活動
    CFRunLoopObserverContext  context = {0, self, NULL, NULL, NULL};
    CFRunLoopObserverRef    observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
            kCFRunLoopAllActivities, YES, 0, &myRunLoopObserver, &context);//myRunLoopObserver是一個回調函數
   
    //如果observer存在,就將其關聯到RunLoop上
    if (observer)
    {
        CFRunLoopRef    cfLoop = [myRunLoop getCFRunLoop];
        CFRunLoopAddObserver(cfLoop, observer, kCFRunLoopDefaultMode);
    }
   
    // 創建一個定時器Timer。注意使用scheduledTimerWithTimeInterval:方法的定時器會自動添加到當前RunLoop的默認模式(kCFRunLoopDefaultMode)下。RunLoop必須有一個Source或Timer才能正常運行
    [NSTimer scheduledTimerWithTimeInterval:0.1 target:self
                selector:@selector(doFireTimer:) userInfo:nil repeats:YES];
   
    //使用do while循環創建RunLoop的退出時機。運行RunLoop十次。
    NSInteger    loopCount = 10;
    do
    {
        // Run the run loop 10 times to let the timer fire.
        [myRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
        loopCount--;
    }
    while (loopCount);
}

這段代碼下面有這么一段話:當你為長期存在的線程配置RunLoop時,最好添加一個輸入源(Source)來接收消息。雖然你可以只添加一個Timer定時器到RunLoop中,但是Timer一旦被觸發,它通常就失效了,這會造成RunLoop的退出。如果你添加一個重復觸發的定時器Timer,它會使RunLoop運行更長的時間,但是這就涉及到了定期觸發定時器去喚醒線程,這實際上是另一種方式的輪詢。相比之下,輸入源會等待事件發生,讓線程保持休眠狀態

3. Starting the Run Loop(啟動RunLoop)

只有在輔助線程(子線程)中才需要啟動RunLoop。一個RunLoop必須有一個Source或Timer,如果沒有,RunLoop會立即退出。

這里有幾種啟動RunLoop的方式:

  • 無條件地啟動
  • 設置一個超時值來啟動
  • 通過特定的Mode啟動

無條件地進入RunLoop是最簡單也是最不被推薦的方式。無條件地RunLoop會使線程置于永久循環之中,可以添加和刪除輸入源和定時器,但停止RunLoop的方式是終止它。

最好使用第二種方式,設置一個超時值。設置一個時間值后,RunLoop將一直運行,直到事件到達或者超出時間值。如果事件到達,則將該事件分派給處理程序進行處理,然后退出RunLoop,然后,你的代碼可以重新啟動RunLoop以處理下一個事件。如果時間到了,你只需要重新啟動RunLoop或使用這個時間去進行任何需要的內務處理。

除了時間值,還可以使用特定的Mode去運行RunLoop。設置時間值和Mode并不互斥。Mode類型將限制事件傳遞到RunLoop的源類型。

下面的代碼顯示了RunLoop的基本結構,實質上,你將輸入源和定時器添加到RunLoop中,然后重復的調用一段程序(這里是do-while)去啟動RunLoop,每次這段調用RunLoop的程序返回時,都去檢查是否出現了退出該線程的條件。如果有,則不再次啟動RunLoop了,將會退出線程。如果沒有,再次啟動RunLoop。

- (void)skeletonThreadMain
{
    // Set up an autorelease pool here if not using garbage collection.
    BOOL done = NO;
 
    // Add your sources or timers to the run loop and do any other setup.
 
    do
    {
        // Start the run loop but return after each source is handled.
        SInt32    result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, YES);
 
        // If a source explicitly stopped the run loop, or if there are no
        // sources or timers, go ahead and exit.
        if ((result ** kCFRunLoopRunStopped) || (result ** kCFRunLoopRunFinished))
            done = YES;
 
        // Check for any other exit conditions here and set the
        // done variable as needed.
    }
    while (!done);
 
    // Clean up code here. Be sure to release any allocated autorelease pools.
}

補充:可以遞歸地運行RunLoop,也就是說,可以嵌套RunLoop。

分析說明:

下面是NSRunLoop中啟動RunLoop的幾種方法:

//Puts the receiver into a permanent loop, during which time it processes data from all attached input sources.
- run
 
//Runs the loop once, blocking for input in the specified mode until a given date.
- runMode:beforeDate:
 
//Runs the loop until the specified date, during which time it processes data from all attached input sources.
- runUntilDate:
 
//Runs the loop once or until the specified date, accepting input only for the specified mode.
- acceptInputForMode:beforeDate:
 

4. Exiting the Run Loop(退出RunLoop)

在RunLoop處理事件之前,有兩種退出方式:

  • 配置RunLoop的超時值,超時就會退出
  • 使用CFRunLoopStop函數顯式停止RunLoop

如果您可以管理它,那么使用超時值肯定是首選。指定超時值可讓運行循環完成所有正常處理,包括在退出之前向運行循環觀察器發送通知。

使用該CFRunLoopStop函數顯式停止運行循環會產生類似于超時的結果。運行循環發出任何剩余的運行循環通知,然后退出。不同之處在于,您可以在無條件啟動的運行循環中使用此技術。

還有一種不可靠的退出方式:

  • 刪除輸入源和定時器,但這不是停止運行循環的可靠方法。某些系統例程將輸入源添加到運行循環以處理所需的事件。因為你的代碼可能不知道這些輸入源,所以它將無法刪除它們,這將阻止RunLoop退出。

分析說明:

  • 退出的三種方式
      1. 超時
      1. CFRunLoopStop函數顯式停止
      1. 刪除Source和Timer,但這種方式不可靠

5. Thread Safety and Run Loop Objects(線程安全和RunLoop對象)

Core Foundation的CFRunLoop是線程安全的,可以從任何線程調用。但是,應該盡可能地在RunLoop所屬于的線程中,去配置RunLoop

Cocoa NSRunLoop類不是線程安全的。你應該在RunLoop所屬于的線程中去修改RunLoop。將Source和Timer添加到屬于不同線程的RunLoop可能會出現錯誤。

Configuring Run Loop Sources(配置RunLoop的Source)

以下部分顯示了如何在Cocoa和Core Foundation中設置不同類型輸入源的示例。

1. Defining a Custom Input Source(定義自定義輸入源,即Source0)

這部分沒看,感興趣的人可以自行去文檔中看一下。

2. Configuring Timer Sources(配置定時器源,即Timer)

要創建一個定時器源,需要做的就是創建一個定時器對象并將其添加到RunLoop上

在Cocoa中,使用NSTimer類創建定時器對象,在Core Foundation中使用CFRunLoopTimerRef類型。NSTimer類只是對Core Foundation的擴展。

創建NSTimer對象的類方法有如下兩類:

第一類:

 + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
 + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo
 + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block ;//iOS10以后新加的方法

上面這類方法在創建NSTimer對象后,會自動添加到RunLoop的默認Mode(NSDefaultRunLoopMode/kCFDefaultRunLoopMode)中

第二類:

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
 + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo
 + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block ;//iOS10以后新加的方法
 

上面這類方法在創建了NSTimer對象后,必須手動添加到RunLoop的Mode中,使用addTimer:forMode:方法。你可以選擇Mode的類型,Mode的默認類型還是其他類型。

注意一點:定時器必須添加到RunLoop中才能夠使用

例子:使用NSTimer創建和調度計時器

NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];
 
// 創建并調度第一個定時器
NSDate* futureDate = [NSDate dateWithTimeIntervalSinceNow:1.0];
NSTimer* myTimer = [[NSTimer alloc] initWithFireDate:futureDate
                        interval:0.1
                        target:self
                        selector:@selector(myDoFireTimer1:)
                        userInfo:nil
                        repeats:YES];
[myRunLoop addTimer:myTimer forMode:NSDefaultRunLoopMode];
 
// 創建并調度第二個定時器
[NSTimer scheduledTimerWithTimeInterval:0.2
                        target:self
                        selector:@selector(myDoFireTimer2:)
                        userInfo:nil
                        repeats:YES];

例子:使用Core Foundation創建和調度計時器

CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFRunLoopTimerContext context = {0,NULL,NULL,NULL,NULL};
CFRunLoopTimerRef timer = CFRunLoopTimerCreate(kCFAllocatorDefault,0.1,0.3,0,0,
                                        &myCFTimerCallback,&context);
 
CFRunLoopAddTimer(runLoop,timer,kCFRunLoopCommonModes);
 

3. Configuring a Port-Based Input Source(配置基于端口的輸入源,即Source1)

Cocoa和Core Foundation都提供了基于端口的對象,用于線程之間或進程之間的通信。以下部分介紹如何使用多種不同類型的端口設置端口通信。

下面的我也沒有認真去研究,只是大致看了一下。

3.1 Configuring an NSMachPort Object(配置NSMachPort對象)

要與NSMachPort對象建立本地連接,要創建端口對象并將其添加到主線程的RunLoop中。啟動子線程時,將同一對象傳遞給線程的入口點函數(entry-point function)。子線程可以使用相同的對象將消息發送回主線程。

3.1.1 Implementing the Main Thread Code(實現主線程)

以下代碼為:在主線程里,啟動子線程。

- (void)launchThread
{
    NSPort* myPort = [NSMachPort port];
    if (myPort)
    {
        // This class handles incoming port messages.
        [myPort setDelegate:self];
 
        // Install the port as an input source on the current run loop.
        [[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode];
 
        // Detach the thread. Let the worker release the port.
        [NSThread detachNewThreadSelector:@selector(LaunchThreadWithPort:)
               toTarget:[MyWorkerClass class] withObject:myPort];
    }
}

....

3.1.2 Configuring an NSMessagePort Object(配置NSMessagePort對象)
4. Configuring a Port-Based Input Source in Core Foundation(在Core Foundation中配置基于端口的輸入源)

到這里就結束了,以上的從 3. Configuring a Port-Based Input Source(配置基于端口的輸入源,即Source1)開始,都是怎么配置Source的,感興趣的自己去看吧。

如有錯誤,煩請指正,謝謝!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容