首頁 > 軟體

iOS開發runloop執行迴圈機制學習

2022-07-21 14:05:44

引言

RunLoop:又叫執行迴圈機制,在iOS中的兩大機制之一。並不是只有iOS有Runloop其他語言也有,他們的方式不太一樣,但是核心都是為了解決效能和良好的執行,例如:webJs裡Runloop也稱作eventLoop,由於js沒有多執行緒,在這樣的情況做了一種呼叫棧來配合主執行緒執行。而在iOS裡面runloop就不太一樣,因為有多執行緒的原因,runloop是配合多執行緒使用的。每一個執行緒都對應一個runloop。

Runloop最核心的事情就是保證程式的持續執行讓執行緒在沒有訊息的時候休眠,在有訊息時喚醒,以提高程式效能。這個機制是依靠系統核心來完成的(蘋果作業系統核心元件 Darwin 中的 Mach)
概念:RunLoop 是通過內部維護的事件迴圈(Event Loop)來對事件/訊息進行管理的一個物件。

1、沒有訊息處理時,休眠已避免資源佔用,由使用者態切換到核心態(CPU-核心態和使用者態)

2、有訊息需要處理時,立刻被喚醒,由核心態切換到使用者態

main函數是不會退出的,為什麼呢?這個時候就是 UIApplicationMain 內部預設開啟了主執行緒的 RunLoop,並執行了一段無限迴圈的程式碼(不是簡單的 for 循 環或 while 迴圈)

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

 UIApplicationMain 函數一直沒有返回,而是不斷地接收處理訊息以及等待休眠,所以執行程式之後會保 持持續執行狀態

// 虛擬碼
int main(int argc, char * argv[]) {
    BOOL running = YES;
    do {
        //執行的事件
    } while(running)
  return 0;
}

一、Runloop的實現機制

RunLoop 通過 mach_msg()函數接收、傳送訊息。它的本質是呼叫函數 mach_msg_trap(),相當於是一 個系統呼叫,會觸發核心狀態切換。

在使用者態呼叫 時會切換到核心態;核心態中核心 實現的 mach_msg()函數會完成實際的工作。

二、Runloop 資料結構

是 CFRunLoop(CoreFoundation)的封裝,提供了物件導向的 API 相關的主要涉及五個類:

  • CFRunLoop:Runloop物件 (由 pthread(執行緒物件,說明 和執行緒是一一對應的)、currentMode(當前所處的執行模式)、 modes(多個執行模式的集合)、 (模式名稱字串集合)、,Timer,Source 集合)構成)
  • CFRunLoopMode:執行模式(由 name、source0、source1、observers、timers 構成)
  • CFRunLoopSource:輸入源/事件源 (分為 source0 和 source1 兩種)
  • CFRunLoopTimer:定時源(基於時間的觸發器,基本上說的就是 NStimer。在預設的時間點喚醒 執行回撥。因為它是基於 RunLoop 的,因此它不是實時的(就是 NStimer 是不準確的。 因為只負責分發源的訊息。如果 執行緒當前正在處理繁重的任務,就有可能導致 Timer 本次延時,或者少執行一次))
  • CFRunLoopObserver:觀察者(對相關事件runloop的狀態進行監聽)

CFRunLoopSource分為兩種source0和source1詳解:

  • source0:
    即非基於 port 的,也就是使用者觸發的事件。需要手動喚醒執行緒,將當前執行緒從核心態切換到使用者 態
  • source1:
    基於 port 的,包含一個mach_port和一個回撥,可監聽系統埠和通過核心和其他執行緒傳送的訊息能主動喚醒Runloop接受分發系統事件 具備喚醒事件的能力

CFRunLoopObserver監聽時間點詳細事件

  • kCFRunLoopEntry RunLoop 準備啟動
  • kCFRunLoopBeforeTimers RunLoop 將要處理一些 Timer 相關事件
  • kCFRunLoopBeforeSources RunLoop 將要處理一些 Source 事件
  • kCFRunLoopBeforeWaiting RunLoop 將要進行休眠狀態,即將由使用者態切換到核心態
  • kCFRunLoopAfterWaiting RunLoop 被喚醒,即從核心態切換到使用者態後
  • kCFRunLoopExit RunLoop 退出
  • kCFRunLoopAllActivities 監聽所有狀態

執行緒和 RunLoop 一一對應, RunLoop 和 Mode 是一對多的,Mode 和 source、timer、observer 也是一對多 的

三、實現機制

Runloop執行的大致邏輯是:

通知觀察者 RunLoop 即將啟動。

通知觀察者即將要處理 Timer 事件。

通知觀察者即將要處理 source0 事件。

處理 source0 事件。

如果基於埠的源(Source1)準備好並處於等待狀態,進入步驟 9。

通知觀察者執行緒即將進入休眠狀態。

將執行緒置於休眠狀態,由使用者態切換到核心態,直到下面的任一事件發生才喚醒執行緒。

  • 一個基於 port 的 Source1 的事件。
  • 一個 Timer 到時間了。
  • RunLoop 自身的超時時間到了。
  • 被其他呼叫者手動喚醒。

通知觀察者執行緒將被喚醒。

處理喚醒時收到的事件

  • 如果使用者定義的定時器啟動,處理定時器事件並重啟 RunLoop。進入步驟 2。
  • 如果輸入源啟動,傳遞相應的訊息。
  • 如果 RunLoop 被顯示喚醒而且時間還沒超時,重啟 RunLoop。進入步驟 2

通知觀察者 RunLoop 結束。

