Y神寫的是真的好。這篇文章的大部份內容來自 Y神的深刻理解 RunLoop,再結合官方文檔 和其餘一些網上的資料再加上本身的一些理解作了一些補充和概括,官方文檔也很是值得一看。
php
RunLoop 直接翻譯過來就是 運行循環。運行是什麼?運行指你的程序運行,循環?額,就是循環。因此運行循環就是指能讓你的程序循環不斷的運行的一個東西。html
RunLoop 是一個讓線程能隨時處理事件但並不退出的機制。這種模型一般被稱爲 Event Loop,實現這種模型的關鍵點在於:如何管理事件/消息,如何讓線程在沒有處理消息時休眠以免資源佔用、在有消息到來時馬上被喚醒。git
因此,RunLoop 實際上就是一個對象,這個對象管理了其須要處理的事件和消息,並提供了一個入口函數來執行上面 Event Loop 的邏輯。線程執行了這個函數後,就會一直處於函數內部 「接受消息->等待->處理」 的循環中,直到這個循環結束(好比傳入 quit
的消息),函數返回。github
至於爲何要有這個 RunLoop,說一個最直接的,iOS 程序啓動後運行在主線程上,而線程通常執行完一段任務就會結束,被 CPU 掛起,若是咱們沒有保住主線程的命,那麼咱們的 App 一打開就會關閉。因此蘋果幫咱們在主線程中默認開啓 RunLoop,讓 App 可以持續運行。swift
直接列一下二者的關係:網絡
RunLoop 與線程是一一對應的。架構
RunLoop 不容許手動建立,只能經過方法去獲取,CFRunLoopGetMain()
和 CFRunLoopGetCurrent()
。app
RunLoop 是懶加載的,若是你不去使用它,那麼它就不會被建立,主線程中的 RunLoop 是蘋果幫咱們默認開啓的。框架
RunLoop 的銷燬發生在線程結束的時候。iphone
RunLoop 與線程的對應關係保存在一個全局的 Dictionary 中
在 Core Foundation 裏面關於 RunLoop 有 5 個類:
每一個 RunLoop 中包含若干個 Mode,每一個 Mode 中包含若干個 Source/Observer/Timer。
每次調用 RunLoop 的主函數,都只能指定一種 Mode,指定的 Mode 稱爲 CurrentMode
,若是須要切換 Mode,就只能退出當前 Loop,而後從新更新指定一種 Mode 進入。這樣是爲了分割不一樣組的 Source/Observer/Timer,使其互不影響。
CFRunLoopSourceRef 是事件產生的地方。Source 有兩個版本:Source0 和 Source1。
CFRunLoopSourceSignal(source)
,將這個 Source 標記爲待處理,而後手動調用 CFRunLoopWakeUp(runloop)
來喚醒 RunLoop,讓其處理事件。CFRunLoopTimeRef 是基於時間的觸發器,它和 NSTimer 和 toll-free bridged 的,能夠混用。其包含一個時間長度和一個回調(函數指針)。當其加入到 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 Modes 的列表。
RunLoop 的大體結構:
struct __CFRunLoop {
CFMutableSetRef _commonModes; // Set
CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
CFRunLoopModeRef _currentMode; // Current Runloop Mode
CFMutableSetRef _modes; // Set
...
};
複製代碼
RunLoop Mode 的大體結構:
struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
...
};
複製代碼
你們能夠下載 Core Foundation 的源碼來查看詳細結構。
系統默認註冊了 5 個 Mode:
kCFRunLoopDefaultMode
: App 的默認 Mode,一般主線程是在這個 Mode 下面運行的UITrackingRunLoopMode
: 界面跟蹤 Mode,用於 ScrollView 追蹤觸摸滑動UIInitializationRunLoopMode
: 在剛啓動 App 時進入的第一個 Mode,啓動完後就再也不使用GSEventReceiveRunLoopMode
: 接受系統事件的內部 Mode,一般用不到kCFRunLoopCommonModes
: 這是一個佔位的 Mode,沒有實際做用能夠點擊這裏查看更多的蘋果內部的 Mode,但那些 Mode 在開發中基本不會遇到。
不一樣 Mode 之間互不干擾。
咱們經常使用的有兩種,kCFRunLoopDefaultMode
和 UITrackingRunLoopMode
,還有一個 kCFRunLoopCommonModes
,不過 kCFRunLoopCommonModes
只是一種僞模式。
關於 Common modes:一個 Mode 能夠將本身標記爲 「Common」 屬性(經過將其 ModeName 添加到 RunLoop 的 「commonModes」 中)。每當 RunLoop 的內容發生變化時,RunLoop 都會自動將 _commonModeItems
裏的 Source/Observer/Timer 同步到具備 「Common」 標記的全部 Mode 裏。
視圖滑動時定時器失效的解決方法:
主線程的 RunLoop 裏有兩個預置的 Mode:kCFRunLoopDefaultMode
和UITrackingRunLoopMode
。這兩個 Mode 都已經被標記爲 "Commoc" 屬性。Default Mode
是 App 平時所處的狀態,UITrackingRunLoopMode
是追蹤ScrollView
滑動時的狀態(UITextView
的滑動也算)。
當你建立一個 Timer 並加到DefaultMode
時,Timer 會獲得重複回調,但此時滑動一個ScrollView
時,RunLoop 會將 mode 切換爲UITrackingRunLoopMode
,這時 Timer 就不會被回調,而且也不會影響的到滑動操做,由於不一樣 Mode 之間是互不干擾的。
有時你須要一個 Timer,在兩個 Mode 中都能獲得回調,一種方法就是將這個 Timer 分別加入這兩個 Mode。還有一種方式,就是將 Timer 加入到頂層的 RunLoop 的commonModeItems
中,commonModeItems
被 RunLoop 自動更新到全部具備 "Common" 屬性的 Mode 裏去。
CFRunLoop 對外暴露的管理 Mode 的接口只有下面 2 個:
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);
複製代碼
Mode 暴露的管理 mode item 的接口有下面幾個:
CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer,CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
複製代碼
你只能經過 mode name 來操做內部的 mode,當你傳入一個新的 mode name 但 RunLoop 內部沒有對應 mode 時,RunLoop 會自動幫你建立對應的 CFRunLoopModeRef
。對於一個 RunLoop 來講,其內部的 mode 只能增長不能刪除。
蘋果公開提供的 Mode 有兩個:kCFRunLoopDefaultMode(NSDefalutRunLoopMode)
和 UITrackingRunLoopMode
,你能夠用這兩個 Mode Name 來操做其對應的 Mode。
同時蘋果還提供了一個操做 Common 標記的字符串:kCFRunLoopCommonModes(NSRunLoopCommonModes)
,你能夠用這個字符串來操做 Common Items,或標記一個 Mode 爲 「Common」。使用時注意區分這個字符串和其餘 mode name。
因此 RunLoop Mode 是能夠自定義建立的。
輸入源以異步的方式向線程傳遞事件。事件的來源取決於輸入源的類型,一般是兩個類別之一:
基於端口的輸入源監聽應用程序的 Mach 端口,自定義輸入源監聽自定義事件源。就 RunLoop 而言,輸入源是基於端口的仍是自定義的是可有可無的,兩個來源之間的惟一區別就是它們如何發出信號。基於端口的源由內核自動發出信號,可是自定義的源必須從另外一個線程手動發送信號。
當建立輸入源(input sources)時,會將其分配給 RunLoop 的一個或多個 Mode。不一樣的 Mode 會影響對這些輸入源的監聽。大部分狀況下你在默認模式(kCFRunLoopDefaultMode
)下運行 RunLoop,但你也能夠指定自定義的 Mode。若是輸入源未處於當前監聽的 Mode,則在它生成的任何事件,都將被保留,直到 RunLoop 被指定到正確的 Mode 運行。
Cocoa 和 Core Foundation 提供內置支持,使用與端口相關的對象和函數建立基於端口的輸入源。例如,在 Cocoa 中,你根本沒必要直接建立輸入源。你只須要建立一個端口對象並使用 NSPort
提供的方法將該端口添加到 RunLoop 中,port 對象會自動爲你建立和配置所需的輸入源。
在 Core Foundation 中,你必須手動建立端口和輸入源。也就是使用 CFMachPortRef
、CFMessagePortRef
、CFSocketRef
去建立合適的對象。
自定義輸入源的建立和使用的例子你們能夠去查一下官方文檔。
除了基於端口的源以外,Cocoa 還定義了一個自定義 input source,容許你在任何線程上執行選擇器。與基於端口的 source 相似,perform selector
請求在目標線程上被序列化,從而減小在一個線程上運行多個方法時可能發生的許多同步問題。與基於端口的 source 不一樣,perform selector
源在執行其 selector
後將其自身從 RunLoop 中移除。
在另外一個線程執行選擇器時,目標線程必須開啓了 RunLoop。對於你本身建立的線程,意味着要顯式啓動 RunLoop。因爲主線程中默認啓動了 RunLoop,因此只要應用程序調用 applicationDidFinishLaunching:
,就能夠開始在主線程上發出調用。RunLoop 每次經過循環處理全部排隊的 perform selector
調用,而不是在每次循環迭代期間處理一個。
關於在其餘線程上執行選擇器的方法,能夠查看官方文檔的表3.2。
其實就是 NSTimer,計時器,一個 NSTimer 註冊到 RunLoop 以後,RunLoop 會爲其重複的時間點註冊好事件。不過 RunLoop 爲了節省資源,並不會在很是準確的時間點回調這個 Timer,由於 RunLoop 內部是有一個處理邏輯的,這個咱們放到下面再講。
定時器是線程通知本身作事情的一種方式。例如,有一個搜索的功能,咱們可使用計時器,設置一個時間,讓用戶在開始輸入以後通過這個時間後開始搜索,這樣咱們就可使用戶在開始搜索以前輸入儘量的搜索字符串。
雖然咱們設置的時間到了計時器就會發出通知讓 RunLoop 去作事情,但它並不會真正的實時去處理。與輸入源相似,計時器與 RunLoop 的 Mode 相關聯。好比你添加定時器到 kCFRunLoopDefaultMode
中,若是此時你的 Mode 是 UITrackingRunLoopMode
,那麼這個計時器是不會被觸發的。除非你切換 Mode 爲 kCFRunLoopDefaultMode
。若是 Timer 設置的時間到了,該執行 Timer 對應的事件了,可是此時 RunLoop 還在忙着處理其餘的事情,那麼 Timer 會等到 RunLoop 執行完其餘事情再執行。若是線程中的 RunLoop 根本沒有被啓動,那麼 Timer 永遠不會被觸發。
你能夠設置計時器只觸發一次或者重複觸發,當你設置爲重複觸發的時候,計時器會按照你原來設置的間隔去不斷的觸發事件,而不是按照實際觸發事件的間隔。好比說你在 11:00
的時候,設置了每隔 10 分鐘,觸發一次計時器事件,也就是 11:10
、11:20
、11:30
...若是因爲某些緣由,原本應該在 11:10
分觸發的事件,被推遲到了 11:15
才觸發,這時雖然你設置的時間間隔是 10 分鐘,好像是應該 11:25
才觸發下一次事件,可是其實不是的,仍是會在 11:20
分觸發下一次時間,而後在 11:30
分觸發下下次事件。因此計時器是按照你最開始計劃的時間來發出通知的。
RunLoop 在內部會處理 Source 事件、Timer 觸發的事件、會休眠、會退出等,在這些特定的時期,系統會經過 Observer 來通知開發者,Observer 關聯了 RunLoop 的下列時刻:
與 Timer 相似,Observer 能夠一次或重複使用。一次性 Observer 在觸發後將其自身從 RunLoop 中移除,你能夠在建立 Observer 時指定是運行一次仍是重複運行。
下面是官方文檔提到的內部邏輯:
由於 Timer 和 Source 的 Observer 通知是在這些事件實際發生以前傳遞的,所以通知事件與實際事件的時間可能存在差距。若是這些事件之間的時間關係很重要,你可使用休眠和喚醒休眠通知來幫助你關聯實際事件之間的時間。
實際上 RunLoop 其內部就是一個 do-while 循環。當你調用 CFRunLoopRun()
時,線程就會一直停留在這個循環裏,直到超時或被手動中止,該函數纔會返回。
RunLoop 的核心是基於 mach port 的,其進入休眠時調用的函數是 mach_msg()
。
Mach 自己提供的 API 很是有限,並且蘋果也不鼓勵使用 Mach 的 API,可是這些 API 很是基礎,若是沒有這些 API 的話,其餘任何工做都沒法實施。在 Mach 中,全部的東西都是經過本身的對象實現的,進程、線程和虛擬內存都被稱爲 「對象」。和其餘架構不一樣,Mach 的對象間不能直接調用,只能經過消息傳遞的方式實現對象間的通訊。「消息」 是 Mach 中最基礎的概念,消息在兩個端口(port)之間傳遞,這就是 Mach 的 IPC(進程間通訊)的核心。
爲了實現消息的發送和接受,mach_msg()
函數其實是調用了一個 Mach 陷阱(trap),即函數 mach_msg_trap()
,陷阱這個概念在 Mach 中等同於系統調用。當你在用戶態調用 mach_msg_trap()
時會觸發陷阱機制,切換到內核態。內核態中內核實現的 mach_msg()
函數會完成實際的工做。
也就是你在用戶態調用了 mach_msg()
函數,會觸發 mach trap,進入由系統調用的 mach_msg()
函數中去執行實際的內容。關於用戶態和內核態的概念,不知道的朋友能夠百度一下。
RunLoop 的核心就是一個 mach_msg()
,RunLoop 調用這個函數去接收消息,若是沒有別人發送 port 消息過來,內核會將線程置於等待狀態。例如你在模擬器跑起一個 iOS 的 App,而後在 App 靜止時點擊暫停,你會看到主線程調用棧停留在 mach_msg_trap()
這個地方。
按照官方文檔的說法,你惟一須要使用到 RunLoop 的時候是爲你的應用程序建立輔助線程(create secondary threads)。
App 的主線程的 RunLoop 是一個相當重要的基礎架構,所以,App 框架默認在運行時啓動主線程並開啓主線程中的 RunLoop。若是是用 Xcode 的模板項目來建立應用程序,那麼這些系統都已經幫你作好了,不須要顯式調用。
對於輔助線程(secondary threads),要肯定實際狀況看是否須要開啓 RunLoop,若是須要的話,就自行配置並啓動它。在任何狀況下,都不該該爲一個線程開啓 RunLoop。例如,若是使用線程執行某些長時間運行且自定義的任務,則能夠避免啓動 RunLoop。(我以爲這個說的我有點雲裏霧裏,我貼一下原文)
You do not need to start a thread’s run loop in all cases. For example, if you use a thread to perform some long-running and predetermined task, you can probably avoid starting the run loop. Run loops are intended for situations where you want more interactivity with the thread.
在如下的幾種狀況,須要啓動 RunLoop:
performSelector
調用其餘線程方法的時候在開發中我還沒直接用到 RunLoop 去作過什麼東西,能夠作一個常駐線程,可是常駐線程這種東西是有問題的,雖然 AFN2.0 曾經用過,可是那是由於當時蘋果的網路請求框架有缺陷。另一個用到 runloop 的地方可能就是作自動輪播那裏,用到了 common mode,其餘的方面就不多使用了。因此仍是具體來看下蘋果對 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 了。
簡單列舉一下步驟:
蘋果註冊了一個 Source1(基於 mach port)用來接收系統事件,其回調函數爲 __IOHIDEventSystemClientQueueCallback()
。
當一個硬件事件(觸摸/鎖屏/搖晃等)發生後,首先由 IOKit.framework
生成一個 IOHIDEvent
事件並由 SpringBoard 接收。這個過程的詳細狀況能夠參考這裏。SpringBoard 只接收按鍵(鎖屏/靜音等)、觸摸、加速,接近傳感器等幾種 Event,隨後用 mach port 轉發給須要的 App 進程。隨後蘋果註冊的那個 Source1 就會觸發回調,並調用 _UIApplicationHandleEventQueue()
進行應用內部的分發。
_UIApplicationHandleEventQueue()
會把 IOHIDEvent
處理幷包裝成 UIEvent
進行處理或分發,其中包括 UIGesture/處理屏幕旋轉/發送給UIWindow 等。一般事件好比 UIButton 的點擊、touchesBegin/Move/End/Cancel 事件都是在這個回調中完成的。
步驟:
當上面的 _UIApplicationHandleEventQueue()
識別了一個手勢時,其首先會調用 Cancel 將當前的 touchesBegin/Move/End 系列回調打斷。隨後系統將對應的 UIGestureRecognizer
標記爲待處理。
蘋果註冊了一個 Observer 監測 BeforeWaiting(Loop即將進入休眠)
事件,這個 Observer 的回調函數是 _UIGestureRecognizerUpdateObserver()
,其內部會獲取全部剛被標記爲待處理的 GestureRecognizer
,並執行 GestureRecognizer
的回調。
當有 UIGestureRecognizer
的變化(建立/銷燬/狀態改變)時,這個回調都會進行相應處理。
步驟:
UIGestureRecognizer
標記爲待處理BeforeWaiting
時,在其函數回調內部會獲取全部剛被標記爲待處理的 GestureRecognizer
,並執行 GestureRecognizer
的回調當在操做 UI 時,好比改變了 frame、更新了 UIView/CALayer 的層次時,或者手動調用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay
方法後,這個 UIView/CALayer 就被標記爲待處理,並被提交到一個全局的容器去。
蘋果註冊了一個 Observer 監聽 BeforeWaiting(即將進入休眠)
和 Exit(即將退出Loop)
事件,回調去執行一個很長的函數:_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
。這個函數裏會遍歷全部待處理的 UIView/CALayer
以執行實際的繪製和調整,並更新 UI 界面。
步驟:
BeforeWaiting
和 Exit
時遍歷全部待處理的 UI 以執行實際的繪製和調整,並更新 UI 界面NSTimer
其實就是 CFRunLoopTimeRef
,他們之間是 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 開源的 AsyncDisplayKit 就是爲了解決界面卡頓的問題,其內部也用到了 RunLoop(模仿了 iOS 界面更新的過程)。
當調用 NSObject 的 performSelector:afterDelay:
後,實際上其內部會建立一個 Timer 並添加到當前線程的 RunLoop 中。因此若是當前線程沒有 RunLoop,則這個方法會失效。
當調用 dispatch_async(dispatch_get_main_queue(), block)
時,libDispatch
會向主線程的 RunLoop 發送消息,RunLoop 會被喚醒,並從消息中取得這個 block,並在回調 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
裏執行這個 block。但這個邏輯僅限於 dispatch 到主線程,dispatch 到其餘線程仍然是由 libDispatch
處理的。
iOS 關於網絡請求的接口自下至上的層次:
CFSocket
CFNetwork ->ASIHttpRequest
NSURLConnection ->AFNetworking
NSURLSession ->AFNetworking2, Alamofire
複製代碼
CFSocket
是最底層的接口,只負責 socket 通訊CFNetwork
是基於 CFSocket
等接口的上層封裝NSURLConnection
是基於 CFNetwork
的更高層的封裝,提供面向對象的接口NSURLSession
是 iOS7 中新增的接口,表面上和 NSURLConnection
並列的,但底層仍然用到了 NSURLConnection
的部分功能(好比 com.apple.NSURLConnectionLoader
線程)下面主要介紹下 NSURLConnection
的工做過程。
一般使用 NSURLConnection
時,你會傳入一個 Delegate
,當調用了 [connection start]
後,這個 Delegate
就會不停收到事件回調。實際上,strat
這個函數的內部會獲取 CurrentRunLoop,而後在其中的 DefaultMode
添加 4 個 Source0
(即須要手動觸發的 Source)。CFMultiplexerSource
是負責各類 Delegate
回調的,CFHTTPCookieStorage
是處理各類 Cookie 的。
當開始網絡傳輸時,咱們能夠看到 NSURLConnection
建立了兩個新線程:com.apple.NSURLConnectionLoader
和 com.apple.CFSocket.private
。其中 CFSocket
線程是處理底層 socket 鏈接的。NSURLConnectionLoader
這個線程內部會使用 RunLoop 來接受底層 socket 事件,並經過以前添加的 Source0 通知到上層的 delegate。
NSURLConnectionLoader
中的 RunLoop 經過一些基於 mach port 的 Source 接收來自底層 CFSocket
的通知。當收到通知後,其會在合適的時機向 CFMultiplexerSource
等 Source0 發送通知,同時喚醒 Delegate
線程的 RunLoop 對 Delegate
執行實際的回調。
步驟:
NSURLConnection
建立兩個新線程,com.apple.CFSocket.private
處理 socket 鏈接,com.aoole.NSURLConnectionLoader
內部使用 RunLoop 來接受底層 socket 事件NSURLConnectionLoader
經過 Source1 接收到這個通知NSURLConnectionLoader
在合適的時機向 CFMultiplexerSource
、CFHTTPCookieStorage
等 Source0 發送通知,同時喚醒 Delegate
線程的 RunLoop 讓其來處理這些通知CFMultiplexerSource
會在 Delegate
線程的 RunLoop 對 Delegate
執行實際的回調RunLoop 是一個讓線程能隨時處理事件但並不退出的機制。這種模型一般被稱爲 Event Loop,實現這種模型的關鍵點在於:如何管理事件/消息,如何讓線程在沒有處理消息時休眠以免資源佔用、在有消息到來時馬上被喚醒。
因此,RunLoop 實際上就是一個對象,這個對象管理了其須要處理的事件和消息,並提供了一個入口函數來執行上面 Event Loop 的邏輯。線程執行了這個函數後,就會一直處於函數內部 「接受消息->等待->處理」 的循環中,直到這個循環結束(好比傳入 quit
的消息),函數返回。
說 RunLoop 就繞不開線程,RunLoop 與線程的關係:
Dictionary
中一個 RunLoop 包含若干個 Mode,每一個 Mode 包含若干個 Source/Timer/Observer,Source/Timer/Observer 被統稱爲 Mode Item。不一樣 Mode 之間互不干擾。
在 Core Foundation 裏面關於 RunLoop 的 5 個類:
Mode
系統默認註冊了 5 個 Mode:
kCFRunLoopDefaultMode
:App 的默認 ModeUITrackingRunLoopMode
:界面跟蹤 Mode,用於 ScrollView
追蹤觸摸滑動UIInitializationRunLoopMode
:在剛啓動 App 時進入的第一個 ModeGSEventReceiveRunLoopMode
:接受系統事件的內部 Mode,一般用不到kCFRunLoopCommonModes
:這是一個佔位的 Mode,沒有實際做用Source
主要分兩種類型:
具體的類型:
Timer
基於時間的觸發器,提早在 RunLoop 中註冊好事件,時間點到達時,RunLoop 將被喚醒以執行事件。受限於 RunLoop 的內部邏輯,計時器並不十分準確。
Observer
觀察者,每一個 Observer 都包含了一個回調(函數指針)。
可觀測的時間點: