何爲RunLoop?RunLoop有哪些應用場景?

1、RunLoop的做用

一個應用開始運行之後放在那裏,若是不對它進行任何操做,這個應用就像靜止了同樣,不會自發的有任何動做發生,可是若是咱們點擊界面上的一個按鈕,這個時候就會有對應的按鈕響應事件發生。給咱們的感受就像應用一直處於隨時待命的狀態,在沒人操做的時候它一直在休息,在讓它幹活的時候,它就能馬上響應。其實,這就是run loop的功勞。安全

2、線程與runloop的關係

  <1>線程任務的類型架構

  線程的任務能夠形象地分爲:app

    (1)直線型:執行一段任務以後,就被釋放掉了。框架

    (2)環型:不斷循環,直到經過某種方式將它終止。函數

  <2>線程與run loop的關係oop

  Run loop,正如其名,loop表示某種循環,和run放在一塊兒就表示一直在運行着的循環。實際上,run loop和線程是緊密相連的,能夠這樣說run loop是爲了線程而生,沒有線程,它就沒有存在的必要。Run loops是線程的基礎架構部分,Cocoa和CoreFundation都提供了run loop對象方便配置和管理線程的run loop(如下都已Cocoa爲例)。每一個線程,包括程序的主線程(main thread)都有與之相應的run loop對象。學習

  iOS 系統中,提供了兩種RunLoop:NSRunLoop 和 CFRunLoopRef。測試

    <1 CFRunLoopRef 是在 CoreFoundation 框架內的,它提供了純 C 函數的 API,全部這些 API 都是線程安全的。優化

    <2 NSRunLoop 是基於 CFRunLoopRef 的封裝,提供了面向對象的 API,可是這些 API 不是線程安全的。spa

    <3 CFRunLoopRef 的代碼是開源的。

  其中:主線程中的runloop是默認啓動的。

  int main(int argc, char *argv[])
 {

        @autoreleasepool {

              return UIApplicationMain(argc, argv, nil, NSStringFromClass([appDelegate class]));

        }

  }

  重點是UIApplicationMain() 函數,這個方法會爲main thread 設置一個NSRunLoop 對象。這樣就能解釋了爲何系統沒有任務執行時進行死亡狀態,有任務執行時又能進行響應。

 3、RunLoop的應用場景

  1.保持線程的存活,而不是線性的執行完任務就退出了

  <1>不開啓RunLoop的線程

  在遇到一些耗時操做時,爲了不主線程阻塞致使界面卡頓,影響用戶體驗,每每咱們會把這些耗時操做放在一個臨時開闢的子線程中。操做完成了,子線程線性的執行了代碼也就退出了,就像下面同樣。

-(void)notDidThread{
    NSLog(@"%@ -------開闢子線程",[NSThread currentThread]);
    MyThread *subThread = [[MyThread alloc]initWithTarget:self selector:@selector(subThreaddo) object:nil];
    subThread.name = @"subThread";
    [subThread start];
}
-(void)subThreaddo{
    NSLog(@"%@----執行子線程任務",[NSThread currentThread]);
}

其中MyThread是一個繼承自NSThread的子類,並重寫了dealloc方法。

-(void)dealloc
{
    NSLog(@"%@線程被釋放了", self.name);
}

看一下打印結果:

<NSThread: 0x600001a22880>{number = 1, name = main} -------開闢子線程
<MyThread: 0x600001a42640>{number = 3, name = subThread}----執行子線程任務
subThread線程被釋放了

能夠看到子線程subThread在任務執行結束後,已經被釋放掉了。

  <1>開啓RunLoop的線程  

  (1)實驗用self來持有子線程

  一樣也是上個代碼,讓self對子線程進行持有,再看輸出結果。

self.subThread = [[MyThread alloc]initWithTarget:self selector:@selector(subThreaddo) object:nil];
self.subThread.name = @"subThread";
[self.subThread start];
 <NSThread: 0x600002f9e900>{number = 1, name = main} -------開闢子線程
<MyThread: 0x600002fc2c40>{number = 3, name = subThread}----執行子線程任務

在任務執行完成以後,子線程並無被釋放掉。那既然沒有被釋放掉,若是再去從新開啓能行嗎?

self.subThread = [[MyThread alloc]initWithTarget:self selector:@selector(subThreaddo) object:nil];
self.subThread.name = @"subThread";
[self.subThread start];
[self.subThread start];//從新開啓一次
<NSThread: 0x600002cb8000>{number = 1, name = main} -------開闢子線程
<MyThread: 0x600002cd5ac0>{number = 3, name = subThread}----執行子線程任務
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[MyThread start]: attempt to start the thread again'

發現已經崩潰了。任務執行完畢後,thread雖然沒有被釋放掉,仍是處於內存中,可是它處於死亡狀態(當線程執行完畢後,都會進如到這種狀態),因此若是從新開啓會出現崩潰。蘋果在線程死亡後不容許從新開啓。

  <2>初步嘗試使用RunLoop

  如今咱們來初步瞭解下RunLoop如何使用,順便作個小測試。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSLog(@"%@----開闢子線程",[NSThread currentThread]);
    
    NSThread *subThread = [[MyThread alloc] initWithTarget:self selector:@selector(subThreadTodo) object:nil];
    subThread.name = @"subThread";
    [subThread start];
    
}
 
