iOS探索:RunLoop本質、數據結構以及常駐線程實現

RunLoop的本質

RunLoop是經過內部維護的事件循環來對事件/消息進行管理的一個對象git

  • 沒有消息須要處理時,休眠以免資源佔用,狀態切換是從用戶態經過系統調用切換到內核態github

  • 有消息處理時,馬上被喚醒,狀態切換是從內核態經過系統調用切換到用戶態數組

這裏有一個問題,咱們應用程序中的main函數爲何能夠保持無退出呢安全

實際上呢,在咱們的main函數中會調用UIApplicationMain函數,在這個函數中會啓動一個運行循環(也就是咱們所說的RunLoop),在這個運行循環中能夠處理不少事件,例如屏幕的點擊,滑動列表,或者網絡請求的返回等等,在處理完事件以後,會進入等待,在這個循環中,並非一個單純的for循環或者while循環,而是從用戶態到內核態的切換,以及再從內核態到用戶態的切換,這裏面的等待也不等於死循環,這裏面最重要的是狀態的切換bash

RunLoop的數據結構

在OC中,系統爲咱們提供了兩個RunLoop,一個是CFRunLoop,另外一個是NSRunLoop,而NSRunLoop是對CFRunLoop的一個封裝,提供了面向對象的API,而且它們也分別屬於不一樣的框架,NSRunLoop是屬於Foundation框架,而CFRunLoop是屬於Core Foundation框架網絡

關於RunLoop的數據結構主要有三種:數據結構

  • CFRunLoop多線程

  • CFRunLoopModeapp

  • Source/Timer/Observer框架

WX20181221-145251@2x.png

  • pthread:表明的是線程,RunLoop與線程的關係是一一對應的

  • currentMode:是一個CFRunLoopMode這樣一個數據結構

  • modes:是一個包含CFRunLoopMode類型的集合(NSMutableSet<CFRunLoopMode*>)

  • commonModes:是一個包含NSString類型的集合(NSMutableSet<NSString*>)

  • commonModeItems:也是一個集合,在這個集合中包含多個元素,其中包括多個Observer,多個Timer,多個Source

WX20181221-150257@2x.png

  • name:名稱,例如NSDefaultRunLoopMode,因此說是經過這樣一個名稱來切換對應的模式,例如在上面的commonModes裏面都是名稱字符串,也就是說經過這些名稱來支持多種模式

  • source0:集合類型的數據結構

  • source1:集合類型的數據結構

  • obsevers:數組類型的數據結構

  • timers:數組類型的數據結構

CFRunLoopSource

  • source0:須要手動喚醒線程

  • source1:具有喚醒線程的能力

CFRunLoopTimer

和NSTimer是toll-free bridge的(免費橋轉換)

CFRunLoopObserver

咱們能夠經過註冊一些Observer來實現對RunLoop相關時間點的觀測

能夠觀測的時間點包括:

  • kCFRunLoopEntry:RunLoop的入口時機,RunLoop將要啓動的時候的回調通知

  • kCFRunLoopBeforeTimers:RunLoop將要處理Timer事件的時候

  • kCFRunLoopBeforeSources:RunLoop將要處理Source事件的時候

  • kCFRunLoopBeforeWaiting:RunLoop將要進入休眠的時候,將要進行用戶態到內核態的切換

  • kCFRunLoopAfterWaiting:RunLoop將要進入喚醒的時候,內核態到用戶態的切換後不久

  • kCFRunLoopExit:RunLoop退出的時候

RunLoop的mode

WX20181221-153513@2x.png

在RunLoop中,假如在mode1中運行,那麼在mode2中事件的回調就會接收不到,RunLoop只接受在當前mode中的回調,那麼這裏有一個經典問題,當咱們在滑動列表時,爲何會出現cell上的定時器中止的狀況以及如何解決

由於在列表滑動的時候當前RunLoop的mode從Default切換到了Tracking,因此致使原來mode中的事件回調接收不到,想要解決即可將其加入commonModes中,下面咱們來看一下commonMode

CommonMode的特殊性

  • CommonMode並非一個實際存在的模式

  • 是同步Source/Timer/Observer到多個Mode中的一中技術方案

