RunLoop相關知識

  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的挺多的,好比說定時器、手勢識別、網絡請求等等,網絡

  

1、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就不會繼續處理事情,因此阻塞在這。

3、RunLoop的運行邏輯 

  咱們在瞭解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;
}

  簡化成流程圖 則是:

  

 

4、RunLoop的應用

控制線程生命週期(線程保活),比較經典的就是AFNetworking案例;

解決NSTimer在滑動時中止工做的問題,這個是咱們平時開發中遇到過的;

滾動視圖流暢性優化

App卡頓監測

阻止App崩潰

 

 

 

 

 

 相關參考資料:

RunLoop源碼解析

RunLoop

相關文章
相關標籤/搜索