1. 程式人生 > >iOS學習——淺談RunLoop

iOS學習——淺談RunLoop

  RunLoop的字面意思是執行迴圈、跑圈,一個App啟動後能一直執行,就是因為啟動後進入了一個迴圈,在這個迴圈中不斷監聽各種狀態、手勢動作,並做出相應的響應。這個迴圈就是我們今天要探究的RunLoop。

1 RunLoop基礎

1.1 RunLoop的基本作用

  • 保持程式的持續執行(ios程式為什麼能一直活著不會死)
  • 處理app中的各種事件(比如觸控事件、定時器事件【NSTimer】、selector事件【選擇器·performSelector···】)
  • 節省CPU資源,提高程式效能,有事情就做事情,沒事情就休息

1.2 關於RunLoop的幾點說明

  1. 如果沒有Runloop,那麼程式一啟動就會退出,什麼事情都做不了。 
  2. 如果有了Runloop,那麼相當於在內部有一個死迴圈,能夠保證程式的持續執行
  3. main函式中的Runloop 
    • a 在UIApplication函式內部就啟動了一個Runloop 該函式返回一個int型別的值 
    • b 這個預設啟動的Runloop是跟主執行緒相關聯的

1.3 RunLoop物件

  1. 在iOS開發中有兩套api來訪問Runloop
    • foundation框架【NSRunloop】 
    • core foundation框架【CFRunloopRef】
  2. NSRunLoop和CFRunLoopRef都代表著RunLoop物件,它們是等價的,可以互相轉換
  3. NSRunLoop是基於CFRunLoopRef的一層OC包裝,所以要了解RunLoop內部結構,需要多研究CFRunLoopRef層面的API(Core Foundation層面)

1.4 RunLoop與執行緒

  1. Runloop和執行緒的關係:
    • 一個Runloop對應著一條唯一的執行緒
    • 問題:如何讓子執行緒不死 回答:給這條子執行緒開啟一個Runloop
  2. Runloop的建立:主執行緒Runloop已經建立好了,子執行緒的runloop需要手動建立
  3. Runloop的生命週期:在第一次獲取時建立,線上程結束時銷燬

1.5 獲取Runloop物件

/*
1.獲得當前Runloop物件*/ //01 NSRunloop NSRunLoop * runloop1 = [NSRunLoop currentRunLoop]; //02 CFRunLoopRef CFRunLoopRef runloop2 = CFRunLoopGetCurrent(); /*2.拿到當前應用程式的主Runloop(主執行緒對應的Runloop)*/ //01 NSRunloop NSRunLoop * runloop1 = [NSRunLoop mainRunLoop]; //02 CFRunLoopRef CFRunLoopRef runloop2 = CFRunLoopGetMain(); /*3.注意點:開一個子執行緒建立runloop,不是通過alloc init方法建立,而是直接通過呼叫currentRunLoop方法來建立,它本身是一個懶載入的。 4.在子執行緒中,如果不主動獲取Runloop的話,那麼子執行緒內部是不會建立Runloop的。可以下載CFRunloopRef的原始碼,搜尋_CFRunloopGet0,檢視程式碼。 5.Runloop物件是利用字典來進行儲存,而且key是對應的執行緒Value為該執行緒對應的Runloop。*/

2 RunLoop相關類

2.1 Runloop執行原理圖

  線上程中開啟RunLoop後,系統會進入一個死迴圈,這個迴圈在有事件觸發時(觸控事件、定時器事件【NSTimer】、selector事件【選擇器·performSelector···】等)就工作,沒事情就休息,提高程式效能,節省CPU資源,示意圖如下。

2.2 RunLoop相關的5個類

  • CFRunloopRef
  • CFRunloopModeRef【Runloop的執行模式】
  • CFRunloopSourceRef【Runloop要處理的事件源】
  • CFRunloopTimerRef【Timer事件】
  • CFRunloopObserverRef【Runloop的觀察者(監聽者)】

  Runloop要想跑起來,它的內部必須要有一個mode,這個mode裡面必須有source\observer\timer,至少要有其中的一個。

2.2.1 CFRunloopModeRef

  1. CFRunloopModeRef代表著Runloop的執行模式
  2. 一個Runloop中可以有多個mode,一個mode裡面又可以有多個source\observer\timer等等
  3. 每次runloop啟動的時候,只能指定一個mode,這個mode被稱為該Runloop的當前mode
  4. 如果需要切換mode,只能先退出當前Runloop,再重新指定一個mode進入,這樣做主要是為了分割不同組的定時器等,讓他們相互之間不受影響
  5. 系統預設註冊了5個mode
    • a.kCFRunLoopDefaultMode:App的預設Mode,通常主執行緒是在這個Mode下執行
    • b.UITrackingRunLoopMode:介面跟蹤 Mode,用於 ScrollView 追蹤觸控滑動,保證介面滑動時不受其他 Mode 影響
    • c.UIInitializationRunLoopMode: 在剛啟動 App 時第進入的第一個 Mode,啟動完成後就不再使用
    • d.GSEventReceiveRunLoopMode: 接受系統事件的內部 Mode,通常用不到
    • e.kCFRunLoopCommonModes: 這是一個佔位用的Mode,不是一種真正的Mode

