OSX / iOS 系統中,有2套API來訪問和使用 RunLoop
。html
CFRunLoopRef
是在 CoreFoundation
框架內的,它提供了純 C 函數的 API,全部這些 API 都是線程安全的。NSRunLoop
是基於 CFRunLoopRef
的封裝,提供了面向對象的 API,可是這些 API 不是線程安全的。因此要了解 RunLoop
內部結構,須要多研究 CFRunLoopRef
層面的API(Core Foundation
層面)NSRunLoop
和 CFRunLoopRef
都表明着 RunLoop
對象。git
main
函數中的 RunLoop
(主運行循環):第14行代碼的 UIApplicationMain
函數內部就啓動了一個 RunLoop
。因此 UIApplicationMain
函數一直沒有返回,保持了程序的持續運行。這個默認啓動的 RunLoop
是跟主線程相關聯的。 程序員
一種 Runloop 運行模式就是一個要監控的 Input 和 Timer 事件源的集合或者是一個要通知的 Runloop 觀察者的集合。每次運 行Runloop,都要指定一個運行模式(顯示地或者隱式地)。在 Runloop 的運行期間,只有和當前運行模式相關的源才能被監控和容許發送事件。類似的,只有和當前運行模式相關的觀察者纔會被通知 Runloop 的行爲。和其餘模式相關的源會保留新的事件直到 Runloop 運行在了合適的模式纔會分發。github
在咱們的代碼中,咱們能夠經過字符串來標識模式。Cocoa和Core Foundation定義了一個默認模式和幾個普通的有用的模式,這些模式都是用字符串來標識的。咱們能夠用一個字符串當作名字來自定義一個模式,雖然咱們自定義模式的名字是隨意的,可是模式的內容不是隨意的,在咱們本身建立的要用的模式中至少要添加一個 Input 源、 Timer 源或者 Runloop 觀察者。面試
在 Runloop 的特殊階段咱們但是使用運行模式來過濾咱們不想要的源的事件,大多數的狀況下,Runloop 都運行在系統提供的默認模式下,然而 Model Panel 可能運行在「模式」模式,當運行在這個模式期間,只有和這個模式相關的事件源纔會發送事件到咱們的線程。對於第二線程來講,咱們一般使用自定義模式來阻止低優先級的事件源在其餘關鍵處理的時間內發送事件。安全
注意:運行模式不是根據事件類型劃分的,而是根據事件源劃分的。咱們不能經過模式來匹配鼠標按下事件或者鍵盤事件,可是咱們能夠用運行模式來監聽一組不一樣的Port、暫時掛起Timers或者改變當前被監控的事件源和Runloop觀察者。bash
下面列舉了一些Cocoa和Core Foundation定義的標準模式:網絡
NSDefaultRunLoopMode
:默認的運行模式,用於大部分操做,除了NSConnection對象事件。NSConnectionReplyMode
:用來監控NSConnection對象的回覆的,不多可以用到。NSModalPanelRunLoopMode
:用於標明和Mode Panel相關的事件。NSEventTrackingRunLoopMode
:用於跟蹤觸摸事件觸發的模式(例如UIScrollView上下滾動)。NSRunLoopCommonModes
:是一個模式集合,當綁定一個事件源到這個模式集合的時候就至關於綁定到了集合內的每個模式。Cocoa 應用默認包含 Default、Panel、Event Tracking 模式,Core Foundation 只包含 Default 模式,咱們能夠經過 CFRunLoopAddCommonMode
添加模式。Runloop接收來自兩種源的事件:app
這兩種事件源都是使用應用指定的事件處理方法來處理到達的事件。框架
下面的圖顯示了Runloop和事件源的概念結構。 Input sources異步的分發事件到響應的處理器,而後引發runUntilDate:(由線程相關的Runloop對象調用)方法退出。 Timer sources同步分發事件到相應的處理器可是不會引發Runloop退出。
備註:
Input Sources 異步地分發事件到線程。大概有兩種類型的 Input Sources,Port-based類型的輸入源監控着應用的Mach端口,自定義的輸入源監控着自定義的事件源。NSRunloop不關心輸入源的類型。兩種輸入源惟一的不一樣是輸入源的觸發方式,Port-based輸入源是由系統內核觸發的,而自定義的輸入源要咱們本身觸發。建立輸入源的時候咱們就給給輸入源添加指定的模式。下面是一些輸入源:
Port-Based Sources
Cocoa 和 Core Foundation 提供了類和接口用來建立 Port-Based 源,Cocoa 只要建立 NSPort 對象,並添加到 NSRunloop 中就能夠啦,NSPort負責輸入源的建立和配置。Core Foundation 須要手動的常見 port 和輸入源。
Custom Input Sources
咱們要用到CFRunLoopSourceRef函數建立輸入源,並定義幾個回調函數用於配置輸入源、處理事件和刪除輸入源。事件的觸發機制要咱們本身定義。
Cocoa Perform Selector Sources
Cocoa定義了能夠在任何線程上執行方法的事件源,在想要執行的線程上執行方法是順序執行的,避免了多個方法在線程上執行的同步問題。Perform Selector Sources在方法執行完以後就會本身從NSRunloop中刪除。
Perform Selector Sources要求目標線程的NSRunloop必須是運行的,主線程默認是運行的。NSRunloop在一次迭代過程當中會處理全部的Perform Selector調用,而不是一次迭代處理一個Perform Selector調用。NSObject中定義的Perform Selector方法以下
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:
performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:
cancelPreviousPerformRequestsWithTarget:
cancelPreviousPerformRequestsWithTarget:selector:object:
延遲執行是在NSRunloop的下一次迭代中過了指定的延遲事件才執行。取消操做是針對延遲執行方法的。
Timer Sources 同步地在未來的一個肯定的時間分發事件到咱們的線程。Timers 可讓線程通知本身去處理一些事情。Timers 不是一個實時的機制,當 Timers 觸發的時候 NSrunloop 恰好正在執行處理函數,Timer s會等待 NSRunloop 調用本身的處理函數。
Timers 能夠建立一次性的和重複性的事件,當建立重複性的事件的時候,Timers 只會根據規劃好的觸發時間來從新規劃觸發時間,而不是根據確切的觸發時間。並且因爲延遲觸發丟失了幾回觸發的話,Timers 只會補充一次觸發。
不像是事件源同樣在事件觸發的時候執行處理函數。NSRunloop 觀察者是在 NSRunloop 幾個執行的特定的點觸發。NSRunloop 能夠觀察的幾個事件是:
建立觀察者的方法是 CFRunLoopObserverRef,咱們能夠通 過Core Foundation 方法添加到指定的 NSRunloop。觀察者也能夠建立一次性的和重複性的。一次性的觀察者觸發以後就會從 NSRunloo p中刪除。
Core Foundation
中關於 RunLoop
的5個類
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
注:RunLoop 若是沒有這些東西會直接退出CFRunLoopModeRef表明RunLoop的運行模式:一個 RunLoop 包含若干個 Mode,每一個Mode又包含若干個 Source/Timer/Observer
每次RunLoop啓動時,只能指定其中一個 Mode,這個Mode被稱做 CurrentMode 若是須要切換 Mode,只能退出 Loop,再從新指定一個 Mode 進入 這樣作主要是爲了分隔開不一樣組的 Source/Timer/Observer,讓其互不影響。
系統默認註冊了5個Mode:(前兩個跟最後一個經常使用)
kCFRunLoopDefaultMode
:App的默認Mode,一般主線程是在這個Mode下運行UITrackingRunLoopMode
:界面跟蹤 Mode,用於 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其餘 Mode 影響UIInitializationRunLoopMode
: 在剛啓動 App 時第進入的第一個 Mode,啓動完成後就再也不使用GSEventReceiveRunLoopMode
: 接受系統事件的內部 Mode,一般用不到kCFRunLoopCommonModes
: 這是一個佔位用的Mode,不是一種真正的Mode按照官方文檔的分類:
按照函數調用棧的分類
函數調用棧
CFRunLoopTimerRef 是基於時間的觸發器,基本上說的就是 NSTimer (CADisplayLink 也是加到 RunLoop),它受 RunLoop 的 Mode 影響。
GCD的定時器不受 RunLoop 的 Mode 影響。
CFRunLoopObserverRef是觀察者,可以監聽RunLoop的狀態改變 能夠監聽的時間點有如下幾個
使用
- (void)observer {
// 建立observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
NSLog(@"----監聽到RunLoop狀態發生改變---%zd", activity);
});
// 添加觀察者:監聽RunLoop的狀態
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
// 釋放Observer
CFRelease(observer);
}
特別注意
/*
CF的內存管理(Core Foundation)
1.凡是帶有Create、Copy、Retain等字眼的函數,建立出來的對象,都須要在最後作一次release
* 好比CFRunLoopObserverCreate
2.release函數:CFRelease(對象);
*/
複製代碼
場景還原:拖拽時模式由 NSDefaultRunLoopMode 進入 UITrackingRunLoopMode ,NSTimer 再也不響應圖片中止輪播,將計時器改爲 NSRunLoopCommonModes 模式下兩種模式均可運行。
- (void)timer {
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
// 定時器只運行在NSDefaultRunLoopMode下,一旦RunLoop進入其餘模式,這個定時器就不會工做
// [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
// 定時器只運行在UITrackingRunLoopMode下,一旦RunLoop進入其餘模式,這個定時器就不會工做
// [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
// 定時器會跑在標記爲common modes的模式下
// 標記爲common modes的模式:UITrackingRunLoopMode和NSDefaultRunLoopMode兼容
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
- (void)timer2 {
// 調用了scheduledTimer返回的定時器,已經自動被添加到當前runLoop中,並且是NSDefaultRunLoopMode
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
// 修改模式
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
複製代碼
需求:當用戶在拖拽時(UI交互時)不顯示圖片,拖拽完成時顯示圖片
// 只在NSDefaultRunLoopMode模式下顯示圖片
// inModes:設置運行模式
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"placeholder"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];
複製代碼
應用場景: 常常在後臺進行耗時操做,如:監控聯網狀態,掃描沙盒等 不但願線程處理完事件就銷燬,保持常駐狀態
- (void)run {
//addPort:添加端口(就是source) forMode:設置模式
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
//啓動RunLoop
[[NSRunLoop currentRunLoop] run];
/*
//另外兩種啓動方式
[NSDate distantFuture]:遙遠的將來 這種寫法跟上面的run是一個意思
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
不設置模式
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];
*/
}
複製代碼
退出-退出當前線程[NSThread exit];
複製代碼
- (void)run {
while (1) {
[[NSRunLoop currentRunLoop] run];
}
}
複製代碼
在休眠前(kCFRunLoopBeforeWaiting)進行釋放,處理事件前建立釋放池,中間建立的對象會放入釋放池。
特別注意:在啓動 RunLoop 以前建議用 @autoreleasepool {...} 包裹。
意義:建立一個大釋放池,釋放 {} 期間建立的臨時對象,通常好的框架的做者都會這麼作。
- (void)execute {
@autoreleasepool {
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
}
複製代碼
通常的NSTimer定時器由於受到RunLoop,會存在時間不許時的狀況。 上文有提到GCD不受RunLoop影響,下面簡單的說一下它的使用
/** 定時器(這裏不用帶*,由於 dispatch_source_t 就是個類,內部已經包含了*) */
@property (nonatomic, strong) dispatch_source_t timer;
int count = 0;
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
// 得到隊列
// dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_queue_t queue = dispatch_get_main_queue();
// 建立一個定時器(dispatch_source_t本質仍是個OC對象)
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 設置定時器的各類屬性(幾時開始任務,每隔多長時間執行一次)
// GCD的時間參數,通常是納秒 NSEC_PER_SEC(1秒 == 10的9次方納秒)
// 什麼時候開始執行第一個任務
// dispatch_time(DISPATCH_TIME_NOW, 3.0 * NSEC_PER_SEC) 比當前時間晚3秒
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
uint64_t interval = (uint64_t)(1.0 * NSEC_PER_SEC);
dispatch_source_set_timer(self.timer, start, interval, 0);
// 設置回調
dispatch_source_set_event_handler(self.timer, ^{
NSLog(@"------------%@", [NSThread currentThread]);
count++;
// if (count == 4) {
// // 取消定時器
// dispatch_cancel(self.timer);
// self.timer = nil;
// }
});
// 啓動定時器
dispatch_resume(self.timer);
}
複製代碼
每條線程都有惟一的一個與之對應的 RunLoop 對象;
主線程的 RunLoop 已經自動建立好了,子線程的RunLoop須要主動建立;
RunLoop在第一次獲取時建立,在線程結束時銷燬;
獲取RunLoop對象
// 工做線程 須要程序員手工寫代碼讓runloop運行起來
[NSRunLoop currentLoop]runUntilDate:]
// Foundation
[NSRunLoop currentRunLoop]; // 得到當前線程的RunLoop對象
[NSRunLoop mainRunLoop]; // 得到主線程的RunLoop對象
// Core Foundation
CFRunLoopGetCurrent(); // 得到當前線程的RunLoop對象
CFRunLoopGetMain(); // 得到主線程的RunLoop對象
複製代碼
線程安全性
基於 Cocoa 的接口不是線程安全的,基於 Core Foundation 的接口是線程安全的。
什麼是RunLoop?
在開發中如何使用RunLoop?什麼應用場景?
在異步線程中下載不少圖片。若是失敗了,該如何處理?請結合runloop來談談解決方案?
答:(提示:在異步線程中啓動一個runloop從新發送網絡圖片)
(1)從新下載圖片
(2)利用 runloop 的輸入源回到主線程刷新 UIImageView。
蘋果官方文檔
CFRunLoop官方文檔
NSRunLoop官方文檔
CFRunLoopRef
NSRunloop的使用