runloop 小結
OC的兩大核心runtime和runloop
runloop簡介
runloop本質上是一個do-while迴圈,當有任務處理時喚醒,沒有任務時休眠,如果沒有任務沒有觀察者的時候退出。
OSX/iOS系統中,提供了兩個這樣的物件:NSRunLoop和CFRunLoopRef.
CFRunLoopRef是CoreFoundation框架提供的純c的api,所有這些api都是執行緒安全的。
NSRunLoop是對CFRunLoopRef的OC封裝,提供了面向物件的api,這些api不是執行緒安全的。
runloop和執行緒的關係
首先,iOS提供了兩個執行緒物件pthread_t和NSThread,這兩個執行緒物件不能互相轉換,但是一一對應。比如:可以通過pthread_main_thread_np()和[NSThread mainThread]獲取主執行緒;也可以通過pthread_self()和[NSThread currentThread]獲取當前執行緒。 CFRunLoopRef是寄予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是一一對應的,儲存在一個全域性的CFMutableDictionaryRef,key為pthread,value為runloop。執行緒剛建立時沒有runloop,如果你沒有主動獲取,那它一直不會有。當你第一次獲取runloop時,建立runloop,當執行緒結束時,runloop銷燬。
主執行緒的runloop預設開啟,程式啟動時,main方法,applicationMain方法內開啟runloop。
runloop的類
在corefoundation框架中提供了五個類關於runloop:
- CFRunLoopRef
- CFRunLoopModeRef
- CFRunLoopSourceRef
- CFRunLoopTimerRef
- CFRunLoopObserverRef
它們的關係如下:

