一般來講,一個線程一次只能執(zhí)行一個任務(wù),執(zhí)行完任務(wù)后線程就會退出。如果我們需要線程隨時處理任務(wù)而不退出,通常的代碼邏輯是這樣的:
function loop() {
initialize();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}
這種模型通常被稱為Event Loop。Event Loop在iOS里的實現(xiàn)就是RunLoop。實現(xiàn)這種模型的關(guān)鍵點在于:如何管理事件/消息;如何讓線程在沒有消息處理時處于休眠以避免資源占用、在有消息到來時被喚醒來處理消息。
所以,RunLoop實際上就是一個對象,這個對象管理了其需要處理的事件和消息,并提供了一個入口函數(shù)來執(zhí)行上面的Event Loop的邏輯。
iOS系統(tǒng)中提供了兩個這樣的對象:NSRunLoop和CFRunLoopRef。
- CFRunLoopRef是在CoreFoundation框架內(nèi)的,它提供了純C函數(shù)的API,所有這些API都是線程安全的;
- NSRunLoop是基于CFRunLoopRef的封裝,提供了面向?qū)ο蟮腁PI,但這些對象不是線程安全的。
CFRunLoopRef是開源的,在這里可以下載到整個CoreFoundation。
RunLoop與線程的關(guān)系
iOS里有兩個線程對象:NSThread和pthread_t。CFRunloop是基于pthread來管理的。
蘋果不允許直接創(chuàng)建RunLoop,它只提供了兩個自動獲取的函數(shù):CFRunLoopGetMain()和CFRunLoopGetCurrent()。這兩個函數(shù)內(nèi)部的邏輯大概是下面這樣的:
/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 訪問 loopsDic 時的鎖
static CFSpinLock_t loopsLock;
/// 獲取一個 pthread 對應(yīng)的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
OSSpinLockLock(&loopsLock);
if (!loopsDic) {
// 第一次進(jìn)入時,初始化全局Dic,并先為主線程創(chuàng)建一個 RunLoop。
loopsDic = CFDictionaryCreateMutable();
CFRunLoopRef mainLoop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
}
/// 直接從 Dictionary 里獲取。
CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
if (!loop) {
/// 取不到時,創(chuàng)建一個
loop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, thread, loop);
/// 注冊一個回調(diào),當(dāng)線程銷毀時,順便也銷毀其對應(yīng)的 RunLoop。
_CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
}
OSSpinLockUnLock(&loopsLock);
return loop;
}
CFRunLoopRef CFRunLoopGetMain() {
return _CFRunLoopGet(pthread_main_thread_np());
}
CFRunLoopRef CFRunLoopGetCurrent() {
return _CFRunLoopGet(pthread_self());
}
從上面的代碼可以看出,線程和RunLoop之間是一一對應(yīng)的,線程作為key、CFRunLoopRef作為value保存到了全局的字典里。線程剛創(chuàng)建時沒有對應(yīng)的RunLoop,RunLoop的創(chuàng)建是發(fā)生在第一次獲取時,RunLoop的銷毀是發(fā)生在線程結(jié)束時。你只能在一個線程的內(nèi)部獲取RunLoop(主線程除外)。
我們的應(yīng)用程序不需要自己創(chuàng)建RunLoop,而是在合適的時間來啟動RunLoop。可以通過 [NSRunLoop currentRunLoop] 或 [NSRunLoop mainRunLoop]來獲取RunLoop。
RunLoop對外的接口
在CoreFoundation里面關(guān)于RunLoop有5各類:
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
其中CFRunLoopModeRef類并沒有對外暴露,只是通過CFRunLoopRef的接口進(jìn)行了封裝。它們的關(guān)系如下:

