這幾天研究了一下iOS的Runloop,看了不少的文章,收獲不少,但是疑問也挺多。所以我就試著去翻譯了并分析總結了一下蘋果的Runloop文檔,注意:并不是CFRunloop的源碼,而是這篇總體概述了Runloop的文檔。
這里先給出一些有關RunLoop的官方文檔及一些好的文章的鏈接:
官方文檔:
入門可以看:
大神文章:
RunLoop怎么用:
RunLoop問題集:
提前說明,這篇文章你可以對照著英文文檔來看,目錄結構是一樣的。但并不是逐句翻譯的,會有省略,并加上了我自己的總結分析。
本文目錄
- Run Loops(RunLoop)
- Anatomy of a Run Loop(RunLoop的剖析)
- Run Loop Modes(RunLoop的Mode)
- Input Sources
- Port-Based Sources(基于端口的輸入源)
- Custom Input Sources(自定義輸入源)
- 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對象)
- Getting a Run Loop Object(獲取RunLoop對象)
- Configuring the Run Loop(配置RunLoop)
- Starting the Run Loop(啟動RunLoop)
- Exiting the Run Loop(退出RunLoop)
- Thread Safety and Run Loop Objects(線程安全和RunLoop對象)
- Configuring Run Loop Sources(配置RunLoop的Source)
- Defining a Custom Input Source(定義自定義輸入源,即Source0)
- Configuring Timer Sources(配置定時器源,即Timer)
- Configuring a Port-Based Input Source(配置基于端口的輸入源,即Source1)
Run Loops(RunLoop)
RunLoop是與線程相關的基礎架構中的一部分。RunLoop是一個事件處理環,它可以用來安排工作并協調傳入的事件。RunLoop的目的是在有任務的時候保持線程的運行,在沒有任務的時候使線程休眠。
RunLoop的使用管理不是完全自動的。你必須在合適的時機使用線程代碼啟動RunLoop,并對接收的事件進行反應處理。Cocoa(NSRunLoop)和Core Foundation(CFRunLoop)都提供了RunLoop的對象。
每一個線程都對應著一個RunLoop。在應用程序啟動的時候,主線程的RunLoop會自動啟動,而輔助線程(即子線程)是需要你手動去獲取的。
Anatomy of a Run Loop(RunLoop的剖析)
RunLoop與它的名字的意思相像,它是一個環,線程進入這個環并且可以對收到的事件進行運行和處理。
如上圖所示: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有三種:
- kCFRunLoopDefaultMode(CFRunLoop)/NSDefaultRunLoopMode(NSRunLoop)
默認模式,在RunLoop沒有指定Mode的時候,默認就跑在DefaultMode下。一般情況下App都是運行在這個mode下的
- CFStringRef)UITrackingRunLoopMode(CFRunLoop)/UITrackingRunLoopMode(NSRunLoop)
一般作用于ScrollView滾動的時候的模式,保證滑動的時候不受其他事件影響。
- 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觸發的位置(可以去看下面的那張圖):
- 即將進入RunLoop,通知observer;
- 即將處理Timer,通知observer;
- 即將處理Source(非端口的輸入源),通知observer;
- 線程即將休眠,通知observer;
- 線程剛被喚醒,但在它處理喚醒它的事件之前,通知observer;
- 線程退出了RunLoop,通知observer;
你可以使用Core Foundation添加Observer到應用程序里,需使用CFRunLoopObserverRef類型。
與Timer類似,Observer可以使用一次或重復使用。一次性Observer在觸發后將其自身從RunLoop中移除,而重復的Observer會繼續存在于RunLoop中,您可以指定Observer在創建時運行一次還是重復運行。
The Run Loop Sequence of Events(RunLoop的內部運行邏輯)
每一次運行RunLoop,線程對應的RunLoop就會處理掛起的事件,并通知觀察者。它執行的順序如下:
- 通知Observer即將進入RunLoop
- 通知Observer即將處理Timer
- 通知Observer即將處理Source0(非端口的輸入源)
- 處理Source0(非端口的輸入源)
- 如果有Source1(基于端口的輸入源)準備就緒并等待被觸發,立即處理該事件,并跳到步驟9
- 通知Observer即將休眠
- 線程休眠,直到發生以下事件之一:
- 一個事件到達Source1(基于端口的輸入源)
- 一個定時器(Timer)觸發
- RunLoop超時
- RunLoop被手動喚醒(例如添加一個Source0非端口的輸入源)
- 通知Observer線程剛剛喚醒
- 處理待處理的事件
- 如果用戶定義的Timer觸發了,則處理這個定時器事件并重新啟動RunLoop循環,跳到步驟2
- 如果輸入源觸發了,則傳遞事件
- 如果RunLoop被手動喚醒,但尚未超時,重新啟動RunLoop循環,跳到步驟2
- 通知Observer RunLoop已經退出。
可以使用RunLoop對象顯式喚醒RunLoop,其它事件也可能導致RunLoop被喚醒,例如添加一個Source0(非端口的輸入源)會喚醒RunLoop,以便立即處理輸入源,而不是等到其它事件發生。
****注意:下圖有錯誤,最左邊應該改為Source1(port),且缺少一個超時喚醒;10應該改為通知Observer,RunLoop已經退出,而不是即將退出****。
以下是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?)
你需要顯式運行RunLoop的唯一一種情況是,為應用程序創建輔助線程(這里的輔助線程就是指子線程)。因為應用程序的主線程的RunLoop會在應用創建的時候自動啟動,所以不需要你去管主線程的RunLoop。
對于輔助線程,你需要確定是否需要RunLoop,如果是,則自行去配置并啟動它。通常在所有的情況下,你都不需要去啟動一個線程的RunLoop。比如你要使用一個線程去執行一個長時間運行且預定義的任務,你可以避免去使用RunLoop。
-
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退出。
分析說明:
- 退出的三種方式
- 超時
- CFRunLoopStop函數顯式停止
- 刪除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的,感興趣的自己去看吧。
如有錯誤,煩請指正,謝謝!