一撕贞、Runloop 簡介
1. 簡介
- RunLoop就是讓線程隨時處理事件但不退出的機制
- 每一個線程都有一個RunLoop
- RunLoop 實際上就是一個對象唆涝,這個對象管理了其需要處理的事件(比如button的點擊、各種手勢的的事件萝究、定時器免都、tableView的代理方法)和消息,是iOS里的一種事件處理機制帆竹。
- 線程執(zhí)行了這個函數(shù)后绕娘,就會一直處于這個函數(shù)內(nèi)部 “接受消息->等待->處理” 的循環(huán)中,直到這個循環(huán)結(jié)束(比如傳入 quit 的消息)栽连,函數(shù)返回险领。
2. 基本作用
- 保持程序的持續(xù)運行(比如主運行循環(huán))
- 處理App中的各種事件(比如觸摸事件、定時器事件秒紧、Selector事件)
- 節(jié)省CPU資源绢陌,提高程序性能:該做事時做事,該休息時休息
3. API
OSX / iOS 系統(tǒng)中熔恢,有2套API來訪問和使用 RunLoop
脐湾。
-
CFRunLoopRef
是在CoreFoundation
框架內(nèi)的,它提供了純 C 函數(shù)的 API绩聘,所有這些 API 都是線程安全的沥割。 -
NSRunLoop
是基于CFRunLoopRef
的封裝,提供了面向?qū)ο蟮?API凿菩,但是這些 API 不是線程安全的机杜。所以要了解RunLoop
內(nèi)部結(jié)構(gòu),需要多研究CFRunLoopRef
層面的API(Core Foundation
層面)
NSRunLoop
和 CFRunLoopRef
都代表著 RunLoop
對象衅谷。
4. 存在價值
main
函數(shù)中的 RunLoop
(主運行循環(huán)):第14行代碼的 UIApplicationMain
函數(shù)內(nèi)部就啟動了一個 RunLoop
椒拗。所以 UIApplicationMain
函數(shù)一直沒有返回,保持了程序的持續(xù)運行。這個默認啟動的 RunLoop
是跟主線程相關(guān)聯(lián)的蚀苛。
二在验、Runloop 解析
1. Runloop 運行模式
一種 Runloop 運行模式就是一個要監(jiān)控的 Input 和 Timer 事件源的集合或者是一個要通知的 Runloop 觀察者的集合。每次運 行Runloop堵未,都要指定一個運行模式(顯示地或者隱式地)腋舌。在 Runloop 的運行期間,只有和當前運行模式相關(guān)的源才能被監(jiān)控和允許發(fā)送事件渗蟹。相似的块饺,只有和當前運行模式相關(guān)的觀察者才會被通知 Runloop 的行為。和其他模式相關(guān)的源會保留新的事件直到 Runloop 運行在了合適的模式才會分發(fā)雌芽。
在我們的代碼中授艰,我們可以通過字符串來標識模式。Cocoa和Core Foundation定義了一個默認模式和幾個普通的有用的模式世落,這些模式都是用字符串來標識的淮腾。我們可以用一個字符串當做名字來自定義一個模式,雖然我們自定義模式的名字是隨意的屉佳,但是模式的內(nèi)容不是隨意的谷朝,在我們自己創(chuàng)建的要用的模式中至少要添加一個 Input 源、 Timer 源或者 Runloop 觀察者忘古。
在 Runloop 的特殊階段我們可是使用運行模式來過濾我們不想要的源的事件徘禁,大多數(shù)的情況下,Runloop 都運行在系統(tǒng)提供的默認模式下髓堪,然而 Model Panel 可能運行在“模式”模式,當運行在這個模式期間娘荡,只有和這個模式相關(guān)的事件源才會發(fā)送事件到我們的線程干旁。對于第二線程來說,我們通常使用自定義模式來阻止低優(yōu)先級的事件源在其他關(guān)鍵處理的時間內(nèi)發(fā)送事件炮沐。
注意:運行模式不是根據(jù)事件類型劃分的争群,而是根據(jù)事件源劃分的。我們不能通過模式來匹配鼠標按下事件或者鍵盤事件大年,但是我們可以用運行模式來監(jiān)聽一組不同的Port换薄、暫時掛起Timers或者改變當前被監(jiān)控的事件源和Runloop觀察者。
下面列舉了一些Cocoa和Core Foundation定義的標準模式:
-
NSDefaultRunLoopMode
:默認的運行模式翔试,用于大部分操作轻要,除了NSConnection對象事件。 -
NSConnectionReplyMode
:用來監(jiān)控NSConnection對象的回復的垦缅,很少能夠用到冲泥。 -
NSModalPanelRunLoopMode
:用于標明和Mode Panel相關(guān)的事件。 -
NSEventTrackingRunLoopMode
:用于跟蹤觸摸事件觸發(fā)的模式(例如UIScrollView上下滾動)。 -
NSRunLoopCommonModes
:是一個模式集合凡恍,當綁定一個事件源到這個模式集合的時候就相當于綁定到了集合內(nèi)的每一個模式志秃。Cocoa 應(yīng)用默認包含 Default、Panel嚼酝、Event Tracking 模式浮还,Core Foundation 只包含 Default 模式,我們可以通過CFRunLoopAddCommonMode
添加模式闽巩。
2. Runloop 處理邏輯
Runloop接收來自兩種源的事件:
- 輸入源(Input sources):傳遞異步消息碑定,通常來自于其他線程或者程序。
- 定時源(Timer sources):傳遞同步消息又官,在設(shè)定好的時間或者循環(huán)間斷地發(fā)生的事件延刘。
這兩種事件源都是使用應(yīng)用指定的事件處理方法來處理到達的事件。
下面的圖顯示了Runloop和事件源的概念結(jié)構(gòu)六敬。 Input sources異步的分發(fā)事件到響應(yīng)的處理器碘赖,然后引起runUntilDate:(由線程相關(guān)的Runloop對象調(diào)用)方法退出。 Timer sources同步分發(fā)事件到相應(yīng)的處理器但是不會引起Runloop退出外构。
備注:
- 輸入源:每一個需要Runloop處理事件的對象都有一個輸入源(InputSource),并且把這個輸入源添加到Runloop里普泡,每產(chǎn)生一個事件(比如用戶做了一個手勢、點了一個button审编、滑動了一下tableview撼班、定時器到時)就把這個事件放到對應(yīng)的輸入源。Runloop運行時循環(huán)檢查每一個輸入源是否有事件需要處理垒酬,如果有事件要處理Runloop就就調(diào)用這個事件的處理方法(通過addTargetxxx指定的方法或者是代理的方法)砰嘁。如果Runloop里所有的輸入源都沒有事件要處理,Runloop會休眠勘究。如果Runloop里一個輸入源都沒有(對象銷毀前會把它之前添加的那個輸入源取消)矮湘,Runloop(runUntilDate:這個方法)就退出來了。
- 除了處理輸入源的事件口糕,Runloop也會生成Runloop行為的通知缅阳。注冊Runloop的觀察者可以收到這些消息,然后在線程內(nèi)用他們做一些額外的處理景描。我們只能使用Core Foundation接口來注冊線程的Runloop觀察者
3. Input Sources
Input Sources 異步地分發(fā)事件到線程十办。大概有兩種類型的 Input Sources,Port-based類型的輸入源監(jiān)控著應(yīng)用的Mach端口超棺,自定義的輸入源監(jiān)控著自定義的事件源向族。NSRunloop不關(guān)心輸入源的類型。兩種輸入源唯一的不同是輸入源的觸發(fā)方式说搅,Port-based輸入源是由系統(tǒng)內(nèi)核觸發(fā)的炸枣,而自定義的輸入源要我們自己觸發(fā)。創(chuàng)建輸入源的時候我們就給給輸入源添加指定的模式。下面是一些輸入源:
Port-Based Sources
Cocoa 和 Core Foundation 提供了類和接口用來創(chuàng)建 Port-Based 源适肠,Cocoa 只要創(chuàng)建 NSPort 對象霍衫,并添加到 NSRunloop 中就可以啦,NSPort負責輸入源的創(chuàng)建和配置侯养。Core Foundation 需要手動的常見 port 和輸入源敦跌。Custom Input Sources
我們要用到CFRunLoopSourceRef函數(shù)創(chuàng)建輸入源,并定義幾個回調(diào)函數(shù)用于配置輸入源逛揩、處理事件和刪除輸入源柠傍。事件的觸發(fā)機制要我們自己定義。-
Cocoa Perform Selector Sources
Cocoa定義了可以在任何線程上執(zhí)行方法的事件源辩稽,在想要執(zhí)行的線程上執(zhí)行方法是順序執(zhí)行的惧笛,避免了多個方法在線程上執(zhí)行的同步問題。Perform Selector Sources在方法執(zhí)行完之后就會自己從NSRunloop中刪除逞泄。
Perform Selector Sources要求目標線程的NSRunloop必須是運行的患整,主線程默認是運行的。NSRunloop在一次迭代過程中會處理所有的Perform Selector調(diào)用喷众,而不是一次迭代處理一個Perform Selector調(diào)用各谚。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:
延遲執(zhí)行是在NSRunloop的下一次迭代中過了指定的延遲事件才執(zhí)行。取消操作是針對延遲執(zhí)行方法的到千。
4. Timer Sources
Timer Sources 同步地在將來的一個確定的時間分發(fā)事件到我們的線程昌渤。Timers 可以讓線程通知自己去處理一些事情曹锨。Timers 不是一個實時的機制娃弓,當 Timers 觸發(fā)的時候 NSrunloop 剛好正在執(zhí)行處理函數(shù)爽丹,Timer s會等待 NSRunloop 調(diào)用自己的處理函數(shù)奴潘。
Timers 可以創(chuàng)建一次性的和重復性的事件,當創(chuàng)建重復性的事件的時候冬耿,Timers 只會根據(jù)規(guī)劃好的觸發(fā)時間來重新規(guī)劃觸發(fā)時間蚀腿,而不是根據(jù)確切的觸發(fā)時間宣羊。而且由于延遲觸發(fā)丟失了幾次觸發(fā)的話斟览,Timers 只會補充一次觸發(fā)。
5. NSRunloop 觀察者
不像是事件源一樣在事件觸發(fā)的時候執(zhí)行處理函數(shù)辑奈。NSRunloop 觀察者是在 NSRunloop 幾個執(zhí)行的特定的點觸發(fā)苛茂。NSRunloop 可以觀察的幾個事件是:
- 進入 NSRunloop
- NSRunloop 將要處理 Timer 事件
- NSRunloop 將要處理 Input 事件
- NSRunloop 將要進入睡眠
- NSRunloop 被喚醒,但是是在處理事件之前
- 退出 NSRunloop
創(chuàng)建觀察者的方法是 CFRunLoopObserverRef鸠窗,我們可以通 過Core Foundation 方法添加到指定的 NSRunloop妓羊。觀察者也可以創(chuàng)建一次性的和重復性的。一次性的觀察者觸發(fā)之后就會從 NSRunloo p中刪除稍计。
三躁绸、RunLoop 相關(guān)類
Core Foundation
中關(guān)于 RunLoop
的5個類
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
-
CFRunLoopObserverRef
注:RunLoop 如果沒有這些東西會直接退出
1. CFRunLoopModeRef
CFRunLoopModeRef代表RunLoop的運行模式:一個 RunLoop 包含若干個 Mode,每個Mode又包含若干個 Source/Timer/Observer
每次RunLoop啟動時,只能指定其中一個 Mode净刮,這個Mode被稱作 CurrentMode 如果需要切換 Mode剥哑,只能退出 Loop,再重新指定一個 Mode 進入 這樣做主要是為了分隔開不同組的 Source/Timer/Observer淹父,讓其互不影響株婴。
系統(tǒng)默認注冊了5個Mode:(前兩個跟最后一個常用)
-
kCFRunLoopDefaultMode
:App的默認Mode,通常主線程是在這個Mode下運行 -
UITrackingRunLoopMode
:界面跟蹤 Mode暑认,用于 ScrollView 追蹤觸摸滑動困介,保證界面滑動時不受其他 Mode 影響 -
UIInitializationRunLoopMode
: 在剛啟動 App 時第進入的第一個 Mode,啟動完成后就不再使用 -
GSEventReceiveRunLoopMode
: 接受系統(tǒng)事件的內(nèi)部 Mode蘸际,通常用不到 -
kCFRunLoopCommonModes
: 這是一個占位用的Mode座哩,不是一種真正的Mode
2. CFRunLoopSourceRef 事件源(輸入源)
按照官方文檔的分類:
- Port-Based Sources (基于端口,跟其他線程交互,通過內(nèi)核發(fā)布的消息)
- Custom Input Sources (自定義)
- Cocoa Perform Selector Sources (performSelector...方法)
按照函數(shù)調(diào)用棧的分類
- Source0:非基于Port的,event事件粮彤,只含有回調(diào)根穷,需要先調(diào)用 CFRunLoopSourceSignal(source),將這個 Source 標記為待處理驾诈,然后手動調(diào)用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop缠诅。
- Source1:基于Port的,包含了一個 mach_port 和一個回調(diào)乍迄,被用于通過內(nèi)核和其他線程相互發(fā)送消息,能主動喚醒 RunLoop 的線程管引。
函數(shù)調(diào)用棧
3. CFRunLoopTimerRef
CFRunLoopTimerRef 是基于時間的觸發(fā)器,基本上說的就是 NSTimer (CADisplayLink 也是加到 RunLoop),它受 RunLoop 的 Mode 影響闯两。
GCD的定時器不受 RunLoop 的 Mode 影響褥伴。
4. CFRunLoopObserverRef
CFRunLoopObserverRef是觀察者,能夠監(jiān)聽RunLoop的狀態(tài)改變 可以監(jiān)聽的時間點有以下幾個
使用
- (void)observer {
// 創(chuàng)建observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
NSLog(@"----監(jiān)聽到RunLoop狀態(tài)發(fā)生改變---%zd", activity);
});
// 添加觀察者:監(jiān)聽RunLoop的狀態(tài)
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
// 釋放Observer
CFRelease(observer);
}
特別注意
/*
CF的內(nèi)存管理(Core Foundation)
1.凡是帶有Create漾狼、Copy重慢、Retain等字眼的函數(shù),創(chuàng)建出來的對象逊躁,都需要在最后做一次release
* 比如CFRunLoopObserverCreate
2.release函數(shù):CFRelease(對象);
*/
復制代碼
四似踱、runloop應(yīng)用
- NSTimer
- PerformSelector
- ImageView顯示
- 需要讓線程執(zhí)行周期性的工作(常駐線程)
- 自動釋放池
- 需要使用 Port 或者自定義 Input Source 與其他線程進行通訊
- NSURLConnection 在子線程中發(fā)起異步請求
1. NSTimer (最常見RunLoop使用)
場景還原:拖拽時模式由 NSDefaultRunLoopMode 進入 UITrackingRunLoopMode ,NSTimer 不再響應(yīng)圖片停止輪播稽煤,將計時器改成 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 {
// 調(diào)用了scheduledTimer返回的定時器匾二,已經(jīng)自動被添加到當前runLoop中哮独,而且是NSDefaultRunLoopMode
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
// 修改模式
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
復制代碼
2. ImageView
需求:當用戶在拖拽時(UI交互時)不顯示圖片,拖拽完成時顯示圖片
方法1 監(jiān)聽UIScrollerView滾動 (通過UIScrollViewDelegate監(jiān)聽,此處不再舉例)
-
方法2 RunLoop 設(shè)置運行模式
// 只在NSDefaultRunLoopMode模式下顯示圖片 // inModes:設(shè)置運行模式 [self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"placeholder"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]]; 復制代碼
3. 常駐線程 (重要)
應(yīng)用場景: 經(jīng)常在后臺進行耗時操作,如:監(jiān)控聯(lián)網(wǎng)狀態(tài)拳芙,掃描沙盒等 不希望線程處理完事件就銷毀,保持常駐狀態(tài)
-
第一種(推薦)
開啟- (void)run { //addPort:添加端口(就是source) forMode:設(shè)置模式 [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode]; //啟動RunLoop [[NSRunLoop currentRunLoop] run]; /* //另外兩種啟動方式 [NSDate distantFuture]:遙遠的未來 這種寫法跟上面的run是一個意思 [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; 不設(shè)置模式 [[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]]; */ } 復制代碼
退出-退出當前線程
[NSThread exit]; 復制代碼
-
第二種(奇葩法)
優(yōu)點:退出RunLoop比較方便-定義個標記 while(flag){...}- (void)run { while (1) { [[NSRunLoop currentRunLoop] run]; } } 復制代碼
4. 自動釋放池
在休眠前(kCFRunLoopBeforeWaiting)進行釋放皮璧,處理事件前創(chuàng)建釋放池舟扎,中間創(chuàng)建的對象會放入釋放池。
特別注意:在啟動 RunLoop 之前建議用 @autoreleasepool {...} 包裹恶导。
意義:創(chuàng)建一個大釋放池,釋放 {} 期間創(chuàng)建的臨時對象浆竭,一般好的框架的作者都會這么做。
- (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];
}
復制代碼
5. 補充: GCD定時器
一般的NSTimer定時器因為受到RunLoop惨寿,會存在時間不準時的情況邦泄。 上文有提到GCD不受RunLoop影響,下面簡單的說一下它的使用
/** 定時器(這里不用帶*,因為 dispatch_source_t 就是個類裂垦,內(nèi)部已經(jīng)包含了*) */
@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();
// 創(chuàng)建一個定時器(dispatch_source_t本質(zhì)還是個OC對象)
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 設(shè)置定時器的各種屬性(幾時開始任務(wù)顺囊,每隔多長時間執(zhí)行一次)
// GCD的時間參數(shù),一般是納秒 NSEC_PER_SEC(1秒 == 10的9次方納秒)
// 何時開始執(zhí)行第一個任務(wù)
// 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);
// 設(shè)置回調(diào)
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 與線程
每條線程都有唯一的一個與之對應(yīng)的 RunLoop 對象;
主線程的 RunLoop 已經(jīng)自動創(chuàng)建好了特碳,子線程的RunLoop需要主動創(chuàng)建;
RunLoop在第一次獲取時創(chuàng)建,在線程結(jié)束時銷毀;
-
獲取RunLoop對象
// 工作線程 需要程序員手工寫代碼讓runloop運行起來 [NSRunLoop currentLoop]runUntilDate:] // Foundation [NSRunLoop currentRunLoop]; // 獲得當前線程的RunLoop對象 [NSRunLoop mainRunLoop]; // 獲得主線程的RunLoop對象 // Core Foundation CFRunLoopGetCurrent(); // 獲得當前線程的RunLoop對象 CFRunLoopGetMain(); // 獲得主線程的RunLoop對象 復制代碼
線程安全性
基于 Cocoa 的接口不是線程安全的晕换,基于 Core Foundation 的接口是線程安全的午乓。
六、RunLoop 面試題
-
什么是RunLoop闸准?
- 其實它內(nèi)部就是do-while循環(huán),在這個循環(huán)內(nèi)部不斷的處理各種任務(wù)(比如Source益愈、Timer、Observer)夷家。
- 一個線程對應(yīng)一個RunLoop,主線程的RunLoop默認已經(jīng)啟動,子線程的RunLoop需要手動啟動(調(diào)用run方法) 蒸其。
- RunLoop只能選擇一個Mode啟動,如果當前Mode中沒有任何Soure、Timer库快、Observer,那么就直接退出RunLoop摸袁。
-
在開發(fā)中如何使用RunLoop?什么應(yīng)用場景义屏?
- 開啟一個常駐線程(讓一個子線程不進入消亡狀態(tài),等待其他線程發(fā)來消息,處理其他事件)
- 在子線程中開啟一個定時器
- 在子線程中進行一些長期監(jiān)控
- 可以控制定時器在特定模式下執(zhí)行
- 可以讓某些事件(行為靠汁、任務(wù))在特定模式下執(zhí)行
- 可以添加 Observer 監(jiān)聽 RunLoop 的狀態(tài),比如監(jiān)聽點擊事件的處理(在所有點擊事件之前做一些事情)
在異步線程中下載很多圖片。如果失敗了闽铐,該如何處理膀曾?請結(jié)合runloop來談?wù)劷鉀Q方案?
答:(提示:在異步線程中啟動一個runloop重新發(fā)送網(wǎng)絡(luò)圖片)
(1)重新下載圖片
(2)利用 runloop 的輸入源回到主線程刷新 UIImageView阳啥。