讀 Threading Programming Guide 筆記(二)

本文首發CSDN,如需轉載請與CSDN聯系。

記得第一次讀這個文檔還是3年前,那時也只是泛讀。如今關于iOS多線程的文章層出不窮,但我覺得若想更好的領會各個實踐者的文章,應該先仔細讀讀官方的相關文檔,打好基礎,定會有更好的效果。文章中有對官方文檔的翻譯,也有自己的理解,官方文檔中代碼片段的示例在這篇文章中都進行了完整的重寫,還有一些文檔中沒有的代碼示例,并且都使用Swift完成,給大家一些Objc與Swift轉換的參考。
官方文檔地址:Threading Programming Guide

線程屬性配置

線程也是具有若干屬性的,自然一些屬性也是可配置的,在啟動線程之前我們可以對其進行配置,比如線程占用的內存空間大小、線程持久層中的數據、設置線程類型、優先級等。

配置線程的??臻g大小

在前文中提到過線程對內存空間的消耗,其中一部分就是線程棧,我們可以對線程棧的大小進行配置:

  • Cocoa框架:在OS X v10.5之后的版本和iOS2.0之后的版本中,我們可以通過修改NSThread類的stackSize屬性,改變二級線程的線程棧大小,不過這里要注意的是該屬性的單位是字節,并且設置的大小必須得是4KB的倍數。
  • POSIX API:通過pthread_attr_- setstacksize函數給線程屬性pthread_attr_t結構體設置線程棧大小,然后在使用pthread_create函數創建線程時將線程屬性傳入即可。

注意:在使用Cocoa框架的前提下修改線程棧時,不能使用NSThreaddetachNewThreadSelector: toTarget:withObject:方法,因為上文中說過,該方法先創建線程,即刻便啟動了線程,所以根本沒有機會修改線程屬性。

配置線程存儲字典

每一個線程,在整個生命周期里都會有一個字典,以key-value的形式存儲著在線程執行過程中你希望保存下來的各種類型的數據,比如一個常駐線程的運行狀態,線程可以在任何時候訪問該字典里的數據。

在Cocoa框架中,可以通過NSThread類的threadDictionary屬性,獲取到NSMutableDictionary類型對象,然后自定義key值,存入任何里先儲存的對象或數據。如果使用POSIX線程,可以使用pthread_setspecificpthread_getspecific函數設置獲取線程字典。

配置線程類型

在上文中提到過,線程有Joinable和Detached類型,大多數非底層的線程默認都是Detached類型的,相比Joinable類型的線程來說,Detached類型的線程不用與其他線程結合,并且在執行完任務后可自動被系統回收資源,而且主線程不會因此而阻塞,這著實要方便許多。

使用NSThread創建的線程默認都是Detached類型,而且似乎也不能將其設置為Joinable類型。而使用POSIX API創建的線程則默認為Joinable類型,而且這也是唯一創建Joinable類型線程的方式。通過POSIX API可以在創建線程前通過函數pthread_attr_setdetachstate更新線程屬性,將其設置為不同的類型,如果線程已經創建,那么可以使用pthread_detach函數改變其類型。Joinable類型的線程還有一個特性,那就是在終止之前可以將數據傳給與之相結合的線程,從而達到線程之間的交互。即將要終止的線程可以通過pthread_exit函數傳遞指針或者任務執行的結果,然后與之結合的線程可以通過pthread_join函數接受數據。

雖然通過POSIX API創建的線程使用和管理起來較為復雜和麻煩,但這也說明這種方式更為靈活,更能滿足不同的使用場景和需求。比如當執行一些關鍵的任務,不能被打斷的任務,像執行I/O操作之類。

設置線程優先級

每一個新創建的二級線程都有它自己的默認優先級,內核會根據線程的各屬性通過分配算法計算出線程的優先級。這里需要明確一個概念,高優先級的線程雖然會更早的運行,但這其中并沒有執行時間效率的因素,也就是說高優先級的線程會更早的執行它的任務,但在執行任務的時間長短方面并沒有特別之處。

