【本次開發環境: Xcode:7.2 iOS Simulator:iphone6 By:啊左 本文Demo下載鏈接:RunLoop-Demo】 基本概念 一、RunLoop簡介 RunLoop,跑圈。在iOS開發中,也就是運行迴圈。 在應用需要的時候自己跑起來運行,在用戶沒有操作的時候就停下來休息。 ...
【本次開發環境: Xcode:7.2 iOS Simulator:iphone6 By:啊左 本文Demo下載鏈接:RunLoop-Demo】
-----------------------------基本概念-----------------------------
一、RunLoop簡介
RunLoop,跑圈。在iOS開發中,也就是運行迴圈。
在應用需要的時候自己跑起來運行,在用戶沒有操作的時候就停下來休息。充分節省CPU資源,提高程式性能。
二. RunLoop的概念與作用
概念:一般來講,一個線程一次只能執行一個任務,執行完成後線程就會退出。但是有時候我們需要線程能夠一直“待命”隨時處理事件而不退出,這就需要一個機制來完成這樣的任務。
這樣一種機制的代碼邏輯如下:
function loop() {
initialize();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}
這種模型通常被稱作 Event Loop。 Event Loop 在很多系統和框架里都有實現。而實現這種模型的關鍵點在於:如何管理事件/消息,如何讓線程在沒有處理消息時休眠以避免資源占用、在有消息到來時立刻被喚醒。
例如一個應用放那裡,不進行操作就像靜止休息一樣,點擊按鈕,就有響應,就像“隨時待命”一樣,這就是RunLoop的功勞。
所以RunLoop 實際上就是一個對象,這個對象管理了其需要處理的事件和消息,並提供了一個入口函數來執行RunLoop 的邏輯。
線程開始這個函數之後,便一直會處於此函數 "接受消息->等待->處理" 的迴圈中:(有事:做出反應; 木事:休眠省電; 再次有事:重新喚醒、處理事件。)
直到這個迴圈結束(比如傳入 quit 的消息),最後函數返回。
作用:
- 保持程式持續運行:例如程式一啟動就會開一個主線程,主線程一開起來就會跑一個主線程對應的RunLoop,RunLoop保證主線程不會被銷毀,也就保證了程式的持續運行;
- 處理App中的各種事件(比如:觸摸事件,定時器事件,Selector事件等 );
- 節省CPU資源,優化程式性能:程式運行起來時,當什麼操作都沒有做的時候,RunLoop就通知系統,現在沒有事情做,然後進行休息待命狀態,這時系統就會將其資源釋放出來去做其他的事情。當有事情做,也就是一有響應的時候RunLoop就會立馬起來去做事情;
RunLoop,最重要的作用,也就是用來管理線程的。可以說,沒有線程,也就沒有RunLoop的存在必要。
當線程的RunLoop一開啟,RunLoop便開始對線程進行管理工作:在線程執行完任務後,線程便會進入休眠狀態,並且不會退出,隨時等待新的任務。
三、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,我們可以做一下驗證,
1,在UIApplicationMain函數前後加入一段輸出代碼:
點擊運行,輸出“開始”,卻始終沒有運行到“結束”這個輸出,那是因為當運行完 "NSLog(@"開始");"這一段代碼後,系統運行UIApplicationMain函數,並且進入了:
主線程的運行迴圈,RunLoop使得主線程一直處在迴圈中而不會跳出來進入下一段執行。
(並且,可以看到Xcode會對 “NSLog(@"結束");” 這段代碼警告“Code will never be executed”,提示代碼不會執行到這一步。)
2,在“Main.storyboard”中隨意放置幾個按鈕控制項,main.m文件代碼再次修改如下:
int main(int argc, char * argv[]) { @autoreleasepool { NSLog(@"開始"); return 0; } }
點擊運行,輸出“開始”後,模擬器界面也是一片空白。“stop”按鈕也點不下去了:;
因為當輸出“開始”後,“return 0”,之後沒有進入主線程運行迴圈,程式一啟動就結束了,控制項與其他程式有關的都沒有執行,所以界面空白。
說明瞭在UIApplicationMain函數中,開啟了一個和主線程相關的RunLoop,導致UIApplicationMain不會返回,一直在運行中,也就保證了程式的持續運行。
這也是為什麼應用能夠在我們無任何操作時休息,在我們進行操作的時候又能夠立刻進行響應活動,恰恰因為應用處於RunLoop的“等待命令”的狀態。
四、RunLoop對象與相關類。
對象:
從RunLoop的概念,我們可以知道RunLoop 實際上就是一個管理著線程對象。那麼,如何獲取RunLoop對象呢?
Foundation框架中:
[NSRunLoop currentRunLoop]; // 獲得當前線程的RunLoop對象
[NSRunLoop mainRunLoop]; // 獲得主線程的RunLoop對象
Core Foundation框架中:
CFRunLoopGetCurrent(); // 獲得當前線程的RunLoop對象
CFRunLoopGetMain(); // 獲得主線程的RunLoop對象
文檔中的相關類:
CFRunLoopRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopModeRef
CFRunLoopObserverRef
他們的關係如下圖:
- 1.一個 RunLoop 包含若幹個 Mode,而每個 Mode 又包含若幹個 Source/Timer/Observer。
- 2.RunLoop每次只能指定一種Mode。而且如果需要切換 Mode,只能退出當前 Loop。這樣做主要是為了分隔開不同組的 Source/Timer/Observer,讓其互不影響。
- 3.如果一個 mode 中一個 “Source/Timer/Observer” 都沒有,則 RunLoop 會直接退出,不進入迴圈。
CFRunLoopSourceRef 輸入源
是事件產生的地方,函數調用棧上Source有兩個版本:Source0 和 Source1。
- Source0:非基於埠port,例如觸摸,滾動,selector選擇器等用戶觸發的事件;(只包含了一個回調函數,它並不能主動觸發事件)
- Source1:基於埠port,一些系統事件; (包含了一個 mach_port 和一個回調函數,被用於通過內核和其他線程相互發送消息。能主動喚醒 RunLoop 的線程)
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 };
-----------------------------例子-----------------------------
測試一、二的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 //如果要測試“二、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語句來驅動run loop,以便能夠進行迴圈。
第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 -- 二、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種類型:
- Selector源:如例子按鈕事件中的performSelector,當在子線程中執行Selector時,目標線程必須RunLoop處於開啟狀態,不然Selector就一直處於休眠狀態;
- 基於埠的輸入源:就是之前提到的Source1。通過內置的埠相關的對象和函數,創建配置基於埠的輸入源。 例如可以使用NSPort的方法把該埠添加到 RunLoop;
- 自定義輸入源:創建custom輸入源,必須使用Core Foundation裡面的CFRunLoopSourceRef類型相關的函數來創建,並自定義自己的行為和消息傳遞機制;
在測試一中,當我們點擊按鈕後,執行UI按鈕控制項的事件,此時“performSelector”一個Selector輸入源,所以,系統執行Selector方法。
2.RunLoop的內部流程的邏輯如下:
所以在測試一中,處於while一直進行著的語句:
[[NSRunLoop currentRunLoop]run];
每次的Run都代表著:進行一次消息輪詢,如果沒有任務需要處理的消息源,則直接返回;
---------------
本文主要闡述基本概念與應用,如果有興趣的童鞋可以:
2.參考ibireme的文章,關於RunLoop背後的底層原理的詳解:
【http://blog.ibireme.com/2015/05/18/runloop/】
3、以及這篇關於輸入源定時源的詳解介紹:
【http://blog.csdn.net/ztp800201/article/details/9240913】