iOS開發之Runloop的介紹

文章主要分爲四個部分安全

  • 1、RunLoop 簡介
  • 2、RunLoop 相關接口
  • 3、RunLoop 相關邏輯流程
  • 4、RunLoop 實際應用

1、RunLoop 簡介

1.1 RunLoop 基本概念

一個線程一次只能執行一個任務,執行完成後線程就會退出。RunLoop 機制能讓線程隨時處理事件但並不退出。這裏說的隨時是指:程序須要運行時就保持程序的持續運行,不須要的時候就進入休眠狀態。網絡

NSRunLoop 和 CFRunLoopRef 都是和RunLoop 機制相關的類。CFRunLoopRef 基於 CoreFoundation 框架內,是純 C 函數的 API,全部這些 API 都是線程安全的。CFRunLoopRef 的代碼是開源的。NSRunLoop 是基於 CFRunLoopRef ,提供了面向對象的 API,可是這些 API 不是線程安全的。app

1.2 RunLoop 和 線程的關係

關於RunLoop 和線程之間的關係要知道如下幾點:框架

  • 蘋果不容許直接建立 RunLoop,它只提供了兩個自動獲取的函數:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。
  • 線程和 RunLoop 是一一對應的,其關係是保存在一個全局的 Dictionary 裏。
  • 只能在當前線程中操做當前線程的RunLoop,而不能去操做其餘線程的RunLoop。
  • RunLoop對象在第一次獲取RunLoop時建立,銷燬則是在線程結束的時候。
  • 主線程的RunLoop對象系統自動幫助咱們建立好了,而子線程的RunLoop對象須要咱們主動獲取,由於子線程剛建立時並無 RunLoop,若是你不主動獲取,那它一直都不會有。

2、RunLoop 相關接口

2.1 RunLoop 的結構

和 RunLoop 相關的主要涉及五個類:異步

  • CFRunLoopRef:RunLoop對象
  • CFRunLoopModeRef:運行模式
  • CFRunLoopSourceRef:輸入源/事件源
  • CFRunLoopTimerRef:定時源
  • CFRunLoopObserverRef:觀察者

 

RunLoop的結構函數

從上圖能夠看出,RunLoop 對象中能夠包含多個 Mode,每一個 Mode 又包含多個個 Source、Timer、Observer。oop

2.2 RunLoop 中的 Mode

關於Mode首先要知道一個RunLoop 對象中可能包含多個Mode,且每次調用 RunLoop 的主函數時,只能指定其中一個 Mode(CurrentMode)。切換 Mode,須要從新指定一個 Mode 。主要是爲了分隔開不一樣的 Source、Timer、Observer,讓它們之間互不影響。測試

總共是有五種Mode:spa

  • kCFRunLoopDefaultMode:默認模式,主線程是在這個運行模式下運行
  • UITrackingRunLoopMode:跟蹤用戶交互事件(用於 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其餘Mode影響)
  • UIInitializationRunLoopMode:在剛啓動App時第進入的第一個 Mode,啓動完成後就再也不使用
  • GSEventReceiveRunLoopMode:接受系統內部事件,一般用不到
  • kCFRunLoopCommonModes:僞模式,不是一種真正的運行模式,實際是kCFRunLoopDefaultModeUITrackingRunLoopMode的結合。

有這樣一個場景,假設本身封裝一個無限輪播視圖,頗有可能會出現這樣一種狀況:當你滑動輪播視圖時,輪播視圖的定時器再也不起做用,不能經過定時器調整UIScrollView的偏移值。之因此會出項上述現象,是由於主線程的 RunLoop 裏有兩個 Mode:kCFRunLoopDefaultModeUITrackingRunLoopMode。默認狀況下是defaultMode,可是當滑動UIScrollView時,RunLoop 會將 mode 切換爲 TrackingRunLoopMode,這時 Timer 就不會被回。若是想在滑動的時候不讓定時器失效,可使用CommonMode來解決。線程

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

2.3 Mode 中的 CFRunLoopSourceRef

CFRunLoopSourceRef是事件源,主要有兩種分類方式,一種是蘋果官方的分類方式,另外一種是按照函數調用棧棧分類方式。

2.3.1 官方分類

  • Port-Based Sources(基於端口)
  • Custom Input Sources(自定義)
  • Cocoa Perform Selector Sources

2.3.2 按照函數調用棧分類

  • Source0 :非基於 Port。只包含了一個回調(函數指針),不能主動觸發事件。使用時,需先調用 CFRunLoopSourceSignal(source),將 Source 標記爲待處理,而後手動調用 CFRunLoopWakeUp(runloop)喚醒 RunLoop,讓其處理這個事件。
  • Source1:基於Port,經過內核和
    其餘線程通訊,接收、分發系統事件。 包含了一個 mach_port 和一個回調(函數指針),被用於經過內核和其餘線程相互發送消息。這種 Source 能主動喚醒 RunLoop 的線程。