不論是通過NSThread創建線程還是通過POSIX API創建線程,他們都提供了設置線程優先級的方法。我們可以通過NSThread的類方法setThreadPriority:設置優先級,因為線程的優先級由0.0~1.0表示,所以設置優先級時也一樣。我們也可以通過pthread_setschedparam函數設置線程優先級。

注意:設置線程的優先級時可以在線程運行時設置。

雖然我們可以調節線程的優先級,但不到必要時還是不建議調節線程的優先級。因為一旦調高了某個線程的優先級,與低優先級線程的優先等級差距太大,就有可能導致低優先級線程永遠得不到運行的機會,從而產生性能瓶頸。比如說有兩個線程A和B,起初優先級相差無幾,那么在執行任務的時候都會相繼無序的運行,如果將線程A的優先級調高,并且當線程A不會因為執行的任務而阻塞時,線程B就可能一直不能運行,此時如果線程A中執行的任務需要與線程B中任務進行數據交互,而遲遲得不到線程B中的結果,此時線程A就會被阻塞,那么程序的性能自然就會產生瓶頸。

線程執行的任務

在任何平臺,線程存在的價值和意義都是一樣的,那就是執行任務,不論是方法、函數或一段代碼,除了依照語言語法正常編寫外,還有一些額外需要大家注意的事項。

Autorelease Pool

在Xcode4.3之前,我們都處在手動管理引用計數的時代,代碼里滿是retainrelease的方法,所以那個時候,被線程執行的任務中,為了能自動處理大量對象的retainrelease操作,都會使用NSAutoreleasePool類創建自動釋放池,它的作用是將線程中要執行的任務都放在自動釋放池中,自動釋放池會捕獲所有任務中的對象,在任務結束或線程關閉之時自動釋放這些對象:

- (void)myThreadMainRoutine
{

    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // 頂層自動釋放池

    // 線程執行任務的邏輯代碼

    [pool release];

}

到了自動引用計數(ARC)時代,就不能使用NSAutoreleasePool進行自動釋放池管理了,而是新加了@autoreleasepool代碼塊語法來創建自動釋放池:

- (void)myThreadMainRoutine
{

    @autoreleasepool {
     
     // 線程執行任務的邏輯代碼
     
    }

}

我們知道每個應用程序都是運行在一個主線程里的,而線程都至少得有一個自動釋放池,所以說整個應用其實是跑在一個自動釋放池中的。大家都知道C系語言中,程序的入口函數都是main函數,當我們創建一個Objective-C的iOS應用后,Xcode會在Supporting Files目錄下自動為我們創建一個main.m文件:

LearnThread-2

main.m這個文件中就能證實上面說的那點:

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

以上都是在Objective-C中,但在Swift中,就有點不一樣了,NSAutoreleasePool@autoreleasepool都不能用了,取而代之的是Swift提供的一個方法func autoreleasepool(code: () -> ()),接收的參數為一個閉包,我們可以這樣使用:

func performInBackground() {
        
        autoreleasepool({
        
          // 線程執行任務的邏輯代碼
          
          print("I am a event, perform in Background Thread.")  
          
        })
        
    }

根據尾隨閉包的寫法,還可以這樣使用:

func performInBackground() {

        autoreleasepool{
        
          // 線程執行任務的邏輯代碼
          
          print("I am a event, perform in Background Thread.")
            
        }
        
    }

有些人可能會問在ARC的時代下為什么還要用自動釋放池呢?比如在SDWebImage中就大量使用了@autoreleasepool代碼塊,其原因就是為了避免內存峰值,大家都知道在MRC時代,除了retainrelease方法外,還有一個常用的方法是autorelease,用來延遲釋放對象,它釋放對象的時機是當前runloop結束時。到了ARC時代,雖然不用我們手動管理內存了,但其自動管理的本質與MRC時是一樣的,只不過由編譯器幫我們在合適的地方加上了這三個方法,所以說如果在一個線程執行的任務中大量產生需要autorelease的對象時,因為不能及時釋放對象,所以就很有可能產生內存峰值。那么在這種任務中在特定的時候使用@autorelease代碼塊,幫助釋放對象,就可以有效的防止內存峰值的發生。