觀察者observer 怎麼監聽Runloop,監聽的狀態

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    // 即將進入runloop
    kCFRunLoopEntry = (1UL << 0),
    // 即將處理timer
    kCFRunLoopBeforeTimers = (1UL << 1),
    // 即將處理source
    kCFRunLoopBeforeSources = (1UL << 2),
    // 即將進入休眠
    kCFRunLoopBeforeWaiting = (1UL << 5),
    // 休眠後喚醒
    kCFRunLoopAfterWaiting = (1UL << 6),
    // 退出runloop
    kCFRunLoopExit = (1UL << 7),
    // runloop所有活動
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

四、runloop 和 執行緒

1、怎麼建立一個常駐執行緒?

  • 為當前執行緒開啟一個 RunLoop(第一次呼叫 [NSRunLoop currentRunLoop]方法時實際是會先去建立一 個 RunLoop)
  • 向當前 RunLoop 中新增一個 Port/Source 等維持 RunLoop 的事件迴圈(如果 RunLoop 的 mode 中一個 item 都沒有, runloop會退出)
  • 啟動該RunLoop
    NSRunLoop *runloop = [NSRunLoop currentRunLoop];
    [[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
    [runloop run];

2、如果我們開闢一個新的執行緒 加入了定時器的 這個時候定時器是不會執行的,我們看下下面的程式碼

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLogMeth(@"1")
    ygweakify(self);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        ygstrongify(self);
        NSLogMeth(@"2")
        [self performSelector: @selector(test) afterDelay:10];
        NSLogMeth(@"3")
    });
    NSLogMeth(@"4")
}
- (void)test {
    NSLogMeth(@"5")
}

答案是 1423,test 方法並不會執行。

原因是如果是帶 afterDelay 的延時函數,會在內部建立一個 NSTimer,然後新增到當前執行緒的 RunLoop 中。 也就是如果當前執行緒沒有開啟 RunLoop,該方法會失效。

我們再看另一個

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        ygstrongify(self);
        NSLogMeth(@"2")
        [NSRunLoop currentRunLoop] run];
        [self performSelector: @selector(test) afterDelay:10];
        NSLogMeth(@"3")
    });
    NSLogMeth(@"4")
}

答案依然是 1423,test 方法並不會執行。

原因是如果 RunLoop 的 mode 中一個 item 都沒有,RunLoop 會退出。即在呼叫 RunLoop 的 run 方法後,由 於其 mode 中沒有新增任何 item 去維持 RunLoop 的時間迴圈,RunLoop 隨即還是會退出。 所以我們自己啟動 RunLoop,一定要在新增 item 後 所以我們把 開啟runloop的程式碼 放在 延時方法之後 就好了

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        ygstrongify(self);
        NSLogMeth(@"2")
        [self performSelector: @selector(test) afterDelay:10];
        [NSRunLoop currentRunLoop] run];
        NSLogMeth(@"3")
    });
    NSLogMeth(@"4")
}

這個時候test的方法就執行了

3、怎樣保證子執行緒資料回來更新 UI 的時候不打斷使用者的滑動操作?

當我們在子請求資料的同時滑動瀏覽當前頁面,如果資料請求成功要切回主執行緒更新 UI,那麼就會影響當 前正在滑動的體驗。
我們就可以將更新 UI 事件放在主執行緒的 上執行即可,這樣就會等使用者不再滑動頁 面,主執行緒 RunLoop 由 切換到 時再去更新 UI

[self performSelectorOnMainThread: @selector(readload) withObject:nil waitUntilDone:NO modes:@[NSDefaultRunLoopMode]];

4、NSTimer 在runloop中的關係

NSTimer其實就是 CFRunLoopTimerRef(基於時間的觸發器) ,他們之間是tool-free bridged 的。一個 NSTimer 註冊到RunLoop後, RunLoop會為其重複的時間點註冊好事件。例如 10:00, 10:10, 10:20 這幾個時間點。 RunLoop為了節省資源,並不會在非常準確的時間點回撥這個 Timer。Timer 有個屬性叫做Tolerance(寬容度),標示了當時間點到後,容許有多少最大誤差。

如果某個時間點被錯過了,例如執行了一個很長的任務,則那個時間點的回撥也會跳過去,不會延後執行。就比如等公交,如果 10:10 時我忙著玩手機錯過了那個點的公交,那我只能等 10:20 這一趟了。

CADisplayLink 是一個和螢幕重新整理率一致的定時器(但實際實現原理更復雜,和 NSTimer 並不一樣, 其內部實際是操作了一個 Source)。如果在兩次螢幕重新整理之間執行了一個長任務,那其中就會有一幀被 跳過去(和NSTimer相似),造成介面卡頓的感覺。在快速滑動 TableView 時,即使一幀的卡頓也會 讓使用者有所察覺。 FaceBook開源的 AsyncDisplayLink 就是為了解決介面卡頓的問題,其內部也用 到了 RunLoop

五、非同步繪製

非同步繪製,就是可以在子執行緒把需要繪製的圖形,提前在子執行緒處理好。將準備好影象資料直接返給主執行緒使用,這樣可以降低主執行緒的壓力。(一般情況下我們都是在主執行緒繪製的大家可以作為了解,特殊情況下在處理研究)

非同步繪製的過程:

要通過系統的 [view.delegate displayLayer:] 這個入口來實現非同步繪製。

  • 代理負責生成對應的 Bitmap
  • 設定該 Bitmap 為 layer.contents 屬性的值。

以上就是iOS開發runloop執行迴圈機制學習的詳細內容,更多關於iOS runloop執行迴圈的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com