1. 程式人生 > >何為RunLoop?RunLoop有哪些應用場景?

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

一、RunLoop的作用

一個應用開始執行以後放在那裡,如果不對它進行任何操作,這個應用就像靜止了一樣,不會自發的有任何動作發生,但是如果我們點選介面上的一個按鈕,這個時候就會有對應的按鈕響應事件發生。給我們的感覺就像應用一直處於隨時待命的狀態,在沒人操作的時候它一直在休息,在讓它幹活的時候,它就能立刻響應。其實,這就是run loop的功勞。

二、執行緒與runloop的關係

  <1>執行緒任務的型別

  執行緒的任務可以形象地分為:

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

    (2)環型:不斷迴圈,直到通過某種方式將它終止。

  <2>執行緒與run loop的關係

  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 不是執行緒安全的。

    <3 CFRunLoopRef 的程式碼是開源的。

  其中:主執行緒中的runloop是預設啟動的。

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

        @autoreleasepool {

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

  重點是UIApplicationMain() 函式,這個方法會為main thread 設定一個NSRunLoop 物件。這樣就能解釋了為什麼系統沒有任務執行時進行死亡狀態,有任務執行時又能進行響應。

 三、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());
}

  注:這並不是原始碼,而是大神為了方便我們理解,對原始碼進行了一些可讀性優化後的結果。

  1、執行緒預設不開啟RunLoop,為什麼我們的App或者說主執行緒卻可以一直執行而不會結束?

主執行緒是唯一一個例外,當App啟動以後主執行緒會自動開啟一個RunLoop來保證主執行緒的存活並處理各種事件。而且從上面的原始碼來看,任意一個子執行緒的RunLoop都會保證主執行緒的RunLoop的存在。

  2、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也是無法正常執行的。