- (void)subThreadTodo
{
    NSLog(@"%@----開始執行子線程任務",[NSThread currentThread]);
    //獲取當前子線程的RunLoop
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    //下面這一行必須加,不然RunLoop沒法正常啓用。咱們暫時先無論這一行的意思,稍後再講。
    [runLoop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];
    //讓RunLoop跑起來
    [runLoop run];
    NSLog(@"%@----執行子線程任務結束",[NSThread currentThread]);

  查看輸出結果:

<NSThread: 0x600002621400>{number = 1, name = main} -------開闢子線程
<MyThread: 0x600002677ec0>{number = 3, name = subThread}----執行子線程任務

  這裏沒有對線程進行引用,也沒有讓線程內部的任務進行顯式的循環。爲何子線程的裏面的任務沒有執行到輸出任務結束這一步,爲何子線程沒有銷燬?就是由於[runLoop run];這一行的存在。

  RunLoop本質就是個Event Loop的do while循環,因此運行到這一行之後子線程就一直在進行接受消息->等待->處理的循環。因此不會運行[runLoop run];以後的代碼(這點須要注意,在使用RunLoop的時候若是要進行一些數據處理之類的要放在這個函數以前不然寫的代碼不會被執行),也就不會由於任務結束致使線程死亡進而銷燬。

  <3>如何建立RunLoop?

  蘋果不容許直接建立 RunLoop,它只提供了四個自動獲取的函數

[NSRunLoop currentRunLoop];//獲取當前線程的RunLoop
[NSRunLoop mainRunLoop];//獲取主線程的RunLoop
CFRunLoopGetMain();
CFRunLoopGetCurrent();

  函數內部的邏輯大概是下面這樣:

/// 全局的Dictionary,key 是 線程, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 訪問 loopsDic 時的鎖
static CFSpinLock_t loopsLock;
 
/// 獲取一個 pthread 對應的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
    OSSpinLockLock(&loopsLock);
    
    if (!loopsDic) {
        // 第一次進入時,初始化全局Dic,並先爲主線程建立一個 RunLoop。
        loopsDic = CFDictionaryCreateMutable();
        CFRunLoopRef mainLoop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
    }
    
    /// 直接從 Dictionary 裏獲取。
    CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
     
    if (!loop) {
        /// 取不到時,建立一個
        loop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, thread, loop);
        /// 註冊一個回調,當線程銷燬時,順便也銷燬其對應的 RunLoop。
        _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
    }
  
    OSSpinLockUnLock(&loopsLock);
    return loop;
}
 
CFRunLoopRef CFRunLoopGetMain() {
    return _CFRunLoopGet(pthread_main_thread_np());
}
 
CFRunLoopRef CFRunLoopGetCurrent() {
    return _CFRunLoopGet(pthread_self());
}

  注:這並非源碼,而是大神爲了方便咱們理解,對源碼進行了一些可讀性優化後的結果。

  一、線程默認不開啓RunLoop,爲何咱們的App或者說主線程卻能夠一直運行而不會結束?

主線程是惟一一個例外,當App啓動之後主線程會自動開啓一個RunLoop來保證主線程的存活並處理各類事件。並且從上面的源代碼來看,任意一個子線程的RunLoop都會保證主線程的RunLoop的存在。

  二、RunLoop能正常運行的條件是什麼?

看到剛纔代碼中註釋說暫時無論的代碼,第一次接觸確定會想[runLoop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];這一句是什麼意思?爲何必須加這一句RunLoop才能正常運行?

- (void)viewDidLoad {
    [super viewDidLoad];
     
    NSLog(@"%@----開闢子線程",[NSThread currentThread]);
    
    NSThread *subThread = [[MyThread alloc] initWithTarget:self selector:@selector(subThreadTodo) object:nil];
    subThread.name = @"subThread";
    [subThread start];
  
}
 