設置異常處理

在線程執行任務的時候,難免會出現異常,如果不能及時捕獲異常任由其拋出,就會導致整個應用程序退出。在Swift2.0中,Apple提供了新的異??刂铺幚頇C制,讓我們能像Java中一樣形如流水的捕獲處理異常。所以在線程執行的任務中,我們盡量使用異常處理機制,提高健壯性。

創建Runloop

大家知道,一個線程只能執行一個任務,當任務結束后也就意味著這個線程也要結束,頻繁的創建線程也是挺消耗資源的一件事,于是就有了常駐線程,前文介紹線程相關概念時也提到過:

簡單的來說,RunLoop用于管理和監聽異步添加到線程中的事件,當有事件輸入時,系統喚醒線程并將事件分派給RunLoop,當沒有需要處理的事件時,RunLoop會讓線程進入休眠狀態。這樣就能讓線程常駐在進程中,而不會過多的消耗系統資源,達到有事做事,沒事睡覺的效果。

如果想要線程不結束,那就要被執行的任務不結束,讓被執行的任務不結束顯然不靠譜,那么就需要一個機制,能占著線程。該機制就是事件循環機制(Eventloop),體現在代碼中就是一個do-while循環,不斷的接收事件消息、處理事件、等待新事件消息,除非接收到一個讓其退出的事件消息,否則它將一直這么循環著,線程自然就不會結束。Runloop就是管理消息和事件,并提供Eventloop函數的對象,線程執行的任務其實就是在Runloop對象的Eventloop函數里運行。關于Runloop更詳細的知識及配置
操作在后文中會有講述。

終止線程

打個不恰當的比方,人終有一死,或正常生老病死,或非正常出事故意外而亡,前者尚合情合理后者悲痛欲絕。線程也一樣,有正常終止結束,也有非正常的強制結束,不管是線程本身還是應用程序都希望線程能正常結束,因為正常結束也就意味著被執行的任務正常執行完成,從而讓線程處理完后事隨即結束,如果在任務執行途中強制終止線程,會導致線程沒有機會處理后事,也就是正常釋放資源對象等,這樣會給應用程序帶來例如內存溢出這類潛在的問題,所以強烈不推薦強制終止線程的做法。

如果確實有在任務執行途中終止線程的需求,那么可以使用Runloop,在任務執行過程中定期查看是否有收到終止任務的事件消息,這樣一來可以在任務執行途中判斷出終止任務的信號,然后進行終止任務的相關處理,比如保存數據等,二來可以讓線程有充分的時間釋放資源。

Run Loop

Run Loops是線程中的基礎結構,在上文中也提到過,Run Loops其實是一個事件循環機制,用來分配、分派線程接受到的事件任務,同時可以讓線程成為一個常駐線程,即有任務時處理任務,沒任務時休眠,且不消耗資源。在實際應用時,Run Loop的生命周期并不全是自動完成的,還是需要人工進行配置,不論是Cocoa框架還是Core Foundation框架都提供了Run Loop的相關對象對其進行配置和管理。

注:Core Foundation框架是一組C語言接口,它們為iOS應用程序提供基本數據管理和服務功能,比如線程和Run Loop、端口、Socket、時間日期等。

在所有的線程中,不論是主線程還是二級線程,都不需要顯示的創建Run Loop對象,這里的顯示指的是通過任何create打頭的方法創建Run Loop。對于主線程來說,當應用程序通過UIApplicationMain啟動時,主線程中的Run Loop就已經創建并啟動了,而且也配置好了。那么如果是二級線程,則需要我們手動先獲取Run Loop,然后再手動進行配置并啟動。下面的章節會向大家詳細介紹Run Loop的知識。

注:在二級線程中獲取Run Loop有兩種方式,通過NSRunloop的類方法currentRunLoop獲取Run Loop對象(NSRunLoop),或者通過Core Foundation框架中的CFRunLoopGetCurrent()函數獲取當前線程的Run Loop對象(CFRunLoop)。NSRunLoopCFRunLoop的上層封裝。

