一般來講,一個線程一次只能執行一個任務,執行完任務后線程就會退出。如果我們需要線程隨時處理任務而不退出,通常的代碼邏輯是這樣的:
function loop() {
initialize();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}
這種模型通常被稱為Event Loop。Event Loop在iOS里的實現就是RunLoop。實現這種模型的關鍵點在于:如何管理事件/消息;如何讓線程在沒有消息處理時處于休眠以避免資源占用、在有消息到來時被喚醒來處理消息。
所以,RunLoop實際上就是一個對象,這個對象管理了其需要處理的事件和消息,并提供了一個入口函數來執行上面的Event Loop的邏輯。
iOS系統中提供了兩個這樣的對象:NSRunLoop和CFRunLoopRef。
- CFRunLoopRef是在CoreFoundation框架內的,它提供了純C函數的API,所有這些API都是線程安全的;
- NSRunLoop是基于CFRunLoopRef的封裝,提供了面向對象的API,但這些對象不是線程安全的。
CFRunLoopRef是開源的,在這里可以下載到整個CoreFoundation。
RunLoop與線程的關系
iOS里有兩個線程對象:NSThread和pthread_t。CFRunloop是基于pthread來管理的。
蘋果不允許直接創建RunLoop,它只提供了兩個自動獲取的函數:CFRunLoopGetMain()和CFRunLoopGetCurrent()。這兩個函數內部的邏輯大概是下面這樣的:
/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 訪問 loopsDic 時的鎖
static CFSpinLock_t loopsLock;
/// 獲取一個 pthread 對應的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
OSSpinLockLock(&loopsLock);
if (!loopsDic) {
// 第一次進入時,初始化全局Dic,并先為主線程創建一個 RunLoop。
loopsDic = CFDictionaryCreateMutable();
CFRunLoopRef mainLoop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
}
/// 直接從 Dictionary 里獲取。
CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
if (!loop) {
/// 取不到時,創建一個
loop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, thread, loop);
/// 注冊一個回調,當線程銷毀時,順便也銷毀其對應的 RunLoop。
_CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
}
OSSpinLockUnLock(&loopsLock);
return loop;
}
CFRunLoopRef CFRunLoopGetMain() {
return _CFRunLoopGet(pthread_main_thread_np());
}
CFRunLoopRef CFRunLoopGetCurrent() {
return _CFRunLoopGet(pthread_self());
}
從上面的代碼可以看出,線程和RunLoop之間是一一對應的,線程作為key、CFRunLoopRef作為value保存到了全局的字典里。線程剛創建時沒有對應的RunLoop,RunLoop的創建是發生在第一次獲取時,RunLoop的銷毀是發生在線程結束時。你只能在一個線程的內部獲取RunLoop(主線程除外)。
我們的應用程序不需要自己創建RunLoop,而是在合適的時間來啟動RunLoop??梢酝ㄟ^ [NSRunLoop currentRunLoop] 或 [NSRunLoop mainRunLoop]來獲取RunLoop。
RunLoop對外的接口
在CoreFoundation里面關于RunLoop有5各類:
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
其中CFRunLoopModeRef類并沒有對外暴露,只是通過CFRunLoopRef的接口進行了封裝。它們的關系如下:
RunLoop的Mode
CFRunLoopMode和CFRunLoop的結構大致如下:
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里有兩個預制的mode:
KCFRunLoopDefaultMode和UITrackingRunLoopMode,這兩個Mode都已經被標記為“Common”屬性。DefaultMode是App平時所處的狀態,TrackingRunLoopMode是追蹤scrollView滑動是的狀態。NSTimer初始化后默認是KCFRunLoopDefaultMode的狀態。
要想讓timer在兩個狀態中都能正常使用,一種方法是將timer分別加入兩個mode中,還有一種,就是將timer加入到頂層的RunLoop的“commonModeItems”中?!癱ommonModeItems”被更新到所有具有“Common”屬性的Mode里去。
開啟一個NSTimer實際上就是在當前的RunLoop中注冊了一個新的事件源,當scrollView滑動的過程中,當前的RunLoop會處于UITrackingRunLoopMode的模式下,在這個模式下,是不會處理NSDefaultRunLoopMode的消息。簡單地說就是NSTimer不會開啟新的進程,只是在RunLoop里注冊了一下,RunLoop每次loop時都會檢測這個timer,看是否可以觸發。當RunLoop處于A Mode中,而timer注冊在B Mode時就無法檢測到這個timer,所以需要把timer也注冊到A Mode,這樣就可以檢測到。
解決方法:
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
如果定時器所在的runloop沒有運行,或者runloop所處的mode和定時器的不一致,都會導致定時器失效。
NSRunLoopMode
typedef NSString *NSRunLoopMode;
NSRunLoopCommonMode:被添加到RunLoop里的對象使用這個mode會被那些已經描述作為“common”Modes的所有的RunLoop所監測到。
NSDefaultRunLoopMode:這個model處理輸入源除了NSConnection對象。
NSEventTrackingRunLoopMode
NSModalPanelRunLoopMode
UITrackingRunLoopMode
NSThread
NSThread * th = [[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil];
[th start];
start方法會異步地生成一個新的線程并且在線程中調用NSThread對象的main方法。
start方法只能使用一次,自此調用會引起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");
}
運行之后發現,log方法并沒有調用,原因是線程在執行完startThread方法后,便退出了。為了讓線程能夠再次使用,可以讓線程對應的runLoop運行起來。我們把上面的startThread方法修改一下:
- (void)startThread{
NSRunLoop * runloop = [NSRunLoop currentRunLoop];
[runloop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runloop run];
}
然后運行可以看到打印的log。
即使RunLoop開始運行,如果RunLoop中的modes為空,或者要執行的mode里沒有item,那么RunLoop會直接在當前loop中返回,并進入睡眠狀態。
如果把上面的[runloop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];給去掉,也是沒有log的。
當在另外一個線程執行selector的時候,這個線程必須有一個運行的runloop。
如果在主線程里使用 initWithRequest:delegate:startImmediately:創建了一個NSURLConnetion,當調用star方法時,這個NSURLConnetion會被安排到當前的runloop里,并且是默認的mode。如果此時滑動uiscrollview,由于主線程的runloop的mode被切換到UITrackingRunLoopMode,導致NSURLConnetion的代理方法無法回調。
所以需要用到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并且在適當的時候處里到來的事件。應用的每一個線程都有對應的RunLoop對象,只有除了主線程之外的線程需要啟動運行它們的RunLoop,app的框架會自動的配置和運行主線程的RunLoop。
RunLoop從兩個不同類型的源里接收事件。Input sources 異步地傳遞事件,這些事件通常來源于另外一個線程或應用。Timer Source同步地傳遞事件,通常發生在預訂的時間或重復間隔。
Input Source異步地把事件提交到相應的handles并且調用了runUntilDate:(與線程相關聯的RunLoop對象調用這個方法)來退出。Timer Source提供事件到handles但不會引起RunLoop退出。
除了處理Input Source,RunLoop也會生成關于RunLoop運行行為的通知。注冊成為RunLoop的觀察者可以收到那些通知并且使用它們在線程上做一些額外的處理。
RunLoop的Mode是一個集合,它包括了輸入源,timers,observers(會受到通知)。每次你運行了你的RunLoop,你需要為RunLoop指定一個mode。
你可以自定義一個mode,給mode的name設置一個自定義的字符串。你必須要給自定義的mode添加source,timers和observes。
mode | Name | Description |
---|---|---|
Default | NSDefaultRunLoopMode | 大部分情況下,你需要使用這個mode來開啟你的RunLoop并且配置你的輸入源 |
Common modes | NSRunLoopCommonModes | 這是一組可配置的常用模式,將輸入源于此模式相關聯還將其與組中的每種模式向關聯。默認情況下,此設置包括了默認模式和事件追蹤模式。 |
Input Source
Input Source異步地傳遞事件到線程里。Input Source通常有兩類:一種是基于Port的input source,用于檢測應用程序的Mach ports;一種是自定義的input source,用于監測自定義的事件源。這兩個源唯一的區別是如何發出信號的,基于Port的源由內核自動發出信號,自定義源必須從另一個線程手動發出信號。
當你創建一個intput source,你可以把它分配到runloop的多個mode中。如果一個intput source不在runloop當下的監測到的mode中,他所生成的任何事件都會被掛起知道runloop切換到正確的mode中。
什么時候使用runloop
應用的主線程是默認開啟了runloop。當我們使用自定義的線程時,如果想要向和線程進行更多的交互,想要線程處于活躍狀態,就要考慮使用runloop。
如果使用到了以下操作,需要使用runloop:
- 使用了port或自定義的inputsource來與其他線程通信;
- 在線程中使用了定時器;
- 在線程中使用了performSelector:
- 保持線程執行周期性的任務。
RunLoop對象
在run一個runloop之前,必須要至少添加一個inputsource,否則run后runloop會立即退出。
除了添加inputsource,也可以添加observes來監測runloop的運行狀態。你可以使用CFRunLoopObserveRef來創建對象并使用CFRunLoopAddObserve函數來添加。RunLoop Observe必須使用Core Foundation框架來創建。