函數調用棧分類舉例
建立一個按鈕,添加點擊事件,並在按鈕回調事件添加斷點,當執行到斷點出左側會出現相關棧調用信息。從上圖能夠看出:點擊事件就是在Sources0中處理的。至於 Source1 主要是用來接收、分發系統事件,而後再分發到Sources0中處理。

2.4 Mode 中的 CFRunLoopTimerRef

CFRunLoopTimerRef 是定時源,你能夠簡單把它理解爲NSTimer。其包含一個時間點和一個回調(函數指針)。當被加入到 RunLoop 時,RunLoop 會註冊對應的時間點,當時間到時,RunLoop 會執行對應時間點的回調。

2.5 Mode 中的 CFRunLoopObserverRef

CFRunLoopObserverRef是觀察者,主要用來監聽RunLoop 的狀態,主要有如下幾種狀態。

  • kCFRunLoopEntry : 即將進入RunLoop
  • kCFRunLoopBeforeTimers :即將處理Timer
  • kCFRunLoopBeforeSources:即將處理Source
  • kCFRunLoopBeforeWaiting :即將進入休眠
  • kCFRunLoopAfterWaiting:即將從休眠中喚醒
  • kCFRunLoopExit :即將從RunLoop中退出
  • kCFRunLoopAllActivities:監聽所有狀態改變

能夠經過如下代碼驗證RunLoop的幾種狀態:

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

3、RunLoop 相關邏輯流程

 

RunLoop 邏輯流程

上圖是筆者從網上找到的一張 RunLoop 運行的相關流程邏輯圖。具體來講主要執行邏輯是這樣的:

  • 一、通知觀察者 RunLoop 已經啓動。
  • 二、通知觀察者即將要開始定時器。
  • 三、通知觀察者任何即將啓動的非基於端口的源。
  • 四、啓動任何準備好的非基於端口的源(Source0)。
  • 五、若是基於端口的源(Source1)準備好並處於等待狀態,進入步驟9。
  • 六、通知觀察者線程進入休眠狀態。
  • 七、將線程置於休眠狀態,知道下面的任一事件發生才喚醒線程。
    . 某一事件到達基於端口的源
    . 定時器啓動。
    . RunLoop 設置的時間已經超時。
    . RunLoop 被喚醒。
  • 八、通知觀察者線程將被喚醒。
  • 九、處理未處理的事件。
    .若是用戶定義的定時器啓動,處理定時器事件並重啓RunLoop。進入步驟2。
    .若是輸入源啓動,傳遞相應的消息。
    .若是RunLoop被顯示喚醒並且時間還沒超時,重啓RunLoop。進入步驟2
  • 十、通知觀察者RunLoop結束。

4、RunLoop 實際應用

4.1 後臺常駐線程

藉助RunLoop能夠實現線程後臺常駐的功能,關鍵是在於兩行代碼,具體請看以下代碼。

- (void)viewDidLoad {
    [super viewDidLoad];
    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(runOne) object:nil];
    [self.thread start];
}
- (void) runOne{
    NSLog(@"----任務1-----");
    // 下面兩句代碼能夠實現線程保活
    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];
    // 測試是否開啓了RunLoop,若是開啓RunLoop,則來不了這裏,由於RunLoop開啓了循環。
    NSLog(@"未開啓RunLoop");
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    // 利用performSelector,在self.thread的線程中調用run2方法執行任務
    [self performSelector:@selector(runTwo) onThread:self.thread withObject:nil waitUntilDone:NO];
}

- (void) runTwo{
    NSLog(@"----任務2------");
}

實現了上述代碼以後,每次點擊屏幕都會打印----任務2------,這說明子線程處於活躍狀態。

在一些分析AFNetworking源碼的文章中,也常常會出現以下這些代碼。其核心也是爲了實現線程後臺常駐。

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}
 
+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        [_networkRequestThread start];
    });
    return _networkRequestThread;
}

當後臺線程執行任務時,經過 performSelector:onThread:..方法將任務放在後臺線程的 RunLoop 中。正常來講,一個線程執行完任務後就退出了。開啓runloop是爲了防止線程退出。一方面避免每次請求都要建立新的線程;另外一方面,由於connection 的請求是異步的,若是不開啓runloop,線程執行完代碼後不會等待網絡請求完的回調就退出了,這會致使網絡回調的代理方法不執行。

- (void)start {
    [self.lock lock];
    if ([self isCancelled]) {
        [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    } else if ([self isReady]) {
        self.state = AFOperationExecutingState;
        [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    }
    [self.lock unlock];
}

4.2 AutoreleasePool

應用程序一旦啓動,主線程 RunLoop 裏註冊了兩個 Observer。一個 Observer 監聽即將進入Loop事件,回調內會調用 _objc_autoreleasePoolPush() 建立自動釋放池,並保證建立釋放池發生在其餘全部回調以前。另一個 Observer 監視了兩個事件(RunLoop即將進入休眠和即將退出 RunLoop 事件) ,前者會調用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 釋放舊的池並建立新池;後者會調用 _objc_autoreleasePoolPop() 來釋放自動釋放池,並保證釋放自動釋放池事件發生在其它回調以後。

相關文章
相關標籤/搜索