let nsrunloop = NSRunLoop.currentRunLoop()
        
let cfrunloop = CFRunLoopGetCurrent()

Run Loop的事件來源

Run Loop有兩個事件來源,一個是Input source,接收來自其他線程或應用程序(進程)的異步事件消息,并將消息分派給對應的事件處理方法。另一個是Timer source,接收定期循環執行或定時執行的同步事件消息,同樣會將消息分派給對應的事件處理方法。

LearnThread-3

上圖展示了Run Loop的兩類事件來源,以及在Input source中的兩種不同的子類型,它們分別對應著Run Loop中不同的處理器。當不同的事件源接收到消息后,通過NSRunLooprunUntilDate:方法啟動運行Run Loop,將事件消息分派給對應的處理器執行,一直到指定的時間時退出Run Loop。

Run Loop的觀察者

Run Loop的觀察者可以理解為Run Loop自身運行狀態的監聽器,它可以監聽Run Loop的下面這些運行狀態:

  • Run Loop準備開始運行時。
  • 當Run Loop準備要執行一個Timer Source事件時。
  • 當Run Loop準備要執行一個Input Source事件時。
  • 當Run Loop準備休眠時。
  • 當Run Loop被進入的事件消息喚醒并且還沒有開始讓處理器執行事件消息時。
  • 退出Run Loop時。

Run Loop的觀察者在NSRunloop中沒有提供相關接口,所以我們需要通過Core Foundation框架使用它,可以通過CFRunLoopObserverCreate方法創建Run Loop的觀察者,類型為CFRunLoopObserverRef,它其實是CFRunLoopObserver的重定義名稱。上述的那些可以被監聽的運行狀態被封裝在了CFRunLoopActivity結構體中,對應關系如下:

  • CFRunLoopActivity.Entry
  • CFRunLoopActivity.BeforeTimers
  • CFRunLoopActivity.BeforeSources
  • CFRunLoopActivity.BeforeWaiting
  • CFRunLoopActivity.AfterWaiting
  • CFRunLoopActivity.Exit

Run Loop的觀察者和Timer事件類似,可以只使用一次,也可以重復使用,在創建觀察者時可以設置。如果只使用一次,那么當監聽到對應的狀態后會自行移除,如果是重復使用的,那么會留在Run Loop中多次監聽Run Loop相同的運行狀態。

Run Loop Modes

Run Loop Modes可以稱之為Run Loop模式,這個模式可以理解為對Run Loop各種設置項的不同組合,舉個例子,iPhone手機運行的iOS有很多系統設置項,假設白天我打開蜂窩數據,晚上我關閉蜂窩數據,而打開無線網絡,到睡覺時我關閉蜂窩數據和無線網絡,而打開飛行模式。假設在這三個時段中其他的所有設置項都相同,而只有這三個設置項不同,那么就可以說我的手機有三種不同的設置模式,對應著不同的時間段。那么Run Loop的設置項是什么呢?那自然就是前文中提到的不同的事件來源以及觀察者了,比如說,Run Loop的模式A(Mode A),只包含接收Timer Source事件源的事件消息以及監聽Run Loop運行時的觀察者,而模式B(Mode B)只包含接收Input Source事件源的事件消息以及監聽Run Loop準備休眠時和退出Run Loop時的觀察者,如下圖所示:

LearnThread-4

所以說,Run Loop的模式就是不同類型的數據源和不同觀察者的集合,當Run Loop運行時要設置它的模式,也就是告知Run Loop只需要關心這個集合中的數據源類型和觀察者,其他的一概不予理會。那么通過模式,就可以讓Run Loop過濾掉它不關心的一些事件,以及避免被無關的觀察者打擾。如果有不在當前模式中的數據源發來事件消息,那只能等Run Loop改為包含有該數據源類型的模式時,才能處理事件消息。

