ObjC RunLoop簡析

RunLoop,顧名思義就是運行循環的意思,是指程序在運行過程當中循環作一些事情。api

RunLoop 簡介

當咱們建立一個terminal項目的時候,此時的main函數中並無一個RunLoop。因此程序運行完main函數以後就退出了。數組

沒有RunLoop

而一個iOS的application程序,默認在主線程開啓了一個RunLoop,這樣一個App就能夠處理一些計時器事件,滑動事件等,不會立刻退出。markdown

有RunLoop

在iOS項目中每一條線程都對應着一個RunLoop對象,RunLoop存放在一個以線程做爲key的散列表中。app

主線程在建立的時候默認開啓RunLoop,而其餘子線程默認不開啓,可是會在第一次獲取RunLoop([NSRunLoop currentRunLoop]或者CFRunLoopGetCurrent())的時候建立。框架

通常狀況下RunLoop的生命週期跟隨線程,線程結束的時候RunLoop也會被銷燬。函數

iOS中提供了一套Foundation框架的NSRunLoop api和一套基於Core Foundation的CFRunLoopRef api來使用RunLoop。其中NSRunLoop是基於CFRunLoopRef作了一層OC的封裝。oop

RunLoop 結構

在CFRunLoop的源碼中RunLoop的基本結構以下:atom

CFRunLoopRef

CFRunLoopRef是一個__CFRunLoop的結構體,結構體中存放了許多mode相關的成員。spa

其中_currentModeCFRunLoopModeRef類型的,它是一個__CFRunLoopMode類型的結構體指針。RunLoop經過它來表徵RunLoop的運行狀態。線程

一個RunLoop中包含有許多Mode。_commonModes是一個可變的集合,集合中存放了許多mode。RunLoop在運行的時候只能選擇一個Mode做爲當前RunLoop執行的狀態,也就是_currentMode

mode是CFRunLoopMode類型的。而CFRunLoopMode是經過typedf__CFRunLoopMode獲得的。__CFRunLoopMode中存放了處理觸摸事件的source0、系統時間捕捉的source1、處理計時器的timers、監聽RunLoop狀態的observer等。

另外,若是RunLoop須要切換運行狀態的時候必須先退出當前的Mode,才能進入新的Mode。若是當前Mode中全部的sourcetimerobserver的時候RunLoop就會馬上退出。

常見的RunLoopMode有默認的modekCFRunLoopDefaultMode、跟蹤界面(好比:保證滑動不受其餘mode影響)的UITrackingRunLoopMode。另外在api中還有一個kCFRunLoopCommonModes可是這並非一個真正的mode,它不存在於_commonModes中,它只是一個標記。

RunLoop 的監聽器會監聽RunLoop的一些狀態:

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0), // 即將進入runloop
    kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理計時器
    kCFRunLoopBeforeSources = (1UL << 2), // 即將處理source
    kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進入休眠
    kCFRunLoopAfterWaiting = (1UL << 6), // 即將從休眠中喚醒
    kCFRunLoopExit = (1UL << 7), // 即將退出RunLoop
    kCFRunLoopAllActivities = 0x0FFFFFFFU // 所有狀態
};
複製代碼

探究RunLoop的執行流程

咱們能夠經過Xcode自帶的lldb 經過bt命令查看函數調用棧找到RunLoop的入口函數

RunLoop的入口函數

CFRunLoop.c文件中找到該函數,咱們發現它經過__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry)監聽進入RunLoop。接着有一個__CFRunLoopRun的函數調用,該函數中封裝了RunLoop處理事件的邏輯。

咱們只關注__CFRunLoopRun主要代碼:咱們發現該函數中存在着一個do-while()循環,當retVal==0的時候循環持續進行,當retVal != 0的時候,回返回給函數CFRunLoopRunSpecific,它調用__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);退出RunLoop。

__CFRunLoopRun中主要的流程都在下圖中進行了描述。總結來講:

  1. 首先會通知監聽者Observers:即將處理Timers
  2. 通知監聽者Observers:即將處理Sources
  3. 處理blocks
  4. 處理source0,若是處理完了會再次處理blocks
  5. 若是存在source1,則跳到handle_msg處理,若是沒有則通知監聽器即將進入休眠
  6. 休眠時期等待消息來喚醒當前線程
  7. 若是有消息喚醒則進入handle_msg處理計時器,gcd,source1這些信息
  8. 再次處理blocks
  9. 獲取返回值retVal
  10. 進入do-while(),若是retVal == 0 則循環持續進行。不然返回給CFRunLoopRunSpecific函數,退出RunLoop

RunLoop的執行流程

RunLoop的應用

Timer

當咱們使用+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;建立一個計時器,而且頁面存在scrollView的時候。滑動scrollView計時器就會中止運行。這是由於一開始runloop存在於NSDefaultRunLoopMode,當滑動事件響應的時候runloop會進入UITrackingRunLoopMode模式處理滑動事件,全部timer就會失去處理。

咱們能夠使用+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;建立timer,而後將它放在一個NSRunLoopCommonModes標記的模式下進行工做,timer是能夠t在_commonModes數組中存放的模式下工做的。這樣就解決了滑動事件和timer計時器事件衝突的問題。

線程保活

當咱們建立一條線程的時候,這條線程並無一個RunLoop。當咱們第一次獲取RunLoop的時候這條線程中才會建立RunLoop。

因此建立一條一直存在的線程,咱們須要在線程中加入一個不會被回收的RunLoop,也就是讓do-while()一直存在,也就是RunLoop一直有事情在處理,而retVal不會爲不是0的其餘值。

實例代碼:

#import "SoCPermanentThread.h"
@interface SoCPermanentThread ()

@property (nonatomic, strong) NSThread *thread;

@end

@implementation SoCPermanentThread

- (instancetype)init {
    if (self = [super init]) {
        self.thread = [[NSThread alloc] initWithBlock:^{
            CFRunLoopSourceContext context = {0};
            CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
            CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
            CFRelease(source);
            CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false);
        }];
        [self.thread start];
    }
    return self;
}

- (void)executeTask:(SoCPermenantThreadTask)task {
    if (!_thread || !task) return;
    [self performSelector:@selector(__task:) onThread:_thread withObject:task waitUntilDone:NO];
}

- (void)stop {
    if (!_thread) return;
    [self performSelector:@selector(__stop) onThread:_thread withObject:nil waitUntilDone:YES];
}

- (void)__task:(SoCPermenantThreadTask)task {
    task();
}

- (void)__stop {
    CFRunLoopStop(CFRunLoopGetCurrent());
    _thread = nil;
}

- (void)dealloc {
    [self stop];
}

@end
複製代碼

上述代碼使用Core Foundation實現的線程保活,其中重要的就是首先往RunLoop中添加Source保證RunLoop有事情能夠作,另外就是CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false);這個方法的最後一個參數BOOL參數returnAfterSourceHandled,其值爲flase表明執行完函數(處理完source)不會返回,而true則相反,表示執行完函數(處理完source)會當即返回。

總結

本篇主要以Core Foundation api 爲基礎(Core Foundation開源)簡述了RunLoop的基本概念,和調用流程,因爲NSRunLoop是基於CFRunLoop作的OC封裝,其原理和流程都是同樣的。另外介紹了兩個使用RunLoop的案例。

相關文章
相關標籤/搜索