解密-神秘的RunLoop
引言
一直以來(lái)RunLoop就是個(gè)神秘的領(lǐng)域,好多2.3年的開(kāi)發(fā)者都不能準(zhǔn)確的表述它的作用,說(shuō)它神秘,其實(shí)RunLoop并沒(méi)有大家想象中的那么神秘,那么不好理解,本文就帶大家好好剖析一下”神秘的RunLoop”
什么是RunLoop
從字面上看
- 運(yùn)行循環(huán)
- 跑圈
循環(huán)
基本作用
- 保持程序的持續(xù)運(yùn)行(比如主運(yùn)行循環(huán))
- 處理App中的各種事件(比如觸摸事件、定時(shí)器事件、Selector事件)
- 節(jié)省CPU資源,提高程序性能:該做事時(shí)做事,該休息時(shí)休息
存在價(jià)值
沒(méi)有RunLoop
有RunLoop
主運(yùn)行循環(huán)
- 第14行代碼的UIApplicationMain函數(shù)內(nèi)部就啟動(dòng)了一個(gè)RunLoop
- 所以UIApplicationMain函數(shù)一直沒(méi)有返回,保持了程序的持續(xù)運(yùn)行
- 這個(gè)默認(rèn)啟動(dòng)的RunLoop是跟主線程相關(guān)聯(lián)的
RunLoop對(duì)象
- iOS中有2套API來(lái)訪問(wèn)和使用RunLoop
Foundation
NSRunLoop
Core Foundation
CFRunLoopRef
- NSRunLoop和CFRunLoopRef都代表著RunLoop對(duì)象
- NSRunLoop是基于CFRunLoopRef的一層OC包裝,所以要了解RunLoop內(nèi)部結(jié)構(gòu),需要多研究CFRunLoopRef層面的API(Core Foundation層面)
RunLoop資料
- 蘋果官方文檔
https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html
- CFRunLoopRef是開(kāi)源的
http://opensource.apple.com/source/CF/CF-1151.16/
RunLoop與線程
每條線程都有唯一的一個(gè)與之對(duì)應(yīng)的RunLoop對(duì)象
主線程的RunLoop已經(jīng)自動(dòng)創(chuàng)建好了,子線程的RunLoop需要主動(dòng)創(chuàng)建
RunLoop在***次獲取時(shí)創(chuàng)建,在線程結(jié)束時(shí)銷毀
獲取RunLoop對(duì)象
- Foundation
- [NSRunLoop currentRunLoop]; // 獲得當(dāng)前線程的RunLoop對(duì)象
- [NSRunLoop mainRunLoop]; // 獲得主線程的RunLoop對(duì)象
- Core Foundation
- CFRunLoopGetCurrent(); // 獲得當(dāng)前線程的RunLoop對(duì)象
- CFRunLoopGetMain(); // 獲得主線程的RunLoop對(duì)象
RunLoop相關(guān)類
- Core Foundation中關(guān)于RunLoop的5個(gè)類
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
注:RunLoop如果沒(méi)有這些東西 會(huì)直接退出
CFRunLoopModeRef
- CFRunLoopModeRef代表RunLoop的運(yùn)行模式
一個(gè) RunLoop 包含若干個(gè) Mode,每個(gè)Mode又包含若干個(gè)Source/Timer/Observer
每次RunLoop啟動(dòng)時(shí),只能指定其中一個(gè) Mode,這個(gè)Mode被稱作 CurrentMode
如果需要切換Mode,只能退出Loop,再重新指定一個(gè)Mode進(jìn)入
這樣做主要是為了分隔開(kāi)不同組的Source/Timer/Observer,讓其互不影響
相關(guān)類
系統(tǒng)默認(rèn)注冊(cè)了5個(gè)Mode:(前兩個(gè)跟***一個(gè)常用)
- kCFRunLoopDefaultMode:App的默認(rèn)Mode,通常主線程是在這個(gè)Mode下運(yùn)行
- UITrackingRunLoopMode:界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動(dòng),保證界面滑動(dòng)時(shí)不受其他 Mode 影響
- UIInitializationRunLoopMode: 在剛啟動(dòng) App 時(shí)第進(jìn)入的***個(gè) Mode,啟動(dòng)完成后就不再使用
- GSEventReceiveRunLoopMode: 接受系統(tǒng)事件的內(nèi)部 Mode,通常用不到
- kCFRunLoopCommonModes: 這是一個(gè)占位用的Mode,不是一種真正的Mode
CFRunLoopSourceRef
- CFRunLoopSourceRef是事件源(輸入源)
- 按照官方文檔的分類
Port-Based Sources (基于端口,跟其他線程交互,通過(guò)內(nèi)核發(fā)布的消息)
Custom Input Sources (自定義)
Cocoa Perform Selector Sources (performSelector…方法)
- 按照函數(shù)調(diào)用棧的分類
Source0:非基于Port的
Source1:基于Port的
Source0: event事件,只含有回調(diào),需要先調(diào)用CFRunLoopSourceSignal(source),將這個(gè) Source 標(biāo)記為待處理,然后手動(dòng)調(diào)用 CFRunLoopWakeUp(runloop) 來(lái)喚醒 RunLoop。
Source1: 包含了一個(gè) mach_port 和一個(gè)回調(diào),被用于通過(guò)內(nèi)核和其他線程相互發(fā)送消息,能主動(dòng)喚醒 RunLoop 的線程。
函數(shù)調(diào)用棧
函數(shù)調(diào)用棧
CFRunLoopTimerRef
- CFRunLoopTimerRef是基于時(shí)間的觸發(fā)器
- 基本上說(shuō)的就是NSTimer(CADisplayLink也是加到RunLoop),它受RunLoop的Mode影響
- GCD的定時(shí)器不受RunLoop的Mode影響
CFRunLoopObserverRef
- CFRunLoopObserverRef是觀察者,能夠監(jiān)聽(tīng)RunLoop的狀態(tài)改變
- 可以監(jiān)聽(tīng)的時(shí)間點(diǎn)有以下幾個(gè)
使用
- - (void)observer
- {
- // 創(chuàng)建observer
- CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(),kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
- NSLog(@"----監(jiān)聽(tīng)到RunLoop狀態(tài)發(fā)生改變---%zd", activity);
- });
- // 添加觀察者:監(jiān)聽(tīng)RunLoop的狀態(tài)
- CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
- // 釋放Observer
- CFRelease(observer);
- }
特別注意
- /*
- CF的內(nèi)存管理(Core Foundation)
- 1.凡是帶有Create、Copy、Retain等字眼的函數(shù),創(chuàng)建出來(lái)的對(duì)象,都需要在***做一次release
- * 比如CFRunLoopObserverCreate
- 2.release函數(shù):CFRelease(對(duì)象);
- */
RunLoop處理邏輯
– 官方版
邏輯
– 網(wǎng)友整理版
網(wǎng)友版
注:進(jìn)入RunLoop前 會(huì)判斷模式是否為空,為空直接退出
RunLoop應(yīng)用
- NSTimer
- ImageView顯示
- PerformSelector
- 常駐線程
- 自動(dòng)釋放池
1.NSTimer(最常見(jiàn)RunLoop使用)
- - (void)timer
- {
- NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
- // 定時(shí)器只運(yùn)行在NSDefaultRunLoopMode下,一旦RunLoop進(jìn)入其他模式,這個(gè)定時(shí)器就不會(huì)工作
- // [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
- // 定時(shí)器只運(yùn)行在UITrackingRunLoopMode下,一旦RunLoop進(jìn)入其他模式,這個(gè)定時(shí)器就不會(huì)工作
- // [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
- // 定時(shí)器會(huì)跑在標(biāo)記為common modes的模式下
- // 標(biāo)記為common modes的模式:UITrackingRunLoopMode和NSDefaultRunLoopMode兼容
- [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
- }
- - (void)timer2
- {
- // 調(diào)用了scheduledTimer返回的定時(shí)器,已經(jīng)自動(dòng)被添加到當(dāng)前runLoop中,而且是NSDefaultRunLoopMode
- NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nilrepeats:YES];
- // 修改模式
- [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
- }
場(chǎng)景還原
拖拽時(shí)模式由NSDefaultRunLoopMode 進(jìn)入 UITrackingRunLoopMode
此時(shí)如下圖: NSTimer 不再響應(yīng) 圖片停止輪播
NSDefaultRunLoopMode模式
NSRunLoopCommonModes 模式下兩種模式都可運(yùn)行
此時(shí)如下圖: NSTimer 在兩個(gè)模式下都可正常運(yùn)行
2.ImageView
需求:當(dāng)用戶在拖拽時(shí)(UI交互時(shí))不顯示圖片,拖拽完成時(shí)顯示圖片
方法1 監(jiān)聽(tīng)UIScrollerView滾動(dòng) (通過(guò)UIScrollViewDelegate監(jiān)聽(tīng),此處不再舉例)
方法2 RunLoop 設(shè)置運(yùn)行模式
- // 只在NSDefaultRunLoopMode模式下顯示圖片
- [self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"placeholder"]afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];
3.PerformSelector
inModes:設(shè)置運(yùn)行模式
4.常駐線程 (重要)
應(yīng)用場(chǎng)景:經(jīng)常在后臺(tái)進(jìn)行耗時(shí)操作,如:監(jiān)控聯(lián)網(wǎng)狀態(tài),掃描沙盒等 不希望線程處理完事件就銷毀,保持常駐狀態(tài)
***種(推薦)
開(kāi)啟
- - (void)run
- {
- //addPort:添加端口(就是source) forMode:設(shè)置模式
- [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
- //啟動(dòng)RunLoop
- [[NSRunLoop currentRunLoop] run];
- /*
- //另外兩種啟動(dòng)方式
- [NSDate distantFuture]:遙遠(yuǎn)的未來(lái) 這種寫法跟上面的run是一個(gè)意思
- [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
- 不設(shè)置模式
- [[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];
- */
- }
退出-退出當(dāng)前線程
- [NSThread exit];
第二種(奇葩法)
優(yōu)點(diǎn):退出RunLoop比較方便-定義個(gè)標(biāo)記 while(flag){…}
- - (void)run
- {
- while (1) {
- [[NSRunLoop currentRunLoop] run];
- }
- }
5.自動(dòng)釋放池
在休眠前(kCFRunLoopBeforeWaiting)進(jìn)行釋放,處理事件前創(chuàng)建釋放池,中間創(chuàng)建的對(duì)象會(huì)放入釋放池
特別注意:
在啟動(dòng)RunLoop之前建議用 @autoreleasepool {…}包裹
意義:創(chuàng)建一個(gè)大釋放池,釋放{}期間創(chuàng)建的臨時(shí)對(duì)象,一般好的框架的作者都會(huì)這么做
- - (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];
- }
- }
題外話:
以后為了增加用戶體驗(yàn) 在用戶UI交互的時(shí)候 不做事件處理 我們可以把需要做的操作放到NSDefaultRunLoopMode
補(bǔ)充:GCD定時(shí)器
一般的NSTimer定時(shí)器因?yàn)槭艿絉unLoop,會(huì)存在時(shí)間不準(zhǔn)時(shí)的情況.
上文有提到GCD不受RunLoop影響,下面簡(jiǎn)單的說(shuō)一下它的使用
- /** 定時(shí)器(這里不用帶*,因?yàn)閐ispatch_source_t就是個(gè)類,內(nèi)部已經(jīng)包含了*) */
- @property (nonatomic, strong) dispatch_source_t timer;
- int count = 0;
- - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
- {
- // 獲得隊(duì)列
- // dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
- dispatch_queue_t queue = dispatch_get_main_queue();
- // 創(chuàng)建一個(gè)定時(shí)器(dispatch_source_t本質(zhì)還是個(gè)OC對(duì)象)
- self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
- // 設(shè)置定時(shí)器的各種屬性(幾時(shí)開(kāi)始任務(wù),每隔多長(zhǎng)時(shí)間執(zhí)行一次)
- // GCD的時(shí)間參數(shù),一般是納秒 NSEC_PER_SEC(1秒 == 10的9次方納秒)
- // 何時(shí)開(kāi)始執(zhí)行***個(gè)任務(wù)
- // dispatch_time(DISPATCH_TIME_NOW, 3.0 * NSEC_PER_SEC) 比當(dāng)前時(shí)間晚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) {
- // // 取消定時(shí)器
- // dispatch_cancel(self.timer);
- // self.timer = nil;
- // }
- });
- // 啟動(dòng)定時(shí)器
- dispatch_resume(self.timer);
- }
RunLoop面試題
經(jīng)常會(huì)有喜歡裝B的面試官,面試的時(shí)候就喜歡問(wèn)RunLoop,其實(shí)他真的會(huì)嗎? 說(shuō)不定他自己都不太理解
下面我對(duì)有關(guān)RunLoop的面試做一個(gè)簡(jiǎn)單的總結(jié),也算是對(duì)全文一個(gè)總結(jié)
- 什么是RunLoop?
從字面上看:運(yùn)行循環(huán)、跑圈
其實(shí)它內(nèi)部就是do-while循環(huán),在這個(gè)循環(huán)內(nèi)部不斷的處理各種任務(wù)(比如Source、Timer、Observer)
一個(gè)線程對(duì)應(yīng)一個(gè)RunLoop,主線程的RunLoop默認(rèn)已經(jīng)啟動(dòng),子線程的RunLoop需要手動(dòng)啟動(dòng)(調(diào)用run方法)
RunLoop只能選擇一個(gè)Mode啟動(dòng),如果當(dāng)前Mode中沒(méi)有任何Soure、Timer、Observer,那么就直接退出RunLoop
- 在開(kāi)發(fā)中如何使用RunLoop?什么應(yīng)用場(chǎng)景?
- 開(kāi)啟一個(gè)常駐線程(讓一個(gè)子線程不進(jìn)入消亡狀態(tài),等待其他線程發(fā)來(lái)消息,處理其他事件)
在子線程中開(kāi)啟一個(gè)定時(shí)器
在子線程中進(jìn)行一些長(zhǎng)期監(jiān)控
- 可以控制定時(shí)器在特定模式下執(zhí)行
- 可以讓某些事件(行為、任務(wù))在特定模式下執(zhí)行
- 可以添加Observer監(jiān)聽(tīng)RunLoop的狀態(tài),比如監(jiān)聽(tīng)點(diǎn)擊事件的處理(在所有點(diǎn)擊事件之前做一些事情)
***
之前發(fā)布的文章寫得不是很完整,我又花了兩天時(shí)間重新做了梳理,還有什么不足之處,歡迎大家指出,我會(huì)***時(shí)間更新.