多執行緒之Runloop
Runloop
實現了執行緒內部的事件迴圈。一個執行緒通常一次只能執行一個任務,任務執行完成執行緒就會退出,但是有時候,我們希望執行緒能夠一直處理事件不退出,或者說有事件就執行,沒事件就等待事件。就比如主執行緒,app能隨時接收訊息,處於接收訊息 -> 等待 -> 處理的迴圈中
Runloop與執行緒的關係
1、Runloop與執行緒一一對應
2、執行緒建立時沒有Runloop,Runloop的建立發生在第一次獲取時,類似懶載入
3、Runloop的銷燬發生線上程結束時
4、主執行緒的Runloop預設開啟
5、Runloop用於管理執行緒中要處理的事件和訊息,並有一個入口函式,線上程執行完後能一直處於接收訊息 -> 等待 -> 處理的迴圈中,直至執行緒結束
iOS中的NSRunloop和CFRunloopRef
Cocoa中的NSRunLoop類並不是執行緒安全的,我們不能在一個執行緒中去操作另外一個執行緒的run loop物件,那很可能會造成意想不到的後果。不過幸運的是CoreFundation中的不透明類CFRunLoopRef是執行緒安全的,而且兩種型別的run loop完全可以混合使用。Cocoa中的NSRunLoop類可以通過例項方法:
- (CFRunLoopRef)getCFRunLoop;
獲取對應的CFRunLoopRef類,來達到執行緒安全的目的
Runloop如何工作
Runloop的內部結構
- CoreFoundation下的Runloop
typedef CFStringRef CFRunLoopMode CF_EXTENSIBLE_STRING_ENUM;
typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoop * CFRunLoopRef;
typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoopSource * CFRunLoopSourceRef;
typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoopObserver * CFRunLoopObserverRef;
typedef struct CF_BRIDGED_MUTABLE_TYPE(NSTimer) __CFRunLoopTimer * CFRunLoopTimerRef;
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
CFRunLoopModeRef _currentMode; // Current Runloop Mode
CFMutableSetRef _modes; // Set
...
};
Runloop的Mode
我們都知道Runloop有幾個常用的模式,比如:NSDefaultRunLoopMode\NSRunLoopCommonModes\UITrackingRunLoopMode,每個Mode包含一組source\observer\timer, 又稱為Mode item,要知道不同mode的下,Runloop的工作機制是不完全相同的,所以,每個Runloop物件中會有很多mode
1、Runloop啟動時,只能選擇一個mode進行配置
2、當mode要切換,Runloop會退出,選擇新的mode重新啟動,這樣做主要是為了分隔開不同組的 Source/Timer/Observer,讓其互不影響
3、每個mode會有一個name用於區分,一組observer觀察Runloop的狀態,另外就是事件源sources和timer,封裝事件訊息
4、當mode切換時,Runloop會將commonModeItems的source\observer\timer,同步到具有common屬性的mode中CFRunLoopSourceRef 是事件產生的地方。Source有兩個版本:Source0 和 Source1。
Source0 只包含了一個回撥(函式指標),它並不能主動觸發事件。使用時,你需要先呼叫 CFRunLoopSourceSignal(source),將這個 Source 標記為待處理,然後手動呼叫 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop,讓其處理這個事件。
Source1 包含了一個 mach_port 和一個回撥(函式指標),被用於通過核心和其他執行緒相互傳送訊息。這種 Source 能主動喚醒 RunLoop 的執行緒,其原理在下面會講到。CFRunLoopTimerRef 是基於時間的觸發器,它和 NSTimer 是toll-free bridged 的,可以混用。其包含一個時間長度和一個回撥(函式指標)。當其加入到 RunLoop 時,RunLoop會註冊對應的時間點,當時間點到時,RunLoop會被喚醒以執行那個回撥。
CFRunLoopObserverRef 是觀察者,每個 Observer 都包含了一個回撥(函式指標),當 RunLoop 的狀態發生變化時,觀察者就能通過回撥接受到這個變化。可以觀測的時間點有以下幾個:
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0),
kCFRunLoopBeforeTimers = (1UL << 1),
kCFRunLoopBeforeSources = (1UL << 2),
kCFRunLoopBeforeWaiting = (1UL << 5),
kCFRunLoopAfterWaiting = (1UL << 6),
kCFRunLoopExit = (1UL << 7),
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
![1207376-89b7c20ff09ede87.png](https://i.iter01.com/images/b2f2119f93f71ea34019b6d50370d5971f3e08449ae6a175d355a280f23cc563.png)
![1207376-055d36d5f59bcac4.png](https://i.iter01.com/images/db53d9662475b1658f30c9890775e472a790269466420e49864d891fb2f9214f.png)
應用場景舉例:主執行緒的 RunLoop 裡有兩個預置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。這兩個 Mode 都已經被標記為"Common"屬性。DefaultMode 是 App 平時所處的狀態,TrackingRunLoopMode 是追蹤 ScrollView 滑動時的狀態。當你建立一個 Timer 並加到 DefaultMode 時,Timer 會得到重複回撥,但此時滑動一個TableView時,RunLoop 會將 mode 切換為 TrackingRunLoopMode,這時 Timer 就不會被回撥,並且也不會影響到滑動操作。
有時你需要一個 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 (NSDefaultRunLoopMode) 和 UITrackingRunLoopMode,你可以用這兩個 Mode Name 來操作其對應的 Mode。
同時蘋果還提供了一個操作 Common 標記的字串:kCFRunLoopCommonModes (NSRunLoopCommonModes),你可以用這個字串來操作 Common Items,或標記一個 Mode 為 "Common"。使用時注意區分這個字串和其他 mode name。
AutoreleasePool
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 的變化(建立/銷燬/狀態改變)時,這個回撥都會進行相應處理。
介面更新
當在操作 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時,即使一幀的卡頓也會讓使用者有所察覺。
PerformSelecter
當呼叫 NSObject 的 performSelecter:afterDelay: 後,實際上其內部會建立一個 Timer 並新增到當前執行緒的 RunLoop 中。所以如果當前執行緒沒有 RunLoop,則這個方法會失效。
當呼叫 performSelector:onThread: 時,實際上其會建立一個 Timer 加到對應的執行緒去,同樣的,如果對應執行緒沒有 RunLoop 該方法也會失效。
關於GCD
實際上 RunLoop 底層也會用到 GCD 的東西,比如 RunLoop 是用 dispatch_source_t 實現的 Timer。但同時 GCD 提供的某些介面也用到了 RunLoop, 例如 dispatch_async()。
當呼叫 dispatch_async(dispatch_get_main_queue(), block) 時,libDispatch 會向主執行緒的 RunLoop 傳送訊息,RunLoop會被喚醒,並從訊息中取得這個 block,並在回撥 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 裡執行這個 block。但這個邏輯僅限於 dispatch 到主執行緒,dispatch 到其他執行緒仍然是由 libDispatch 處理的。
相關文章
- Runloop 多執行緒OOP執行緒
- iOS 多執行緒:『RunLoop』詳盡總結iOS執行緒OOP
- 多執行緒系列之 執行緒安全執行緒
- iOS 多執行緒之執行緒安全iOS執行緒
- Java多執行緒之執行緒中止Java執行緒
- Android多執行緒之執行緒池Android執行緒
- 多執行緒之NSOperation執行緒
- java多執行緒之執行緒的基本使用Java執行緒
- Java多執行緒之守護執行緒實戰Java執行緒
- 併發與多執行緒之執行緒安全篇執行緒
- 多執行緒之間通訊及執行緒池執行緒
- Java多執行緒之執行緒同步【synchronized、Lock、volatitle】Java執行緒synchronized
- iOS 多執行緒之GCDiOS執行緒GC
- iOS 多執行緒之NSOperationiOS執行緒
- iOS 多執行緒之NSThreadiOS執行緒thread
- iOS 多執行緒之NSOperationQueueiOS執行緒
- Java多執行緒之FutureTaskJava執行緒
- 多執行緒之共享模型執行緒模型
- Java多執行緒之CASJava執行緒
- java多執行緒之(synchronized)Java執行緒synchronized
- IOS多執行緒之(GCD)iOS執行緒GC
- 多執行緒和多執行緒同步執行緒
- 多執行緒--執行緒管理執行緒
- 執行緒與多執行緒執行緒
- 多執行緒【執行緒池】執行緒
- iOS - 多執行緒分析之 DispatchQueue ⅠiOS執行緒
- java多執行緒之Thread類Java執行緒thread
- 多執行緒之ReentrantLock篇(五)執行緒ReentrantLock
- java多執行緒之volatile理解Java執行緒
- runloop解決Cell上主執行緒卡頓OOP執行緒
- Java多執行緒-執行緒中止Java執行緒
- 多執行緒之初識執行緒執行緒
- iOS開發面試攻略(KVO、KVC、多執行緒、鎖、runloop、計時器)iOS面試執行緒OOP
- 多執行緒------執行緒與程式/執行緒排程/建立執行緒執行緒
- 多執行緒系列(1),多執行緒基礎執行緒
- a、多執行緒執行緒
- 畫江湖之 PHP 多執行緒開發 【執行緒安全 互斥鎖】PHP執行緒
- 畫江湖之 PHP 多執行緒開發 [執行緒安全 互斥鎖]PHP執行緒
- java 多執行緒之使用 interrupt 停止執行緒的幾種方法Java執行緒