在Cocoa框架和Core Foundation框架中,已經為我們預定義了一些Run Loop模式:

  • 默認模式:在NSRunloop中的定義為NSDefaultRunLoopMode,在CFRunloop中的定義為kCFRunLoopDefaultMode。該模式包含的事件源囊括了除網絡鏈接操作的大多數操作以及時間事件,用于當前Run Loop處于空閑狀態等待事件時,以及Run Loop開始運行時。
  • NSConnectionReplyMode:該模式用于監聽NSConnection相關對象的返回結果和狀態,在系統內部使用,我們一般不會使用該模式。
  • NSModalPanelRunLoopMode:該模式用于過濾在模態面板中處理的事件(Mac App)。
  • NSEventTrackingRunLoopMode:該模式用于跟蹤用戶與界面交互的事件。
  • 模式集合:或者叫模式組,顧名思義就是將多個模式組成一個組,然后將模式組認為是一個模式設置給Run Loop,在NSRunloop中的定義為NSRunLoopCommonModes,在CFRunloop中的定義為kCFRunLoopCommonModes。系統提供的模式組名為Common Modes,它默認包含NSDefaultRunLoopMode、NSModalPanelRunLoopMode、NSEventTrackingRunLoopMode這三個模式。

以上五種系統預定的模式中,前四種屬于只讀模式,也就是我們無法修改它們包含的事件源類型和觀察者類型。而模式組我們可以通過Core Foundation框架提供的CFRunLoopAddCommonMode(_ rl: CFRunLoop!, _ mode: CFString!)方法添加新的模式,甚至是我們自定義的模式。這里需要注意的是,既然在使用時,模式組是被當作一個模式使用的,那么自然可以給它設置不同類型的事件源或觀察者,當給模式組設置事件源或觀察者時,實際是給該模式組包含的所有模式設置。比如說給模式組設置了一個監聽Run Loop準備休眠時的觀察者,那么該模式組里的所有模式都會被設置該觀察者。

Input Source

前文中說過,Input Sources接收到各種操作輸入事件消息,然后異步的分派給對應事件處理方法。在Input Sources中又分兩大類的事件源,一類是基于端口事件源(Port-based source),在CFRunLoopSourceRef的結構中為source1,主要通過監聽應用程序的Mach端口接收事件消息并分派,該類型的事件源可以主動喚醒Run Loop。另一類是自定義事件源(Custom source),在CFRunLoopSourceRef的結構中為source0,一般是接收其他線程的事件消息并分派給當前線程的Run Loop,比如performSwlwctor:onThread:...系列方法,該類型的事件源無法自動喚醒Run Loop,而是需要手動將事件源設置為待執行的標記,然后再手動喚醒Run Loop。雖然這兩種類型的事件源接收事件消息的方式不一樣,但是當接收到消息后,對消息的分派機制是完全相同的。

Port-Based Source

Cocoa框架和Core Foundation框架都提供了相關的對象和函數用于創建基于端口的事件源。在Cocoa框架中,實現基于端口的事件源主要是通過NSPort類實現的,它代表了交流通道,也就是說在不同的線程的Run Loop中都存在NSPort,那么它們之間就可以通過發送與接收消息(NSPortMessage)互相通信。所以我們只需要通過NSPort類的類方法port創建對象實例,然后通過NSRunloop的方法將其添加到Run Loop中,或者在創建二級線程時將創建好的NSPort對象傳入即可,無需我們再做消息、消息上下文、事件源等其他配置,都由Run Loop自行配置好了。而在Core Foundation框架中就比較麻煩一些,大多數配置都需要我們手動配置,在后面會詳細舉例說明。

Custom Input Source

Cocoa框架中沒有提供創建自定義事件源的相關接口,我們只能通過Core Foundation框架中提供的對象和函數創建自定義事件源,手動配置事件源各個階段要處理的邏輯,比如創建CFRunLoopSourceRef事件源對象,通過CFRunLoopScheduleCallBack回調函數配置事件源上下文并注冊事件源,通過CFRunLoopPerformCallBack回調函數處理接收到事件消息后的邏輯,通過CFRunLoopCancelCallBack函數銷毀事件源等等,在后文中會有詳細舉例說明。