事件循環的實現機制

WX20181221-161307@2x.png

  • 在RunLoop啓動以後會發送一個通知,來告知觀察者

  • 將要處理Timer/Source0事件這樣一個通知的發送

  • 處理Source0事件

  • 若是有Source1要處理,這時會經過一個go to語句的實現來進行代碼邏輯的跳轉,處理喚醒是收到的消息

  • 若是沒有Source1要處理,線程就將要休眠,同時發送一個通知,告訴觀察者

  • 而後線程進入一個用戶態到內核態的切換,休眠,而後等待喚醒,喚醒的條件大約包括三種: 一、Source1
    二、Timer事件
    三、外部手動喚醒

  • 線程剛被喚醒以後也要發送一個通知告訴觀察者,而後處理喚醒時收到的消息

  • 回到將要處理Timer/Source0事件這樣一個通知的發送

  • 而後再次進行上面步驟,這就是一個RunLoop的事件循環機制

這裏有一個這樣的問題:當咱們點擊一個app,從咱們點擊到程序啓動、程序運行再到程序殺死這個過程,系統都發生了什麼呢

實際上當咱們調用了main函數以後,會調用UIApplicationMain函數,在這個函數內部會啓動主線程的RunLoop,而後通過一系列的處理,最終主線程的RunLoop會處於一個休眠狀態,而後咱們此時若是點擊一下屏幕,會轉化成一個Source1來說咱們的主線程喚醒,而後當咱們殺死程序時,會調用RunLoop的退出,同時發送通知告訴觀察者

RunLoop與多線程

  • 線程與RunLoop是一一對應的

  • 本身建立的線程默認沒有RunLoop

實現一個常駐線程

  • 爲當前線程開啓一個RunLoop

  • 向該RunLoop中添加一個Port/Source等維持RunLoop的事件循環

  • 啓動該RunLoop

請看下面的一個代碼邏輯

#import "WXObject.h"

static NSThread *thread = nil;
/** 是否繼續事件循環*/
static BOOL runAlways = YES;

@implementation WXObject

+ (NSThread *)threadForDispatch {
    
    if (thread == nil) {
        @synchronized (self) {
            if (thread == nil) {
                thread = [[NSThread alloc] initWithTarget:self selector:@selector(runRequest) object:nil];
                [thread setName:@"alwaysThread"];
                //啓動線程
                [thread start];
            }
        }
    }
    
    return thread;
}

+ (void)runRequest {
    
    //建立一個Source
    CFRunLoopSourceContext context = {0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL};
    CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
    
    //建立RunLoop,同時向RunLoop的defaultMode下面添加Source
    CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
    
    //若是能夠運行
    while (runAlways) {
        @autoreleasepool {
            //令當前RunLoop運行在defaultMode下
            CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, true);
        }
    }
    
    //某一時機,靜態變量runAlways變爲NO時,保證跳出RunLoop,線程推出
    CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
    CFRelease(source);
}

@end
複製代碼
  • 首先咱們在這裏定義兩個全局靜態變量,一個是咱們自定義的線程thread,還有一個是用來控制是否事件循環

  • 而後咱們建立線程,用@synchronized來保證線程安全,建立的時候添加入口方法,而後啓動線程,當線程調用start方法時,會調用下面入口方法

  • 在這個方法中首先建立source,傳入一個上下文,而後建立RunLoop,同時向RunLoop的defaultMode下面添加Source,CFRunLoopGetCurrent()這個方法若是獲取不到就會建立一個RunLoop,而後添加到defaultMode中

  • 經過咱們前面定義的靜態變量來進行判斷,若是能夠運行,就令當前RunLoop運行在defaultMode下,這裏用了一個自動釋放池,減少內存峯值消耗,這裏須要注意的是,若是咱們上面添加到的是defaultMode,這裏也須要運行在defaultMode中,不然會出現死循環

  • 某一時機,靜態變量runAlways變爲NO時,保證跳出RunLoop,線程推出,釋放source

以上就是實現一個常駐線程的代碼邏輯

GitHub

Demo

相關文章
相關標籤/搜索