RunLoop,翻譯過來是運行環路。咱們在建立命令行項目和建立ios項目時,發現命令行項目當最後一行代碼執行完後項目就自動退出了,而ios項目確能夠一直運行,知道用戶手動點擊退出按鈕。這就是由於ios項目在main函數中自動建立了runLoop,從而可使項目能夠一直響應用戶的操做。ios
int main(int argc, char * argv[]) { @autoreleasepool { //這行代碼 會自動建立主線程的RunLoop return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } }
咱們能夠將這個過程咱們能夠簡化成:數組
咱們從這個過程能夠看出RunLoop的基本做用
保持程序的持續運行
處理App中的各類事件(好比觸摸事件、定時器事件等)
節省CPU資源,提升程序性能:該作事時作事,該休息時休息
......安全
咱們平時開發中,涉及到RunLoop的挺多的,好比說定時器、手勢識別、網絡請求等等,網絡
iOS中有2套API來訪問和使用RunLoop:app
①Foundation:NSRunLoop,它是基於 CFRunLoopRef 的封裝,提供了面向對象的 API,可是這些 API 不是線程安全的。函數
②Core Foundation:CFRunLoopRef,它提供了純 C 函數的 API,全部這些 API都是線程安全的。(CFRunLoopRef是開源的)oop
二者關係:性能
因此咱們獲取RunLoop對象也有兩種方法:優化
Foundation [NSRunLoop currentRunLoop]; // 得到當前線程的RunLoop對象 [NSRunLoop mainRunLoop]; // 得到主線程的RunLoop對象 Core Foundation CFRunLoopGetCurrent(); // 得到當前線程的RunLoop對象 CFRunLoopGetMain(); // 得到主線程的RunLoop對象
由於CFRunLoopRef是開源的,因此咱們能夠經過它來看一下它的實現結構。來到CFRunLoop.c文件中,找到了RunLoop的結構體定義:spa
//已剔除非必要部分
struct __CFRunLoop { pthread_t _pthread; CFMutableSetRef _commonModes; CFMutableSetRef _commonModeItems; CFRunLoopModeRef _currentMode; CFMutableSetRef _modes; };
這裏的Set和數組相似,只不過數組是有序的,而set是無序的,都是用來存放數據的,因此 CFMutableSetRef能夠理解成可變數組,也就是說在一個RunLoop對象中,存儲着一個線程對象,三個可變數組,一個當前模式。那麼CFRunLoopModeRef又是什麼呢?
咱們找到了它的定義:
typedef struct __CFRunLoopMode *CFRunLoopModeRef; //剔除了其餘無關屬性 struct __CFRunLoopMode { CFStringRef _name; CFMutableSetRef _sources0; CFMutableSetRef _sources1; CFMutableArrayRef _observers; CFMutableArrayRef _timers; };
因此RunLoop的結構是這樣的:
_pthread就是RunLoop對應的線程,每條線程都有惟一的一個與之對應的RunLoop對象。
_commonModeItems和_commonModes是用來存放某些特定模式和模式內事件的,接下來會講到。
_currentMode,RunLoop當前所處的模式,當前模式是從_modes裏面選擇的。
_modes:RunLoop的運行模式,一共有五種,可是咱們常常用的就兩三種:
- kCFRunLoopDefaultMode, App的默認運行模式,一般主線程是在這個運行模式下運行 - UITrackingRunLoopMode, 跟蹤用戶交互事件(用於 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其餘Mode影響)頁面滾動式所處的模式 - kCFRunLoopCommonModes, 僞模式,不是一種真正的運行模式 - UIInitializationRunLoopMode:在剛啓動App時第進入的第一個Mode,啓動完成後就再也不使用 - GSEventReceiveRunLoopMode:接受系統內部事件,一般用不到
咱們上面提到的_commonModeItems和_commonModes就是存放kCFRunLoopCommonModes這種模式的數據的。CommonModes其實並非一種真正的模式,而是指能夠在標記爲Common Modes的模式下運行的僞模式。 目前被標記爲Common Modes的模式: kCFRunLoopDefaultMode,UITrackingRunLoopMode,簡單來講目前kCFRunLoopCommonModes就是指kCFRunLoopDefaultMode+UITrackingRunLoopMode。好比,咱們常常遇到在tableview添加定時器後,當tableview滾動後timer就不響應了。
這是由於tableview滾動式處在UITrackingRunLoopMode模式下的,而定時器默認是處在kCFRunLoopDefaultMode下的,因此當模式切換後,RunLoop就沒法響應以前模式的時間了,故而沒法響應定時器時間。因此咱們的方案是將定時器添加到RunLoop的kCFRunLoopCommonModes模式下,這樣不管是否滑動tableview均可以響應定時器事件了。
這裏還須要注意的一點是:若是須要切換 Mode,只能退出Loop,再從新指定一個 Mode 進入。這樣作主要是爲了分隔開不一樣組的 Source/Timer/Observer,讓其互不影響。
接下來,咱們再來看一下這個RunLoop中的模式指的是什麼?有什麼做用?
咱們前面經過源碼,看到了CFRunLoopMode的結構,裏面有sources0、sources一、timer、observers,其實這裏面就存儲着app要處理的種種事情,它們分別負責不一樣的工做。它們的分工是這樣的:(我的認爲sources0和sources1實際上是一個總體,當事件發生時sources1先去獲取這個時間,涉及不到端口或內核或其餘線程的事情的話就交給sources0處理,其他的本身處理)
sources0:只包含了一個回調(函數指針),它並不能主動觸發事件,好比點擊事件等操做都是經過sources0處理的。
sources1:包含了一個 mach_port 和一個回調(函數指針),用於經過內核和其餘線程相互發送消息,這種 Source 能主動喚醒 RunLoop 的線程。
timer:是基於時間的觸發器,其包含一個時間長度和一個回調(函數指針)。當其加入到 RunLoop 時,RunLoop會註冊對應的時間點,當時間點到時,RunLoop會被喚醒以執行那個回調。
observers:是觀察者,當 RunLoop 的狀態發生變化時,觀察者就能經過回調接受到這個變化。
RunLoop的狀態有一下幾種:
須要注意的一點是:若是Mode裏沒有任何Source0/Source1/Timer/Observer,RunLoop會立馬退出。
2、RunLoop與線程
關於RunLoop與線程的關係,咱們能夠總結如下幾點:
每條線程都有惟一的一個與之對應的RunLoop對象
線程剛建立時並無RunLoop對象,RunLoop會在第一次獲取它時建立
主線程的RunLoop已經自動獲取(建立),子線程默認沒有開啓RunLoop,子線程沒有開啓RunLoop的話就跟命令行項目同樣,任務執行完就會結束
RunLoop保存在一個全局的Dictionary裏,線程做爲key,RunLoop做爲value
RunLoop會在線程結束時銷燬
接下來,咱們經過源碼來驗證:
當咱們獲取線程的Runloop的時候,發現RunLoop沒有獲取到話,都會調用__CFRunLoopGet0, 並把線程做爲參數傳遞
繼續,跳轉至__CFRunLoopGet0,以下:
發現,RunLoop與線程的關係是一對一的,而且用了個全局字典保存了起來,線程做爲key,RunLoop做爲value。
咱們發現若是線程沒有啓用RunLoop後會執行完立刻銷燬:
添加RunLoop後,發現仍是運行完就銷燬:這是由於若是Mode裏沒有任何Source0/Source1/Timer/Observer,RunLoop會立馬退出
因此咱們須要往Model中添加一個數據:
發現確實執行完後,線程阻塞了,一直沒有被銷燬,這是由於當runtime建立後,若是沒有被事件喚醒後它就一直在休眠,cpu就不會繼續處理事情,因此阻塞在這。
咱們在瞭解RunLoop的結構以及與線程的關係後,咱們再來看一下RunLoop的運行流程:
接下來,咱們經過源碼來看一下RunLoop是如何處理這些事件的?
關於入口的查找,咱們能夠如今touchesBegan:方法中打個斷點,查看程序是怎麼執行到這的:
//RunLoop入口 SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */ //通知Observers 進入RunLoop __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry); //RunLoop的具體運行 result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode); //通知Observers 退出RunLoop __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit); return result; } //RunLoop的具體運行 static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) { int32_t retVal = 0; do { //通知Observers 即將處理Timers __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers); //通知Observers 即將處理Sources __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources); //處理Block __CFRunLoopDoBlocks(rl, rlm); //處理Sources0 if (__CFRunLoopDoSources0(rl, rlm, stopAfterHandle)) { //處理Block __CFRunLoopDoBlocks(rl, rlm); } Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR); // 若是當前是主線程的runloop,而且主線程有事情須要處理,則跳轉至handle_msg處理,即跳過休眠 這條指令網上大部分說法是指判斷Sources1中是否有事情處理,我的以爲這個說法不太對,這篇文章中有正面:資料 if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) { goto handle_msg; } //通知Observers 即將休眠 __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting); //開始休眠 __CFRunLoopSetSleeping(rl); //等待別的消息來喚醒當前線程 若是沒有消息就會一直在這休眠 阻塞在這 cpu不工做 有消息的話則喚醒執行 __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy); //結束休眠 __CFRunLoopUnsetSleeping(rl); //通知Observers 結束休眠 __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting); //handle_msg handle_msg:; if (被timer喚醒) { //處理Timers __CFRunLoopDoTimers(rl, rlm, mach_absolute_time()) } else if (被gcd喚醒) { //處理gcd __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg); } else {//被sources1喚醒 //處理Sources1 __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) } //處理Block __CFRunLoopDoBlocks(rl, rlm); //處理返回值 if (sourceHandledThisLoop && stopAfterHandle) { retVal = kCFRunLoopRunHandledSource; } else if (timeout_context->termTSR < mach_absolute_time()) { retVal = kCFRunLoopRunTimedOut; } else if (__CFRunLoopIsStopped(rl)) { __CFRunLoopUnsetStopped(rl); retVal = kCFRunLoopRunStopped; } else if (rlm->_stopped) { rlm->_stopped = false; retVal = kCFRunLoopRunStopped; } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) { retVal = kCFRunLoopRunFinished; } } while (0 == retVal); return retVal; }
簡化成流程圖 則是:
控制線程生命週期(線程保活),比較經典的就是AFNetworking案例;
解決NSTimer在滑動時中止工做的問題,這個是咱們平時開發中遇到過的;
相關參考資料: