iOS開發-- RunLoop的基本概念與例子分析

看了一下,上一篇貌似5個月前的😅。
最近公司忙着開發一個cordova的項目,本身也是邊工做邊找一些資料學習,都沒怎麼關注博客上的內容...呃,主要仍是懶癌發做吧😌。爭取多寫寫博客,記錄記錄點滴,也但願無論技能、生活仍是職業生涯上都能不斷成長,共勉~
這篇是關於RunLoop的筆記的整理和一點看法。php

本次開發環境: Xcode:7.2     iOS Simulator:iphone6   By:啊左     本文Demo下載連接:RunLoop-Demohtml

 

-----------------------------基本概念-----------------------------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 的消息),最後函數返回。

做用:

  1. 保持程序持續運行:例如程序一啓動就會開一個主線程,主線程一開起來就會跑一個主線程對應的RunLoop,RunLoop保證主線程不會被銷燬,也就保證了程序的持續運行;
  2. 處理App中的各類事件(好比:觸摸事件,定時器事件,Selector事件等 );
  3. 節省CPU資源,優化程序性能:程序運行起來時,當什麼操做都沒有作的時候RunLoop就通知系統,如今沒有事情作,而後進行休息待命狀態,這時系統就會將其資源釋放出來去作其餘的事情。當有事情作,也就是一有響應的時候RunLoop就會立馬起來去作事情;

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對象呢?

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 };


-----------
------------------例子-----------------------------

測試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等效於NSDefaultRunLoopModeNSEventTrackingRunLoopMode兩種模式的結合

因此當咱們在帶有 「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的內部流程的邏輯以下:

(備註:左邊黃色的地方,「source0 (port) 」改成"source1 (port)")

 

 

因此在測試一中,處於while一直進行着的語句:

[[NSRunLoop currentRunLoop]run];

每次的Run都表明着:進行一次消息輪詢,若是沒有任務須要處理的消息源,則直接返回;

 

---------------

本文主要闡述基本概念與應用,若是有興趣的童鞋能夠參考:

1.RunLoop的官方文檔

2.ibireme的文章,關於RunLoop背後的底層原理的詳解:

http://blog.ibireme.com/2015/05/18/runloop/

三、以及這篇關於輸入源定時源的詳解介紹:

http://blog.csdn.net/ztp800201/article/details/9240913

相關文章
相關標籤/搜索