Runloop 是和線程緊密相關的一個基礎組件,是不少線程有關功能的幕後功臣。儘管在日常使用中幾乎不太會直接用到,理解 Runloop 有利於咱們更加深刻地理解 iOS 的多線程模型。bash
Runloop 是什麼?Runloop 仍是比較顧名思義的一個東西,說白了就是一種循環,只不過它這種循環比較高級。通常的 while 循環會致使 CPU 進入忙等待狀態,而 Runloop 則是一種「閒」等待,這部分能夠類比 Linux 下的 epoll。當沒有事件時,Runloop 會進入休眠狀態,有事件發生時, Runloop 會去找對應的 Handler 處理事件。Runloop 可讓線程在須要作事的時候忙起來,不須要的話就讓線程休眠。 盜一張蘋果官方文檔的圖,也是幾乎每一個講 Runloop 的文章都會引用的圖,大致說明了 Runloop 的工做模式: 盜一張蘋果官方文檔的圖,也是幾乎每一個講 Runloop 的文章都會引用的圖,大致說明了 Runloop 的工做模式:多線程
圖中展示了 Runloop 在線程中的做用:從 input source 和 timer source 接受事件,而後在線程中處理事件。Runloop 和線程是綁定在一塊兒的。每一個線程(包括主線程)都有一個對應的 Runloop 對象。咱們並不能本身建立 Runloop 對象,可是能夠獲取到系統提供的 Runloop 對象。函數
主線程的 Runloop 會在應用啓動的時候完成啓動,其餘線程的 Runloop 默認並不會啓動,須要咱們手動啓動。oop
這兩個都是 Runloop 事件的來源,其中 Input Source 又能夠分爲三類spa
Timer Source 顧名思義就是指定時器事件了。線程
在 CoreFoundation 裏面關於 RunLoop 有5個類: CFRunLoopRef CFRunLoopModeRef CFRunLoopSourceRef CFRunLoopTimerRef CFRunLoopObserverRef指針
其中 CFRunLoopModeRef 類並無對外暴露,只是經過 CFRunLoopRef 的接口進行了封裝。他們的關係以下:code
一個 RunLoop 包含若干個 Mode,每一個 Mode 又包含若干個 Source/Timer/Observer。每次調用 RunLoop 的主函數時,只能指定其中一個 Mode,這個Mode被稱做 CurrentMode。若是須要切換 Mode,只能退出 Loop,再從新指定一個 Mode 進入。這樣作主要是爲了分隔開不一樣組的 Source/Timer/Observer,讓其互不影響。CFRunLoopTimerRef 是基於時間的觸發器,它和 NSTimer 是toll-free bridged 的,能夠混用。其包含一個時間長度和一個回調(函數指針)。當其加入到 RunLoop 時,RunLoop會註冊對應的時間點,當時間點到時,RunLoop會被喚醒以執行那個回調。orm
CFRunLoopSourceRef 是事件產生的地方。Source有兩個版本:Source0 和 Source1。 • Source0 只包含了一個回調(函數指針),它並不能主動觸發事件。使用時,你須要先調用 CFRunLoopSourceSignal(source),將這個 Source 標記爲待處理,而後手動調用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop,讓其處理這個事件。 • Source1 包含了一個 mach_port 和一個回調(函數指針),被用於經過內核和其餘線程相互發送消息。這種 Source 能主動喚醒 RunLoop 的線程,其原理在下面會講到。cdn
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 會直接退出,不進入循環。
App啓動後,蘋果在主線程 RunLoop 裏註冊了兩個 Observer,其回調都是 _wrapRunLoopWithAutoreleasePoolHandler()。
第一個 Observer 監視的事件是 Entry(即將進入Loop),其回調內會調用 _objc_autoreleasePoolPush() 建立自動釋放池。其 order 是-2147483647,優先級最高,保證建立釋放池發生在其餘全部回調以前。
第二個 Observer 監視了兩個事件: BeforeWaiting(準備進入休眠) 時調用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 釋放舊的池並建立新池;Exit(即將退出Loop) 時調用 _objc_autoreleasePoolPop() 來釋放自動釋放池。這個 Observer 的 order 是 2147483647,優先級最低,保證其釋放池子發生在其餘全部回調以後。
在主線程執行的代碼,一般是寫在諸如事件回調、Timer回調內的。這些回調會被 RunLoop 建立好的 AutoreleasePool 環繞着,因此不會出現內存泄漏,開發者也沒必要顯示建立 Pool 了。
當在操做 UI 時,好比改變了 Frame、更新了 UIView/CALayer 的層次時,或者手動調用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法後,這個 UIView/CALayer 就被標記爲待處理,並被提交到一個全局的容器去。
蘋果註冊了一個 Observer 監聽 BeforeWaiting(即將進入休眠) 和 Exit (即將退出Loop) 事件,回調去執行一個很長的函數: _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。這個函數裏會遍歷全部待處理的 UIView/CAlayer 以執行實際的繪製和調整,並更新 UI 界面。
NSTimer 其實就是 CFRunLoopTimerRef,他們之間是 toll-free bridged 的。一個 NSTimer 註冊到 RunLoop 後,RunLoop 會爲其重複的時間點註冊好事件。例如 10:00, 10:10, 10:20 這幾個時間點。RunLoop爲了節省資源,並不會在很是準確的時間點回調這個Timer。Timer 有個屬性叫作 Tolerance (寬容度),標示了當時間點到後,允許有多少最大偏差。
若是某個時間點被錯過了,例如執行了一個很長的任務,則那個時間點的回調也會跳過去,不會延後執行。就好比等公交,若是 10:10 時我忙着玩手機錯過了那個點的公交,那我只能等 10:20 這一趟了。
CADisplayLink 是一個和屏幕刷新率一致的定時器(但實際實現原理更復雜,和 NSTimer 並不同,其內部實際是操做了一個 Source)。若是在兩次屏幕刷新之間執行了一個長任務,那其中就會有一幀被跳過去(和 NSTimer 類似),形成界面卡頓的感受。在快速滑動TableView時,即便一幀的卡頓也會讓用戶有所察覺。Facebook 開源的 AsyncDisplayLink 就是爲了解決界面卡頓的問題,其內部也用到了 RunLoop,這個稍後我會再單獨寫一頁博客來分析。
當調用 NSObject 的 performSelecter:afterDelay: 後,實際上其內部會建立一個 Timer 並添加到當前線程的 RunLoop 中。因此若是當前線程沒有 RunLoop,則這個方法會失效。
當調用 performSelector:onThread: 時,實際上其會建立一個 Timer 加到對應的線程去,一樣的,若是對應線程沒有 RunLoop 該方法也會失效。