RunLoop的Mode
CFRunLoopMode和CFRunLoop的結(jié)構(gòu)大致如下:
struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode",@"kCFRunLoopCommonModes"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
...
};
struct __CFRunLoop {
CFMutableSetRef _commonModes; // Set
CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
CFRunLoopModeRef _currentMode; // Current Runloop Mode
CFMutableSetRef _modes; // Set
...
};
滑動ScrollView時NSTimer失效的問題
主線程的RunLoop里有兩個預(yù)制的mode:
KCFRunLoopDefaultMode和UITrackingRunLoopMode,這兩個Mode都已經(jīng)被標(biāo)記為“Common”屬性。DefaultMode是App平時所處的狀態(tài),TrackingRunLoopMode是追蹤scrollView滑動是的狀態(tài)。NSTimer初始化后默認(rèn)是KCFRunLoopDefaultMode的狀態(tài)。
要想讓timer在兩個狀態(tài)中都能正常使用,一種方法是將timer分別加入兩個mode中,還有一種,就是將timer加入到頂層的RunLoop的“commonModeItems”中。“commonModeItems”被更新到所有具有“Common”屬性的Mode里去。
開啟一個NSTimer實際上就是在當(dāng)前的RunLoop中注冊了一個新的事件源,當(dāng)scrollView滑動的過程中,當(dāng)前的RunLoop會處于UITrackingRunLoopMode的模式下,在這個模式下,是不會處理NSDefaultRunLoopMode的消息。簡單地說就是NSTimer不會開啟新的進(jìn)程,只是在RunLoop里注冊了一下,RunLoop每次loop時都會檢測這個timer,看是否可以觸發(fā)。當(dāng)RunLoop處于A Mode中,而timer注冊在B Mode時就無法檢測到這個timer,所以需要把timer也注冊到A Mode,這樣就可以檢測到。
解決方法:
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
如果定時器所在的runloop沒有運行,或者runloop所處的mode和定時器的不一致,都會導(dǎo)致定時器失效。
NSRunLoopMode
typedef NSString *NSRunLoopMode;
NSRunLoopCommonMode:被添加到RunLoop里的對象使用這個mode會被那些已經(jīng)描述作為“common”Modes的所有的RunLoop所監(jiān)測到。
NSDefaultRunLoopMode:這個model處理輸入源除了NSConnection對象。
NSEventTrackingRunLoopMode
NSModalPanelRunLoopMode
UITrackingRunLoopMode
NSThread
NSThread * th = [[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil];
[th start];
start方法會異步地生成一個新的線程并且在線程中調(diào)用NSThread對象的main方法。
start方法只能使用一次,自此調(diào)用會引起crash

如果想多次使用這個線程怎么處理呢:
- (void)viewDidLoad {
[super viewDidLoad];
_thread = [[NSThread alloc] initWithTarget:self selector:@selector(startThread) object:nil];
[_thread start];
[self useThread];
[self useThread];
}
- (void)startThread{
NSLog(@"start");
}
- (void)useThread{
[self performSelector:@selector(log) onThread:_thread withObject:nil waitUntilDone:NO];
}jiu
- (void)log{
NSLog(@"log");
}
運行之后發(fā)現(xiàn),log方法并沒有調(diào)用,原因是線程在執(zhí)行完startThread方法后,便退出了。為了讓線程能夠再次使用,可以讓線程對應(yīng)的runLoop運行起來。我們把上面的startThread方法修改一下:
- (void)startThread{
NSRunLoop * runloop = [NSRunLoop currentRunLoop];
[runloop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runloop run];
}
然后運行可以看到打印的log。
即使RunLoop開始運行,如果RunLoop中的modes為空,或者要執(zhí)行的mode里沒有item,那么RunLoop會直接在當(dāng)前l(fā)oop中返回,并進(jìn)入睡眠狀態(tài)。
如果把上面的[runloop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];給去掉,也是沒有l(wèi)og的。
當(dāng)在另外一個線程執(zhí)行selector的時候,這個線程必須有一個運行的runloop。
如果在主線程里使用 initWithRequest:delegate:startImmediately:創(chuàng)建了一個NSURLConnetion,當(dāng)調(diào)用star方法時,這個NSURLConnetion會被安排到當(dāng)前的runloop里,并且是默認(rèn)的mode。如果此時滑動uiscrollview,由于主線程的runloop的mode被切換到UITrackingRunLoopMode,導(dǎo)致NSURLConnetion的代理方法無法回調(diào)。
所以需要用到scheduleInRunLoop: scheduleInRunLoop
NSURL * url = [NSURL URLWithString:@"https://www.baidu.com"];
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:15];
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
[connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
[connection start];
RunLoop的管理不是全自動的,你仍然需要開啟運行runLoop并且在適當(dāng)?shù)臅r候處里到來的事件。應(yīng)用的每一個線程都有對應(yīng)的RunLoop對象,只有除了主線程之外的線程需要啟動運行它們的RunLoop,app的框架會自動的配置和運行主線程的RunLoop。
RunLoop從兩個不同類型的源里接收事件。Input sources 異步地傳遞事件,這些事件通常來源于另外一個線程或應(yīng)用。Timer Source同步地傳遞事件,通常發(fā)生在預(yù)訂的時間或重復(fù)間隔。
Input Source異步地把事件提交到相應(yīng)的handles并且調(diào)用了runUntilDate:(與線程相關(guān)聯(lián)的RunLoop對象調(diào)用這個方法)來退出。Timer Source提供事件到handles但不會引起RunLoop退出。
除了處理Input Source,RunLoop也會生成關(guān)于RunLoop運行行為的通知。注冊成為RunLoop的觀察者可以收到那些通知并且使用它們在線程上做一些額外的處理。
RunLoop的Mode是一個集合,它包括了輸入源,timers,observers(會受到通知)。每次你運行了你的RunLoop,你需要為RunLoop指定一個mode。
你可以自定義一個mode,給mode的name設(shè)置一個自定義的字符串。你必須要給自定義的mode添加source,timers和observes。
| mode | Name | Description |
|---|---|---|
| Default | NSDefaultRunLoopMode | 大部分情況下,你需要使用這個mode來開啟你的RunLoop并且配置你的輸入源 |
| Common modes | NSRunLoopCommonModes | 這是一組可配置的常用模式,將輸入源于此模式相關(guān)聯(lián)還將其與組中的每種模式向關(guān)聯(lián)。默認(rèn)情況下,此設(shè)置包括了默認(rèn)模式和事件追蹤模式。 |
Input Source
Input Source異步地傳遞事件到線程里。Input Source通常有兩類:一種是基于Port的input source,用于檢測應(yīng)用程序的Mach ports;一種是自定義的input source,用于監(jiān)測自定義的事件源。這兩個源唯一的區(qū)別是如何發(fā)出信號的,基于Port的源由內(nèi)核自動發(fā)出信號,自定義源必須從另一個線程手動發(fā)出信號。
當(dāng)你創(chuàng)建一個intput source,你可以把它分配到runloop的多個mode中。如果一個intput source不在runloop當(dāng)下的監(jiān)測到的mode中,他所生成的任何事件都會被掛起知道runloop切換到正確的mode中。
什么時候使用runloop
應(yīng)用的主線程是默認(rèn)開啟了runloop。當(dāng)我們使用自定義的線程時,如果想要向和線程進(jìn)行更多的交互,想要線程處于活躍狀態(tài),就要考慮使用runloop。
如果使用到了以下操作,需要使用runloop:
- 使用了port或自定義的inputsource來與其他線程通信;
- 在線程中使用了定時器;
- 在線程中使用了performSelector:
- 保持線程執(zhí)行周期性的任務(wù)。
RunLoop對象
在run一個runloop之前,必須要至少添加一個inputsource,否則run后runloop會立即退出。
除了添加inputsource,也可以添加observes來監(jiān)測runloop的運行狀態(tài)。你可以使用CFRunLoopObserveRef來創(chuàng)建對象并使用CFRunLoopAddObserve函數(shù)來添加。RunLoop Observe必須使用Core Foundation框架來創(chuàng)建。