- (void)subThreadTodo
{
    NSLog(@"%@----開始執行子線程任務",[NSThread currentThread]);
    //獲取當前子線程的RunLoop
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    //註釋掉下面這行和不註釋掉下面這行分別運行一次
    [runLoop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];
    NSLog(@"RunLoop:%@",runLoop);
    //讓RunLoop跑起來
    [runLoop run];
    NSLog(@"%@----執行子線程任務結束",[NSThread currentThread]);
}

   註釋掉獲得的結果

  不註釋獲得的結果

  註釋掉之後咱們看似run了RunLoop可是最後線程仍是結束了任務,而後銷燬了。與沒註釋獲得的結果比較,形成這一切的緣由就在上面兩張圖片中標註部分的區別上。要解釋這一部分就又要開始講到讓咱們抓耳撓腮的概念部分,咱們先來看一張眼熟到不行的RunLoop結構圖。

  一開始接觸RunLoop我看到這張圖的時候也是懵逼的,如今咱們結合剛纔的打印結果來理解。

  1. 圖中RunLoop藍色部分就對應咱們打印結果中,整個RunLoop部分的打印結果

  2. 多個綠色部分共同被包含在RunLoop內就對應,打印結果中modes中同時包含多個Mode(這裏但是看打印結果中標註出來的第一行往上再數兩行。modes = ... count = 1。一個RunLoop能夠包含多個Mode,每一個Mode的Name不同,只是在這個打印結果當中目前恰好Mode個數爲1)

  3. 每個綠色部分Mode總體就對應,打印結果中被標註出來的總體。

  4. 黃色部分Source對應標註部分source0+source1

  5. 黃色部分Observer對應標註部分observer部分

  6. 黃色部分Timer對應標註部分timers部分

  <1 Mode

  我對Mode的理解就是」行爲模式「,就像咱們說到上學這個行爲模式,它就應該包含起牀,出門,去學校,上課,午休等等。可是,若是上學這個行爲模式什麼都不包含,那麼即便咱們進行上學這個行爲,咱們也一直睡在牀上什麼都不會作。就像剛纔註釋掉addPort那一行代碼獲得的結果同樣,RunLoop在kCFRunLoopDefaultMode下run了,可是由於該Mode下全部東西都爲null(不包含任何內容),因此RunLoop什麼都沒作又退出來了,而後線程就結束任務最後銷燬。之因此要有Mode的存在是爲了讓RunLoop在不一樣的」行爲模式「之下執行不一樣的」動做「互不影響。好比執行上學這個行爲模式就不能進行娛樂這個行爲模式下的遊戲這個動做。RunLoop同一時間只能運行在一種Mode下,當前運行的這個Mode叫currentMode。(這裏也許比較抽象,在下面timer部分會有實例結合實例分析。)

  通常咱們經常使用的Mode有三種

1.kCFRunLoopDefaultMode(CFRunLoop)/NSDefaultRunLoopMode(NSRunLoop)
默認模式,在RunLoop沒有指定Mode的時候,默認就跑在DefaultMode下。通常狀況下App都是運行在這個mode下的
 
2.(CFStringRef)UITrackingRunLoopMode(CFRunLoop)/UITrackingRunLoopMode(NSRunLoop)
通常做用於ScrollView滾動的時候的模式,保證滑動的時候不受其餘事件影響。
 
3.kCFRunLoopCommonModes(CFRunLoop)/NSRunLoopCommonModes(NSRunLoop)
這個並非某種具體的Mode,而是一種模式組合,在主線程中默認包含了NSDefaultRunLoopMode和 UITrackingRunLoopMode。子線程中只包含NSDefaultRunLoopMode。
注意:
①在選擇RunLoop的runMode時不能夠填這種模式不然會致使RunLoop運行不成功。
②在添加事件源的時候填寫這個模式就至關於向組合中全部包含的Mode中註冊了這個事件源。
③你也能夠經過調用CFRunLoopAddCommonMode()方法將自定義Mode放到 kCFRunLoopCommonModes組合。

  <2 Source

  source就是輸入源事件,分爲source0和source1這兩種。

1.source0:諸如UIEvent(觸摸,滑動等),performSelector這種須要手動觸發的操做。
2.source1:處理系統內核的mach_msg事件(系統內部的端口事件)。諸如喚醒RunLoop或者讓RunLoop進入休眠節省資源等。
通常來講平常開發中咱們須要關注的是source0,source1只須要了解。
之因此說source0更重要是由於平常開發中,咱們須要對常駐線程進行操做的事件大多都是source0,稍後的實驗會講到。

  <3 Timer

  Timer即爲定時源事件。通俗來說就是咱們很熟悉的NSTimer,其實NSTimer定時器的觸發正是基於RunLoop運行的,因此使用NSTimer以前必須註冊到RunLoop,可是RunLoop爲了節省資源並不會在很是準確的時間點調用定時器,若是一個任務執行時間較長,那麼當錯過一個時間點後只能等到下一個時間點執行,並不會延後執行(NSTimer提供了一個tolerance屬性用於設置寬容度,若是確實想要使用NSTimer而且但願儘量的準確,則能夠設置此屬性)。

  <4 Observer

  它至關於消息循環中的一個監聽器,隨時通知外部當前RunLoop的運行狀態。NSRunLoop沒有相關方法,只能經過CFRunLoop相關方法建立

 // 建立observer
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
 
        NSLog(@"----監聽到RunLoop狀態發生改變---%zd", activity);
 
    });
 
    // 添加觀察者:監聽RunLoop的狀態
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);

  因爲它與這一問的關係並不大因此暫時不作過多闡述,但願進一步瞭解Observer能夠查看文末的文檔或者RunLoop入門學習補充資料(3.Observer)。

  重點:它不能做爲讓RunLoop正常運行的條件,只有Observer的RunLoop也是沒法正常運行的。

相關文章
相關標籤/搜索