1552108293479.jpg
一個runloop包含若干個Mode,一個Mode又包含若干個Source/Timer/Observer。每次呼叫runloop的主函式時,只能指定其中一個mode,如果想切換mode,需要退出當前runloop,再重新指定一個mode進入。這樣的好處是,不同組的Source/Timer/Observer互不影響。
CFRunLoopSourceRef是事件產生的地方。Source有兩個版本,Source 0(非埠Source)和Source 1(埠Source)。
- Source 0 只包含一個回撥函式指標,它並不能主動觸發事件。使用時,需要先呼叫CFRunLoopSourceSignal(Source 0)將該source標記為待處理,然後手動呼叫CFRunLoopWakeUp()喚醒runloop,處理該事件。
- Source 1 包含一個mach port(埠)和一個回撥的函式指標,被用於通過核心和其他執行緒相互發送訊息。這種source能主動喚醒runloop。
CFRunLoopTimerRef是基於時間的觸發器。其包含一個時間長度和一個回撥的函式指標。當其加入到runloop時,runloop會註冊對應的時間點,當時間點到時,runloop會被喚醒以執行這個回撥。
CFRunLoopObserverRef是觀察者,每個Observer都包含一個回撥,當runloop的狀態發生改變時,觀察者可以通過回撥接受到這個變化。可以接受到的狀態有如下幾個:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { kCFRunLoopEntry= (1UL << 0), // 即將進入Loop kCFRunLoopBeforeTimers= (1UL << 1), // 即將處理 Timer kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Source kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進入休眠 kCFRunLoopAfterWaiting= (1UL << 6), // 剛從休眠中喚醒 kCFRunLoopExit= (1UL << 7), // 即將退出Loop };
上述的Source/Timer/Observer被統稱為一個mode item,一個item可以被加入多個mode,但一個item被重複加入同一個mode,是沒有效果的。如果一個mode中一個item都沒有,則runloop會自動退出。
runloop的mode
CFRunLoopMode和CFRunLoop的結構大致如下
struct __CFRunLoopMode { CFStringRef _name;// Mode Name, 例如 @"kCFRunLoopDefaultMode" 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 ... };
runloop的mode包含:
- NSDefaultRunLoopMode:預設的mode;
- UITrackingRunLoopMode:跟蹤使用者觸控事件的mode,如UIScrollView的上下滾動;
- NSRunLoopCommonModes:模式集合,將一組item關聯到這個模式集合上,等於將這個item關聯到這個集合下的所有模式上;
- 自定義Mode。
這裡主要解釋一下NSRunLoopCommonModes,這個模式集合。
預設NSDefaultRunLoopMode和UITrackingRunLoopMode都是包含在這個模式集合內的,當然也可以自定義一個mode,通過CFRunLoopAddCommonMode新增到這個模式集合中。
應用場景舉例:
當一個控制器裡有一個UIScrollview和一個NSTimer,UIScrollView不滾動的時候,runloop執行在NSDefaultRunLoopMode下,此時Timer會得到回撥,但當UIScrollView滑動時,會將mode切換成UITrackingRunLoopMode,此時Timer得不到回撥。一個解決辦法就是將這個NSTimer分別繫結到NSDefaultRunLoopMode和UITrackingRunLoopMode,另一個解決辦法是將這個NSTimer繫結到NSRunLoopCommonModes,兩種方法都能使NSTimer在兩個模式下都能得到回撥。
ps.讓runloop執行在NSRunLoopCommonModes模式下是沒有意思的,因為runloop一個時間只能執行在一個模式下。
埠Source通訊的步驟
demo如下:
- (void)testDemo3 { //宣告兩個埠隨便怎麼寫建立方法,返回的總是一個NSMachPort例項 NSMachPort *mainPort = [[NSMachPort alloc]init]; NSPort *threadPort = [NSMachPort port]; //設定執行緒的埠的代理回撥為自己 threadPort.delegate = self; //給主執行緒runloop加一個埠 [[NSRunLoop currentRunLoop]addPort:mainPort forMode:NSDefaultRunLoopMode]; dispatch_async(dispatch_get_global_queue(0, 0), ^{ //新增一個Port [[NSRunLoop currentRunLoop]addPort:threadPort forMode:NSDefaultRunLoopMode]; [[NSRunLoop currentRunLoop]runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; }); NSString *s1 = @"hello"; NSData *data = [s1 dataUsingEncoding:NSUTF8StringEncoding]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ NSMutableArray *array = [NSMutableArray arrayWithArray:@[mainPort,data]]; //過2秒向threadPort傳送一條訊息,第一個引數:傳送時間。msgid 訊息標識。 //components,傳送訊息附帶引數。reserved:為頭部預留的位元組數(從官方文件上看到的,猜測可能是類似請求頭的東西...) [threadPort sendBeforeDate:[NSDate date] msgid:1000 components:array from:mainPort reserved:0]; }); } //這個NSMachPort收到訊息的回撥,注意這個引數,可以先給一個id。如果用文件裡的NSPortMessage會發現無法取值 - (void)handlePortMessage:(id)message { NSLog(@"收到訊息了,執行緒為:%@",[NSThread currentThread]); //只能用KVC的方式取值 NSArray *array = [message valueForKeyPath:@"components"]; NSData *data =array[1]; NSString *s1 = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding]; NSLog(@"%@",s1); //NSMachPort *localPort = [message valueForKeyPath:@"localPort"]; //NSMachPort *remotePort = [message valueForKeyPath:@"remotePort"]; }
宣告兩個埠,sendPort,receivePort,設定receivePort的代理,分別將sendPort和receivePort繫結到兩個執行緒的自己的runloop上,然後回到傳送執行緒用接收埠傳送資料([threadPort sendBeforeDate:[NSDate date] msgid:1000 components:array from:mainPort reserved:0]; from引數標註從傳送埠發出),注意這裡傳送的資料格式為array,內容格式只能為NSPort或者NSData,在代理方法- (void)handlePortMessage:(id)message中接收資料;
RunLoop的內部實現

20170514225238312.png
內部程式碼整理,不想看可以跳過,看下方總結:
/// 用DefaultMode啟動 void CFRunLoopRun(void) { CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false); } /// 用指定的Mode啟動,允許設定RunLoop超時時間 int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) { return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled); } /// RunLoop的實現 int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) { /// 首先根據modeName找到對應mode CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false); /// 如果mode裡沒有source/timer/observer, 直接返回。 if (__CFRunLoopModeIsEmpty(currentMode)) return; /// 1. 通知 Observers: RunLoop 即將進入 loop。 __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry); /// 內部函式,進入loop __CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) { Boolean sourceHandledThisLoop = NO; int retVal = 0; do { /// 2. 通知 Observers: RunLoop 即將觸發 Timer 回撥。 __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers); /// 3. 通知 Observers: RunLoop 即將觸發 Source0 (非port) 回撥。 __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources); /// 執行被加入的block __CFRunLoopDoBlocks(runloop, currentMode); /// 4. RunLoop 觸發 Source0 (非port) 回撥。 sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle); /// 執行被加入的block __CFRunLoopDoBlocks(runloop, currentMode); /// 5. 如果有 Source1 (基於port) 處於 ready 狀態,直接處理這個 Source1 然後跳轉去處理訊息。 if (__Source0DidDispatchPortLastTime) { Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg) if (hasMsg) goto handle_msg; } /// 通知 Observers: RunLoop 的執行緒即將進入休眠(sleep)。 if (!sourceHandledThisLoop) { __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting); } /// 7. 呼叫 mach_msg 等待接受 mach_port 的訊息。執行緒將進入休眠, 直到被下面某一個事件喚醒。 /// • 一個基於 port 的Source 的事件。 /// • 一個 Timer 到時間了 /// • RunLoop 自身的超時時間到了 /// • 被其他什麼呼叫者手動喚醒 __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) { mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg } /// 8. 通知 Observers: RunLoop 的執行緒剛剛被喚醒了。 __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting); /// 收到訊息,處理訊息。 handle_msg: /// 9.1 如果一個 Timer 到時間了,觸發這個Timer的回撥。 if (msg_is_timer) { __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time()) } /// 9.2 如果有dispatch到main_queue的block,執行block。 else if (msg_is_dispatch) { __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg); } /// 9.3 如果一個 Source1 (基於port) 發出事件了,處理這個事件 else { CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort); sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg); if (sourceHandledThisLoop) { mach_msg(reply, MACH_SEND_MSG, reply); } } /// 執行加入到Loop的block __CFRunLoopDoBlocks(runloop, currentMode); if (sourceHandledThisLoop && stopAfterHandle) { /// 進入loop時引數說處理完事件就返回。 retVal = kCFRunLoopRunHandledSource; } else if (timeout) { /// 超出傳入引數標記的超時時間了 retVal = kCFRunLoopRunTimedOut; } else if (__CFRunLoopIsStopped(runloop)) { /// 被外部呼叫者強制停止了 retVal = kCFRunLoopRunStopped; } else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) { /// source/timer/observer一個都沒有了 retVal = kCFRunLoopRunFinished; } /// 如果沒超時,mode裡沒空,loop也沒被停止,那繼續loop。 } while (retVal == 0); } /// 10. 通知 Observers: RunLoop 即將退出。 __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit); }
可以看到,實際上 RunLoop 就是這樣一個函式,其內部是一個 do-while 迴圈。當你呼叫 CFRunLoopRun() 時,執行緒就會一直停留在這個迴圈裡;直到超時或被手動停止,該函式才會返回。
runloop的執行邏輯:
- 通知監聽者,即將進入runloop;
- 通知監聽者,將要處理Timer;
- 通知監聽者,將要處理Source0(埠InputSource);
- 處理Source0;
- 如果有Source1,跳到第9步;
- 通知監聽者,執行緒即將進入休眠;
- runloop進入休眠,等待喚醒;
1.source0;
2.Timer啟動;
3.外部手動喚醒 - 通知監聽者,執行緒將被喚醒;
- 處理未處理的任務;
1.如果使用者定義的定時器任務啟動,處理定時器任務並重啟runloop,進入步驟2;
2.如果輸入源啟動,傳遞相應的訊息;
3.如果runloop被顯示喚醒,且沒有超過設定的時間,重啟runloop,進入步驟2; - 通知監聽者,runloop結束。
1.runloop結束,沒有timer或者沒有source;
2.runloop被停止,使用CFRunloopStop停止Runloop;
3.runloop超時;
4.runloop處理完事件。
蘋果用runloop實現的功能
-
自動釋放池,在主程式啟動時,再即將進入runloop的時候會執行autoreleasepush(),新建一個autoreleasePoolPage,同時push一個哨兵物件到這個page中;當runloop進入休眠模式時,會執行autoreleasepop(),釋放舊池,同時autoreleasepush(),建立新池;當runloop退出時,清空自動釋放池。
-
定時器NSTimer實際上就是CFRunloopTimerRef。
Better Late Than Never!
努力是為了當機會來臨時不會錯失機會。
共勉!