2.2.2  CFRunloopTimerRef:基於時間觸發一個操作。基本上說的就是NSTimer

  NSTimer在實際開發中會出現不準的情況,出現這種情況的主要是NSTimer的初始化有兩種方法如下,然後第一種方法會自動新增到當前的RunLoop中,並且RunLoop的執行模式mode設定為kCFRunLoopDefaultMode,這種模式在介面被拖拽時執行mode變為UITrackingRunLoopMode,這時候defaultmode下的定時器就會停止工作,所以在介面拖拽時定時器不計時,導致計時不準。

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;

  解決上述所說的定時器不準的方案是設定RunLoop的工作mode為kCFRunLoopCommonModes,這種模式可以在多種mode下都進行工作。

/*
    說明:
    (1)runloop一啟動就會選中一種模式,當選中了一種模式之後其它的模式就都不鳥。一個mode裡面可以新增多個NSTimer,也就是說以後當建立NSTimer的時候,可以指定它是在什麼模式下執行的。
    (2)它是基於時間的觸發器,說直白點那就是時間到了我就觸發一個事件,觸發一個操作。基本上說的就是NSTimer
    (3)相關程式碼
*/
- (void)timer2 {
    //NSTimer 呼叫了scheduledTimer方法,那麼會自動新增到當前的runloop裡面去,而且runloop的執行模式kCFRunLoopDefaultMode
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];

    //更改模式
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}

- (void)timer1 {
    //    [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];

    //定時器新增到UITrackingRunLoopMode模式,一旦runloop切換模式,那麼定時器就不工作
    //    [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

    //定時器新增到NSDefaultRunLoopMode模式,一旦runloop切換模式,那麼定時器就不工作
    //    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

    //佔位模式:common modes標記
    //被標記為common modes的模式 kCFRunLoopDefaultMode  UITrackingRunLoopMode
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

    //    NSLog(@"%@",[NSRunLoop currentRunLoop]);
}

- (void)run {
    NSLog(@"---run---%@",[NSRunLoop currentRunLoop].currentMode);
}

- (IBAction)btnClick {
    NSLog(@"---btnClick---");
}

 GCD中的定時器的使用

//0.建立一個佇列
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

    //1.建立一個GCD的定時器
    /*
     第一個引數:說明這是一個定時器
     第四個引數:GCD的回撥任務新增到那個佇列中執行,如果是主佇列則在主執行緒執行
     */
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);

    //2.設定定時器的開始時間,間隔時間以及精準度

    //設定開始時間,三秒鐘之後呼叫
    dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW,3.0 *NSEC_PER_SEC);
    //設定定時器工作的間隔時間
    uint64_t intevel = 1.0 * NSEC_PER_SEC;

    /*
     第一個引數:要給哪個定時器設定
     第二個引數:定時器的開始時間DISPATCH_TIME_NOW表示從當前開始
     第三個引數:定時器呼叫方法的間隔時間
     第四個引數:定時器的精準度,如果傳0則表示採用最精準的方式計算,如果傳大於0的數值,則表示該定時切換i可以接收該值範圍內的誤差,通常傳0
     該引數的意義:可以適當的提高程式的效能
     注意點:GCD定時器中的時間以納秒為單位(面試)
     */

    dispatch_source_set_timer(timer, start, intevel, 0 * NSEC_PER_SEC);

    //3.設定定時器開啟後回撥的方法
    /*
     第一個引數:要給哪個定時器設定
     第二個引數:回撥block
     */
    dispatch_source_set_event_handler(timer, ^{
        NSLog(@"------%@",[NSThread currentThread]);
    });

    //4.執行定時器
    dispatch_resume(timer);

    //注意:dispatch_source_t本質上是OC類,在這裡是個區域性變數,需要強引用
    self.timer = timer;

 2.2.3 CFRunloopSourceRef

CFRunloopSourceRef是事件源也就是輸入源,有兩種分類模式;一種是按照蘋果官方文件進行劃分的,另一種是基於函式的呼叫棧來進行劃分的(source0和source1)。

(1)以前的分法

    • Port-Based Sources
    • Custom Input Sources Cocoa
    • Perform Selector Sources

(2)現在的分法 Source0:非基於Port的; Source1:基於Port的

可以通過打斷點的方式檢視一個方法的函式呼叫棧

2.2.4 CFRunLoopObserverRef

(1)CFRunLoopObserverRef是觀察者,能夠監聽RunLoop的狀態改變

(2)如何監聽

//建立一個runloop監聽者
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(),kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {

        NSLog(@"監聽runloop狀態改變---%zd",activity);
    });

    //為runloop新增一個監聽者
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);

    CFRelease(observer);

(3)監聽的狀態

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),   //即將進入Runloop
    kCFRunLoopBeforeTimers = (1UL << 1),    //即將處理NSTimer
    kCFRunLoopBeforeSources = (1UL << 2),   //即將處理Sources
    kCFRunLoopBeforeWaiting = (1UL << 5),   //即將進入休眠
    kCFRunLoopAfterWaiting = (1UL << 6),    //剛從休眠中喚醒
    kCFRunLoopExit = (1UL << 7),            //即將退出runloop
    kCFRunLoopAllActivities = 0x0FFFFFFFU   //所有狀態改變
};

3 RunLoop的執行邏輯