雖然Cocoa框架沒有提供創建自定義事件源的相關對象和接口,但是它為我們預定義好了一些事件源,能讓我們在當前線程、其他二級線程、主線程中執行我們希望被執行的方法,讓我們看看NSObject中的這些方法:

func performSelectorOnMainThread(_ aSelector: Selector, withObject arg: AnyObject?, waitUntilDone wait: Bool)

func performSelectorOnMainThread(_ aSelector: Selector, withObject arg: AnyObject?, waitUntilDone wait: Bool, modes array: [String]?)

這兩個方法允許我們將當前線程中對象的方法讓主線程去執行,可以選擇是否阻塞當前線程,以及希望被執行的方法作為事件消息被何種Run Loop模式監聽。

注:如果在主線程中使用該方法,當選擇阻塞當前線程,那么發送的方法會立即被主線程執行,若選擇不阻塞當前線程,那么被發送的方法將被排進主線程Run Loop的事件隊列中,并等待執行。

func performSelector(_ aSelector: Selector, withObject anArgument: AnyObject?, afterDelay delay: NSTimeInterval)

func performSelector(_ aSelector: Selector, withObject anArgument: AnyObject?, afterDelay delay: NSTimeInterval, inModes modes: [String])

這兩個方法允許我們給當前線程發送事件消息,當前線程接收到消息后會依次加入Run Loop的事件消息隊列中,等待Run Loop迭代執行。該方法還可以指定消息延遲發送時間及消息希望被何種Run Loop模式監聽。

注:該方法中的延遲時間并不是延遲Run Loop執行事件消息的事件,而是延遲向當前線程發送事件消息的時間。另外,即便不設置延遲時間,那么發送的事件消息也不一定立即被執行,因為在Run Loop的事件消息隊列中可以已有若干等待執行的消息。

func performSelector(_ aSelector: Selector, onThread thr: NSThread, withObject arg: AnyObject?, waitUntilDone wait: Bool)

func performSelector(_ aSelector: Selector, onThread thr: NSThread, withObject arg: AnyObject?, waitUntilDone wait: Bool, modes array: [String]?)

這兩個方法允許我們給其他二級線程發送事件消息,前提是要取得目標二級線程的NSThread對象實例,該方法同樣提供了是否阻塞當前線程的選項和設置Run Loop模式的選項。

注:使用該方法給二級線程發送事件消息時要確保目標線程正在運行,換句話說就是目標線程要有啟動著的Run Loop。并且保證目標線程執行的任務要在應用程序代理執行applicationDidFinishLaunching:方法前完成,否則主線程就結束了,目標線程自然也就結束了。

func performSelectorInBackground(_ aSelector: Selector, withObject arg: AnyObject?)

該方法允許我們在當前應用程序中創建一個二級線程,并將指定的事件消息發送給新創建的二級線程。

class func cancelPreviousPerformRequestsWithTarget(_ aTarget: AnyObject)

class func cancelPreviousPerformRequestsWithTarget(_ aTarget: AnyObject, selector aSelector: Selector, object anArgument: AnyObject?)

這兩個方法是NSObject的類方法,第一個方法作用是在當前線程中取消Run Lop中某對象通過performSelector:withObject:afterDelay:方法發送的所有事件消息執行請求。第二個方法多了兩個過濾參數,那就是方法名稱和參數,取消指定方法名和參數的事件消息執行請求。

Timer Source

Timer Source顧名思義就是向Run Loop發送在將來某一時間執行或周期性重復執行的同步事件消息。當某線程不需要其他線程通知而需要自己通知自己執行任務時就可以用這種事件源。舉個應用場景,在iOS應用中,我們經常會用到搜索功能,而且一些搜索框具有自動搜索的能力,也就是說不用我們點擊搜索按鈕,只需要輸入完我想要搜索的內容就會自動搜索,大家想一想如果每輸入一個字就開始立即搜索,不但沒有意義,性能開銷也大,用戶體驗自然也很糟糕,我們希望當輸入完這句話,或至少輸入一部分之后再開始搜索,所以我們就可以在開始輸入內容時向執行搜索功能的線程發送定時搜索的事件消息,讓其在若干時間后再執行搜索任務,這樣就有緩沖時間輸入搜索內容了。

