看了一下,上一篇貌似5個月前的😅。
最近公司忙着開發一個cordova的項目,本身也是邊工做邊找一些資料學習,都沒怎麼關注博客上的內容...呃,主要仍是懶癌發做吧😌。爭取多寫寫博客,記錄記錄點滴,也但願無論技能、生活仍是職業生涯上都能不斷成長,共勉~
這篇是關於RunLoop的筆記的整理和一點看法。php
【本次開發環境: Xcode:7.2 iOS Simulator:iphone6 By:啊左 本文Demo下載連接:RunLoop-Demo】html
-----------------------------基本概念-----------------------------git
1、RunLoop簡介github
RunLoop,跑圈。在iOS開發中,也就是運行循環。app
在應用須要的時候本身跑起來運行,在用戶沒有操做的時候就停下來休息。充分節省CPU資源,提升程序性能。框架
二. RunLoop的概念與做用iphone
概念:通常來說,一個線程一次只能執行一個任務,執行完成後線程就會退出。可是有時候咱們須要線程可以一直「待命」隨時處理事件而不退出,這就須要一個機制來完成這樣的任務。異步
這樣一種機制的代碼邏輯以下:函數
function loop() { initialize(); do { var message = get_next_message(); process_message(message); } while (message != quit); }
這種模型一般被稱做 Event Loop。 Event Loop 在不少系統和框架裏都有實現。而實現這種模型的關鍵點在於:如何管理事件/消息,如何讓線程在沒有處理消息時休眠以免資源佔用、在有消息到來時馬上被喚醒。oop
例如一個應用放那裏,不進行操做就像靜止休息同樣,點擊按鈕,就有響應,就像「隨時待命」同樣,這就是RunLoop的功勞。
因此RunLoop 實際上就是一個對象,這個對象管理了其須要處理的事件和消息,並提供了一個入口函數來執行RunLoop 的邏輯。
線程開始這個函數以後,便一直會處於此函數 "接受消息->等待->處理" 的循環中:(有事:作出反應; 木事:休眠省電; 再次有事:從新喚醒、處理事件。)
直到這個循環結束(好比傳入 quit 的消息),最後函數返回。
做用:
RunLoop,最重要的做用,也就是用來管理線程的。能夠說,沒有線程,也就沒有RunLoop的存在必要。
當線程的RunLoop一開啓,RunLoop便開始對線程進行管理工做:在線程執行完任務後,線程便會進入休眠狀態,而且不會退出,隨時等待新的任務。
3、RunLoop與線程的關係
1.每條線程都有惟一的一個與之對應的RunLoop對象;
2.RunLoop在第一次獲取時建立,在線程結束時銷燬;只能在一個線程的內部獲取其 RunLoop(主線程除外)。
3.主線程的RunLoop系統默認啓動,子線程的RunLoop須要主動開啓;
其實在咱們每次創建項目的時候,就已經使用上了RunLoop。
在程序的啓動入口main函數中有這樣一段熟悉的代碼:
int main(int argc, char * argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } }
實際上UIApplicationMain 函數內部就啓動了一個與主線程相關聯的RunLoop。
當咱們點擊運行,系統運行UIApplicationMain函數,系統進入了:主線程main的運行循環。RunLoop使得主線程一直處在運行循環中。
咱們能夠作一下驗證,在「Main.storyboard」中隨意放置幾個按鈕控件,main.m文件代碼修改以下:
int main(int argc, char * argv[]) { @autoreleasepool { NSLog(@"開始"); return 0; } }
點擊運行,輸出「開始」後,模擬器界面也是一片空白。「stop」按鈕也點不下去了:;
由於當輸出「開始」後,「return 0」,以後沒有進入主線程運行循環,程序一啓動就結束了,控件與其餘程序有關的都沒有執行,因此界面空白。
說明了在UIApplicationMain函數中,開啓了一個和主線程相關的RunLoop,致使UIApplicationMain不會返回,一直在運行中,也就保證了程序的持續運行。
這也是爲何應用可以在咱們無任何操做時休息,在咱們進行操做的時候又可以馬上進行響應活動,偏偏由於應用處於RunLoop的「等待命令」的狀態。
4、RunLoop對象與相關類。
對象:
從RunLoop的概念,咱們能夠知道RunLoop 實際上就是一個管理着線程對象。那麼,如何獲取RunLoop對象呢?
[NSRunLoop currentRunLoop]; // 得到當前線程的RunLoop對象
[NSRunLoop mainRunLoop]; // 得到主線程的RunLoop對象
CFRunLoopGetCurrent(); // 得到當前線程的RunLoop對象
CFRunLoopGetMain(); // 得到主線程的RunLoop對象
文檔中的相關類:
CFRunLoopRef CFRunLoopSourceRef CFRunLoopTimerRef CFRunLoopModeRef CFRunLoopObserverRef
他們的關係以下圖:
CFRunLoopSourceRef 輸入源
是事件產生的地方,函數調用棧上Source有兩個版本:Source0 和 Source1。
CFRunLoopTimerRef 定時源
基於時間的觸發器,與NSTimer可混用。
包含了一個時間長度和一個回調函數。當其加入到 RunLoop 時,RunLoop會註冊對應的時間點,當時間點到時,RunLoop會被喚醒以執行那個回調。
CFRunLoopModeRef mode類型
事實上CFRunLoopModeRef 類並無對外暴露,而若是在Xcode中查看CFRunLoopRef,能夠看到CFRunLoopModeRef 類,經過 CFRunLoopRef 的接口進行了封裝。
CFRunLoopModeRef有5種形式:(固然,還有一些開發中基本用不到的更多的蘋果內部的 Mode:Mode介紹)
kCFRunLoopDefaultMode 默認模式,一般主線程在這個模式下運行
UITrackingRunLoopMode 界面跟蹤Mode,用於追蹤Scrollview觸摸滑動時的狀態。
kCFRunLoopCommonModes 佔位符,帶有Common標記的字符串,比較特殊的一個mode;
UIInitializationRunLoopMode:剛啓動App時進入的第一個Mode,啓動後不在使用。
GSEventReceiveRunLoop:內部Mode,接收系事件。
從關係圖,咱們能夠知道RunLoop一次只能指定一種Mode,且可以讓不一樣組的 Source/Timer/Observer互不影響,具體的實現後面會用一個項目例子來參考。
CFRunLoopObserverRef 觀察者
RunLoop的觀察者,可以監聽RunLoop的狀態改變。
每一個 Observer 都包含了一個回調(函數指針),當 RunLoop 的狀態發生變化時,觀察者就能經過回調接受到這個變化,能夠觀察到不一樣時刻的狀態有如下幾個:
/* Run Loop Observer Activities */
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 };
-----------------------------例子-----------------------------
測試1、二的UI設計界面以下:
測試一:RunLoop的運用。
在「ViewController.m」中建立一個子線程,在線程方法中一直開啓RunLoop。並在「Main.storyboard」中添加一個名爲「showSource」的按鈕控件,建立RunLoop事件源,使得RunLoop進入循環:
1 @interface ViewController () 2 3 @property (strong,nonatomic)NSThread *thread; //記得使用Strong屬性 4 - (IBAction)showSource:(id)sender; //點擊按鈕,添加RunLoop事件源用。 5 6 @end 7 8 @implementation ViewController 9 10 - (void)viewDidLoad { 11 [super viewDidLoad]; 12 //建立自定義的子線程 13 self.thread = [[NSThread alloc]initWithTarget:self selector:@selector(threadMethod) object:nil]; 14 [self.thread start]; //啓動子線程 15 } 16 -(void)threadMethod 17 { 18 NSLog(@"打開子線程方法"); 19 while (1) { 20 21 //條件一:run,進入循環,若是沒有source/timer就直接退出,不進入循環,後面加上source才能進入工做。 22 /*【緣由:若是線程中有須要處理的源,可是響應的事件沒有到來的時候,線程就會休眠等待相應事件的發生; 23 這就是爲何run loop能夠作到讓線程有工做的時候忙於工做,而沒工做的時候處於休眠狀態。】 24 */ 25 [[NSRunLoop currentRunLoop]run]; 26 27 //上面一行代碼等於加了參數爲1的while,因此當有source進入循環,下面這條代碼的就不會運行。 28 NSLog(@"這裏是threadMethod:%@", [NSThread currentThread]); 29 //若是要測試「2、addTime」按鈕的話,建議註釋掉上面這句代碼。 30 } 31 } 32 33 #pragma mark -- 測試一:子線程Selector源的啓動 34 - (IBAction)showSource:(id)sender { 35 36 //注意:在這個方法裏面輸出的是main主線程,由於是主線程運行的UI控件行爲。 37 NSLog(@"這裏是主線程:%@",[NSThread currentThread]); 38 /* 39 在沒有run以前,一直處於休眠狀態。因此若是要運行selector方法,還須要threadMethod中條件一不斷循環的Run! 40 在咱們指定的線程中調用方法,此處至關於增長了一個帶source的mode,有內容,實現了RunLoop循環運行成立的條件二。 41 */ 42 //試着在這句以前添加[[NSRunLoop currentRunLoop]run];是不能啓動子線程的RunLoop,由於此處是在main主線程上。 43 [self performSelector:@selector(threadSelector) onThread:self.thread withObject:nil waitUntilDone:NO]; 44 } 45 -(void)threadSelector//【此處運行在子線程】 46 { 47 NSLog(@"打開子線程Selector源"); 48 NSLog(@"此處是threadSelector源:%@",[NSThread currentThread]); 49 }
輸出結果:
2016-10-24 10:48:24.971 RunLoop演示[18111:752173] 打開子線程方法 2016-10-24 10:48:24.973 RunLoop演示[18111:752173] 這裏是threadMethod:<NSThread: 0x7fc830411a70>{number = 2, name = (null)} 2016-10-24 10:48:26.256 RunLoop演示[18111:752173] 這裏是threadMethod:<NSThread: 0x7fc830411a70>{number = 2, name = (null)} ........ 2016-10-24 10:48:26.260 RunLoop演示[18111:752173] 這裏是threadMethod:<NSThread: 0x7fc830411a70>{number = 2, name = (null)} 2016-10-24 10:48:26.261 RunLoop演示[18111:751978] 這裏是主線程:<NSThread: 0x7fc830402b30>{number = 1, name = main} 2016-10-24 10:48:26.261 RunLoop演示[18111:752173] 這裏是threadMethod:<NSThread: 0x7fc830411a70>{number = 2, name = (null)} 2016-10-24 10:48:26.263 RunLoop演示[18111:752173] 打開子線程Selector源 2016-10-24 10:48:26.264 RunLoop演示[18111:752173] 此處是threadSelector源:<NSThread: 0x7fc830411a70>{number = 2, name = (null)}
分析代碼:
第3行:爲何子線程thread須要用到strong屬性?
若是使用weak,子線程調用不了,子線程thread一建立就馬上銷燬了。若是咱們使用本身自定義的線程,而且重寫線程的「-(void)dealloc」方法,咱們會看到其實子線程thread一建立就調用dealloc馬上銷燬了。
19-28行:爲何要用到while?
重點:Run loop的管理並不徹底是由系統自動控制的,而是要由咱們手動顯式開啓。因此咱們在設計子線程代碼的時候,必須符合如下條件才能進入循環:
1.RunLoop處於開啓狀態;(子線程由咱們手動開啓)
2.正確響應輸入事件;
因此第一步咱們須要使用while/for語句來驅動RunLoop,以便可以進行循環。
第37行:
經過輸出線程的對象信息,咱們能夠發現,此時處於UI控件按鈕的事件其實屬於主線程main,
(在這裏有個疑問,如何把Run驅動RunLoop的代碼放在此處的話,還能不能performSelector建立事件源呢?
答案是不能的,由於此時是在主線程裏。也就是:Run的不是子線程:self.thread。所以也不會執行threadSelector方法)
第43行:
咱們在while中使RunLoop一直處在開啓的狀態,因此當建立一個Selector源時,知足條件2:RunLoop進入循環中,執行子線程的threadSelector方法,在這個RunLoop子線程處於運行循環管理中,如「while(1)」死循環通常,便不會執行後面那句輸出代碼,也便是中止輸出 「這裏是threadMethod:.........」。
(是否是相似文章開頭關於main函數的測試,當進入循環後,便不會執行後面輸出「結束」那段代碼了。區別是主線程是默認自動開啓的,而子線程的RunLoop則須要咱們手動開啓。)
測試二:mode模式與定時源的同步性
在「Main.storyboard」中進行timer事件測試。
a.添加一個用於顯示內容的名爲「textView」的文本控件,b.再添加一個名爲「addTime」的按鈕控件。
@interface ViewController () //測試一 @property (strong,nonatomic)NSThread *thread; - (IBAction)showSource:(id)sender;
//測試二 @property (weak, nonatomic) IBOutlet UITextView *textView; - (IBAction)addTime:(UIButton *)sender; @end
而後在「ViewController.m」中threadSelector方法後面添加如下代碼;
#pragma mark -- 2、Time測試 - (IBAction)addTime:(UIButton *)sender { NSTimer *timer = [NSTimer timerWithTimeInterval:0.5 target:self selector:@selector(showTimer) userInfo:nil repeats:YES];
//添加timer到RunLoop [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode]; } -(void)showTimer //【在主線程】 { NSLog(@"調用time的線程:%@",[NSThread currentThread]); [self showText:@"-------time-------"]; } #pragma mark --在文本控件textView後面增長str字符串 -(void)showText:(NSString *)str //注意:由於UI控件須要在主線程裏面,嘗試一下,若是是在子線程threadMethod方法執行此段代碼則運行報錯。 { NSString *text = self.textView.text; self.textView.text = [text stringByAppendingString:str]; }
關於mode模式:
操做:當點擊addTime按鈕後,textView控件上不斷顯示「-------time-------」,可是當咱們拖拽textView進度條上下移動時,會發現"-(void)showTime:"不會執行,textView控件上的內容再也不增長「-------time-------」,就像「卡住了,死機了」同樣。當咱們中止對textView進行拖拽後,控件上的內容又不斷添加更新了。
解決方案:修改mode類型:把默認模式NSDefaultRunLoopMode改成佔位符NSRunLoopCommonModes;
發現若是修改爲這樣,那麼即便咱們對textView進行拖拽,內容會一直增長「-------time-------」,不再會因爲拖拽而被牽制住了。
緣由:每次RunLoop只能支持一種mode。當咱們點擊addtime按鈕後,定時源(timer)加入到RunLoop中,而當滑動textView時,RunLoop自動切換成UITrackingRunLoopMode模式,定時器就中止了響應。
而NSRunLoopCommonModes等效於NSDefaultRunLoopMode和NSEventTrackingRunLoopMode兩種模式的結合
因此當咱們在帶有 「Common 」標記的NSRunLoopCommonModes模式下添加定時源(timer)後。即便咱們對textView進行滾動操做,也不會影響到內容的顯示了。
另外提一下,還有另外一種添加time的方法:
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(showTimer) userInfo:nil repeats:YES];
//使用scheduledTimerWithTimeInterval方法,會自動添加到RunLoop,因此能夠不寫如下代碼,只是會默認爲NSDefaultRunLoopMode模式 [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
關於同步:
當咱們觀察控制檯的輸出,能夠發現,其實調用 "-(void)showTimer" 輸出的是在主線程mian中。
這是由於輸入源使用傳遞異步事件,且一般消息來自於其餘線程或程序。
而定時源是在以同步方式傳遞信息的。
-----------------------------其餘補充-----------------------------
1.RunLoop輸入源的結構圖以下:
RunLoop接收輸入事件來自兩種不一樣的來源:輸入源(input source)和定時源(timer source)。
輸入源:傳遞異步事件,一般消息來自於其餘線程或程序。
輸入源有3種類型:
在測試一中,當咱們點擊按鈕後,執行UI按鈕控件的事件,此時「performSelector」一個Selector輸入源,因此,系統執行Selector方法。
2.RunLoop的內部流程的邏輯以下:
(備註:左邊黃色的地方,「source0 (port) 」改成"source1 (port)")
因此在測試一中,處於while一直進行着的語句:
[[NSRunLoop currentRunLoop]run];
每次的Run都表明着:進行一次消息輪詢,若是沒有任務須要處理的消息源,則直接返回;
---------------
本文主要闡述基本概念與應用,若是有興趣的童鞋能夠參考:
2.ibireme的文章,關於RunLoop背後的底層原理的詳解:
【http://blog.ibireme.com/2015/05/18/runloop/】
三、以及這篇關於輸入源定時源的詳解介紹: