iOS探索:RunLoop本質、資料結構以及常駐執行緒實現

熊貓超人發表於2018-12-21

RunLoop的本質

RunLoop是通過內部維護的事件迴圈來對事件/訊息進行管理的一個物件

  • 沒有訊息需要處理時,休眠以避免資源佔用,狀態切換是從使用者態通過系統呼叫切換到核心態

  • 有訊息處理時,立刻被喚醒,狀態切換是從核心態通過系統呼叫切換到使用者態

這裡有一個問題,我們應用程式中的main函式為什麼可以保持無退出呢

實際上呢,在我們的main函式中會呼叫UIApplicationMain函式,在這個函式中會啟動一個執行迴圈(也就是我們所說的RunLoop),在這個執行迴圈中可以處理很多事件,例如螢幕的點選,滑動列表,或者網路請求的返回等等,在處理完事件之後,會進入等待,在這個迴圈中,並不是一個單純的for迴圈或者while迴圈,而是從使用者態到核心態的切換,以及再從核心態到使用者態的切換,這裡面的等待也不等於死迴圈,這裡面最重要的是狀態的切換

RunLoop的資料結構

在OC中,系統為我們提供了兩個RunLoop,一個是CFRunLoop,另一個是NSRunLoop,而NSRunLoop是對CFRunLoop的一個封裝,提供了物件導向的API,並且它們也分別屬於不同的框架,NSRunLoop是屬於Foundation框架,而CFRunLoop是屬於Core Foundation框架

關於RunLoop的資料結構主要有三種:

  • CFRunLoop

  • CFRunLoopMode

  • 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要處理,執行緒就將要休眠,同時傳送一個通知,告訴觀察者

  • 然後執行緒進入一個使用者態到核心態的切換,休眠,然後等待喚醒,喚醒的條件大約包括三種: 1、Source1
    2、Timer事件
    3、外部手動喚醒

  • 執行緒剛被喚醒之後也要傳送一個通知告訴觀察者,然後處理喚醒時收到的訊息

  • 回到將要處理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

相關文章