這里需要注意的是Timer Source發送給Run Loop的周期性執行任務的重復時間是相對時間。比如說給Run Loop發送了一個每隔5秒執行一次的任務,每次執行任務的正常時間為2秒,執行5次后終止,假設該任務被立即執行,那么當該任務終止時應該歷時30秒,但當第一次執行時出現了問題,導致任務執行了20秒,那么該任務只能再執行一次就終止了,執行的這一次其實就是第5次,也就是說不論任務的執行時間延遲與否,Run Loop都會按照初始的時間間隔執行任務,并非按Finish-To-Finish去算的,所以一旦中間任務有延時,那么就會丟失任務執行次數。關于Timer Source的使用,在后文中會有詳細舉例說明。

Run Loop內部運行邏輯

在Run Loop的運行生命周期中,無時無刻都伴隨著執行等待執行的各種任務以及在不同的運行狀態時通知不同的觀察者,下面我們看看Run Loop中的運行邏輯到底是怎樣的:

  1. 通知對應觀察者Run Loop準備開始運行。
  2. 通知對應觀察者準備執行定時任務。
  3. 通知對應觀察者準備執行自定義事件源的任務。
  4. 開始執行自定義事件源任務。
  5. 如果有基于端口事件源的任務準備待執行,那么立即執行該任務。然后跳到步驟9繼續運轉。
  6. 通知對應觀察者線程進入休眠。
  7. 如果有下面的事件發生,則喚醒線程:
  • 接收到基于端口事件源的任務。
  • 定時任務到了該執行的時間點。
  • Run Loop的超時時間到期。
  • Run Loop被手動喚醒。
  1. 通知對應觀察者線程被喚醒。
  2. 執行等待執行的任務。
  • 如果有定時任務已啟動,執行定時任務并重啟Run Loop。然后跳到步驟2繼續運轉。
  • 如果有非定時器事件源的任務待執行,那么分派執行該任務。
  • 如果Run Loop被手動喚醒,重啟Run Loop。然后跳轉到步驟2繼續運轉。
  1. 通知對應觀察者已退出Run Loop。

以上這些Run Loop中的步驟也不是每一步都會觸發,舉一個例子:
1.對應觀察者接收到通知Run Loop準備開始運行 -> 3.對應觀察者接收到通知Run Loop準備執行自定義事件源任務 -> 4.開始執行自定義事件源任務 -> 任務執行完畢且沒有其他任務待執行 -> 6.線程進入休眠狀態,并通知對應觀察者 -> 7.接收到定時任務并喚醒線程 -> 8.通知對應觀察者線程被喚醒 -> 9.執行定時任務并重啟Run Loop -> 2.通知對應觀察者準備執行定時任務 -> Run Loop執行定時任務,并在等待下次執行任務的間隔中線程休眠 -> 6.線程進入休眠狀態,并通知對應觀察者...

這里需要注意的一點是從上面的運行邏輯中可以看出,當觀察者接收到執行任務的通知時,Run Loop并沒有真正開始執行任務,所以觀察者接收到通知的時間與Run Loop真正執行任務的時間有時間差,一般情況下這點時間差影響不大,但如果你需要通過觀察者知道Run Loop執行任務的確切時間,并根據這個時間要進行后續操作的話,那么就需要通過結合多個觀察者接收到的通知共同確定了。一般通過監聽準備執行任務的觀察者、監聽線程進入休眠的觀察者、監聽線程被喚醒的觀察者共同確定執行任務的確切時間。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,321評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,559評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,442評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,835評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,581評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,922評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,931評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,096評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,639評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,374評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,591評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,104評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,789評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,196評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,524評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,322評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,554評論 2 379

推薦閱讀更多精彩內容