讀 Threading Programming Guide 筆記(二)

本文首發(fā)CSDN雇庙,如需轉(zhuǎn)載請(qǐng)與CSDN聯(lián)系鱼鼓。

記得第一次讀這個(gè)文檔還是3年前,那時(shí)也只是泛讀宣蔚。如今關(guān)于iOS多線程的文章層出不窮向抢,但我覺得若想更好的領(lǐng)會(huì)各個(gè)實(shí)踐者的文章,應(yīng)該先仔細(xì)讀讀官方的相關(guān)文檔胚委,打好基礎(chǔ)挟鸠,定會(huì)有更好的效果。文章中有對(duì)官方文檔的翻譯亩冬,也有自己的理解艘希,官方文檔中代碼片段的示例在這篇文章中都進(jìn)行了完整的重寫,還有一些文檔中沒有的代碼示例硅急,并且都使用Swift完成覆享,給大家一些Objc與Swift轉(zhuǎn)換的參考。
官方文檔地址:Threading Programming Guide

線程屬性配置

線程也是具有若干屬性的营袜,自然一些屬性也是可配置的撒顿,在啟動(dòng)線程之前我們可以對(duì)其進(jìn)行配置,比如線程占用的內(nèi)存空間大小荚板、線程持久層中的數(shù)據(jù)凤壁、設(shè)置線程類型、優(yōu)先級(jí)等跪另。

配置線程的椗《叮空間大小

在前文中提到過線程對(duì)內(nèi)存空間的消耗,其中一部分就是線程棧免绿,我們可以對(duì)線程棧的大小進(jìn)行配置:

  • Cocoa框架:在OS X v10.5之后的版本和iOS2.0之后的版本中徙鱼,我們可以通過修改NSThread類的stackSize屬性,改變二級(jí)線程的線程棧大小针姿,不過這里要注意的是該屬性的單位是字節(jié),并且設(shè)置的大小必須得是4KB的倍數(shù)厌衙。
  • POSIX API:通過pthread_attr_- setstacksize函數(shù)給線程屬性pthread_attr_t結(jié)構(gòu)體設(shè)置線程棧大小距淫,然后在使用pthread_create函數(shù)創(chuàng)建線程時(shí)將線程屬性傳入即可。

注意:在使用Cocoa框架的前提下修改線程棧時(shí)婶希,不能使用NSThreaddetachNewThreadSelector: toTarget:withObject:方法榕暇,因?yàn)樯衔闹姓f過,該方法先創(chuàng)建線程,即刻便啟動(dòng)了線程彤枢,所以根本沒有機(jī)會(huì)修改線程屬性狰晚。

配置線程存儲(chǔ)字典

每一個(gè)線程,在整個(gè)生命周期里都會(huì)有一個(gè)字典缴啡,以key-value的形式存儲(chǔ)著在線程執(zhí)行過程中你希望保存下來的各種類型的數(shù)據(jù)壁晒,比如一個(gè)常駐線程的運(yùn)行狀態(tài),線程可以在任何時(shí)候訪問該字典里的數(shù)據(jù)业栅。

在Cocoa框架中秒咐,可以通過NSThread類的threadDictionary屬性,獲取到NSMutableDictionary類型對(duì)象碘裕,然后自定義key值携取,存入任何里先儲(chǔ)存的對(duì)象或數(shù)據(jù)。如果使用POSIX線程帮孔,可以使用pthread_setspecificpthread_getspecific函數(shù)設(shè)置獲取線程字典雷滋。

配置線程類型

在上文中提到過,線程有Joinable和Detached類型文兢,大多數(shù)非底層的線程默認(rèn)都是Detached類型的晤斩,相比Joinable類型的線程來說,Detached類型的線程不用與其他線程結(jié)合禽作,并且在執(zhí)行完任務(wù)后可自動(dòng)被系統(tǒng)回收資源尸昧,而且主線程不會(huì)因此而阻塞,這著實(shí)要方便許多旷偿。

使用NSThread創(chuàng)建的線程默認(rèn)都是Detached類型烹俗,而且似乎也不能將其設(shè)置為Joinable類型。而使用POSIX API創(chuàng)建的線程則默認(rèn)為Joinable類型萍程,而且這也是唯一創(chuàng)建Joinable類型線程的方式幢妄。通過POSIX API可以在創(chuàng)建線程前通過函數(shù)pthread_attr_setdetachstate更新線程屬性,將其設(shè)置為不同的類型茫负,如果線程已經(jīng)創(chuàng)建蕉鸳,那么可以使用pthread_detach函數(shù)改變其類型。Joinable類型的線程還有一個(gè)特性忍法,那就是在終止之前可以將數(shù)據(jù)傳給與之相結(jié)合的線程潮尝,從而達(dá)到線程之間的交互。即將要終止的線程可以通過pthread_exit函數(shù)傳遞指針或者任務(wù)執(zhí)行的結(jié)果饿序,然后與之結(jié)合的線程可以通過pthread_join函數(shù)接受數(shù)據(jù)勉失。

雖然通過POSIX API創(chuàng)建的線程使用和管理起來較為復(fù)雜和麻煩,但這也說明這種方式更為靈活原探,更能滿足不同的使用場(chǎng)景和需求乱凿。比如當(dāng)執(zhí)行一些關(guān)鍵的任務(wù)顽素,不能被打斷的任務(wù),像執(zhí)行I/O操作之類徒蟆。

設(shè)置線程優(yōu)先級(jí)

每一個(gè)新創(chuàng)建的二級(jí)線程都有它自己的默認(rèn)優(yōu)先級(jí)胁出,內(nèi)核會(huì)根據(jù)線程的各屬性通過分配算法計(jì)算出線程的優(yōu)先級(jí)。這里需要明確一個(gè)概念段审,高優(yōu)先級(jí)的線程雖然會(huì)更早的運(yùn)行全蝶,但這其中并沒有執(zhí)行時(shí)間效率的因素,也就是說高優(yōu)先級(jí)的線程會(huì)更早的執(zhí)行它的任務(wù)戚哎,但在執(zhí)行任務(wù)的時(shí)間長(zhǎng)短方面并沒有特別之處裸诽。

不論是通過NSThread創(chuàng)建線程還是通過POSIX API創(chuàng)建線程,他們都提供了設(shè)置線程優(yōu)先級(jí)的方法型凳。我們可以通過NSThread的類方法setThreadPriority:設(shè)置優(yōu)先級(jí)丈冬,因?yàn)榫€程的優(yōu)先級(jí)由0.0~1.0表示,所以設(shè)置優(yōu)先級(jí)時(shí)也一樣甘畅。我們也可以通過pthread_setschedparam函數(shù)設(shè)置線程優(yōu)先級(jí)埂蕊。

注意:設(shè)置線程的優(yōu)先級(jí)時(shí)可以在線程運(yùn)行時(shí)設(shè)置。

雖然我們可以調(diào)節(jié)線程的優(yōu)先級(jí)疏唾,但不到必要時(shí)還是不建議調(diào)節(jié)線程的優(yōu)先級(jí)蓄氧。因?yàn)橐坏┱{(diào)高了某個(gè)線程的優(yōu)先級(jí),與低優(yōu)先級(jí)線程的優(yōu)先等級(jí)差距太大槐脏,就有可能導(dǎo)致低優(yōu)先級(jí)線程永遠(yuǎn)得不到運(yùn)行的機(jī)會(huì)喉童,從而產(chǎn)生性能瓶頸。比如說有兩個(gè)線程A和B顿天,起初優(yōu)先級(jí)相差無幾堂氯,那么在執(zhí)行任務(wù)的時(shí)候都會(huì)相繼無序的運(yùn)行,如果將線程A的優(yōu)先級(jí)調(diào)高牌废,并且當(dāng)線程A不會(huì)因?yàn)閳?zhí)行的任務(wù)而阻塞時(shí)咽白,線程B就可能一直不能運(yùn)行,此時(shí)如果線程A中執(zhí)行的任務(wù)需要與線程B中任務(wù)進(jìn)行數(shù)據(jù)交互鸟缕,而遲遲得不到線程B中的結(jié)果晶框,此時(shí)線程A就會(huì)被阻塞,那么程序的性能自然就會(huì)產(chǎn)生瓶頸懂从。

線程執(zhí)行的任務(wù)

在任何平臺(tái)授段,線程存在的價(jià)值和意義都是一樣的,那就是執(zhí)行任務(wù)番甩,不論是方法畴蒲、函數(shù)或一段代碼,除了依照語(yǔ)言語(yǔ)法正常編寫外对室,還有一些額外需要大家注意的事項(xiàng)模燥。

Autorelease Pool

在Xcode4.3之前,我們都處在手動(dòng)管理引用計(jì)數(shù)的時(shí)代掩宜,代碼里滿是retainrelease的方法蔫骂,所以那個(gè)時(shí)候,被線程執(zhí)行的任務(wù)中牺汤,為了能自動(dòng)處理大量對(duì)象的retainrelease操作辽旋,都會(huì)使用NSAutoreleasePool類創(chuàng)建自動(dòng)釋放池,它的作用是將線程中要執(zhí)行的任務(wù)都放在自動(dòng)釋放池中檐迟,自動(dòng)釋放池會(huì)捕獲所有任務(wù)中的對(duì)象补胚,在任務(wù)結(jié)束或線程關(guān)閉之時(shí)自動(dòng)釋放這些對(duì)象:

- (void)myThreadMainRoutine
{

    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // 頂層自動(dòng)釋放池

    // 線程執(zhí)行任務(wù)的邏輯代碼

    [pool release];

}

到了自動(dòng)引用計(jì)數(shù)(ARC)時(shí)代,就不能使用NSAutoreleasePool進(jìn)行自動(dòng)釋放池管理了追迟,而是新加了@autoreleasepool代碼塊語(yǔ)法來創(chuàng)建自動(dòng)釋放池:

- (void)myThreadMainRoutine
{

    @autoreleasepool {
     
     // 線程執(zhí)行任務(wù)的邏輯代碼
     
    }

}

我們知道每個(gè)應(yīng)用程序都是運(yùn)行在一個(gè)主線程里的溶其,而線程都至少得有一個(gè)自動(dòng)釋放池,所以說整個(gè)應(yīng)用其實(shí)是跑在一個(gè)自動(dòng)釋放池中的敦间。大家都知道C系語(yǔ)言中瓶逃,程序的入口函數(shù)都是main函數(shù),當(dāng)我們創(chuàng)建一個(gè)Objective-C的iOS應(yīng)用后廓块,Xcode會(huì)在Supporting Files目錄下自動(dòng)為我們創(chuàng)建一個(gè)main.m文件:

LearnThread-2

main.m這個(gè)文件中就能證實(shí)上面說的那點(diǎn):

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

以上都是在Objective-C中厢绝,但在Swift中,就有點(diǎn)不一樣了带猴,NSAutoreleasePool@autoreleasepool都不能用了昔汉,取而代之的是Swift提供的一個(gè)方法func autoreleasepool(code: () -> ()),接收的參數(shù)為一個(gè)閉包拴清,我們可以這樣使用:

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

根據(jù)尾隨閉包的寫法靶病,還可以這樣使用:

func performInBackground() {

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

有些人可能會(huì)問在ARC的時(shí)代下為什么還要用自動(dòng)釋放池呢?比如在SDWebImage中就大量使用了@autoreleasepool代碼塊贷掖,其原因就是為了避免內(nèi)存峰值嫡秕,大家都知道在MRC時(shí)代,除了retainrelease方法外苹威,還有一個(gè)常用的方法是autorelease昆咽,用來延遲釋放對(duì)象,它釋放對(duì)象的時(shí)機(jī)是當(dāng)前runloop結(jié)束時(shí)牙甫。到了ARC時(shí)代掷酗,雖然不用我們手動(dòng)管理內(nèi)存了,但其自動(dòng)管理的本質(zhì)與MRC時(shí)是一樣的窟哺,只不過由編譯器幫我們?cè)诤线m的地方加上了這三個(gè)方法泻轰,所以說如果在一個(gè)線程執(zhí)行的任務(wù)中大量產(chǎn)生需要autorelease的對(duì)象時(shí),因?yàn)椴荒芗皶r(shí)釋放對(duì)象且轨,所以就很有可能產(chǎn)生內(nèi)存峰值浮声。那么在這種任務(wù)中在特定的時(shí)候使用@autorelease代碼塊虚婿,幫助釋放對(duì)象,就可以有效的防止內(nèi)存峰值的發(fā)生泳挥。

設(shè)置異常處理

在線程執(zhí)行任務(wù)的時(shí)候然痊,難免會(huì)出現(xiàn)異常,如果不能及時(shí)捕獲異常任由其拋出屉符,就會(huì)導(dǎo)致整個(gè)應(yīng)用程序退出剧浸。在Swift2.0中,Apple提供了新的異炒V樱控制處理機(jī)制唆香,讓我們能像Java中一樣形如流水的捕獲處理異常。所以在線程執(zhí)行的任務(wù)中吨艇,我們盡量使用異常處理機(jī)制躬它,提高健壯性。

創(chuàng)建Runloop

大家知道秸应,一個(gè)線程只能執(zhí)行一個(gè)任務(wù)虑凛,當(dāng)任務(wù)結(jié)束后也就意味著這個(gè)線程也要結(jié)束,頻繁的創(chuàng)建線程也是挺消耗資源的一件事软啼,于是就有了常駐線程桑谍,前文介紹線程相關(guān)概念時(shí)也提到過:

簡(jiǎn)單的來說,RunLoop用于管理和監(jiān)聽異步添加到線程中的事件祸挪,當(dāng)有事件輸入時(shí)锣披,系統(tǒng)喚醒線程并將事件分派給RunLoop,當(dāng)沒有需要處理的事件時(shí)贿条,RunLoop會(huì)讓線程進(jìn)入休眠狀態(tài)雹仿。這樣就能讓線程常駐在進(jìn)程中,而不會(huì)過多的消耗系統(tǒng)資源整以,達(dá)到有事做事胧辽,沒事睡覺的效果。

如果想要線程不結(jié)束公黑,那就要被執(zhí)行的任務(wù)不結(jié)束邑商,讓被執(zhí)行的任務(wù)不結(jié)束顯然不靠譜,那么就需要一個(gè)機(jī)制凡蚜,能占著線程人断。該機(jī)制就是事件循環(huán)機(jī)制(Eventloop),體現(xiàn)在代碼中就是一個(gè)do-while循環(huán)朝蜘,不斷的接收事件消息恶迈、處理事件、等待新事件消息暇仲,除非接收到一個(gè)讓其退出的事件消息步做,否則它將一直這么循環(huán)著,線程自然就不會(huì)結(jié)束奈附。Runloop就是管理消息和事件辆床,并提供Eventloop函數(shù)的對(duì)象,線程執(zhí)行的任務(wù)其實(shí)就是在Runloop對(duì)象的Eventloop函數(shù)里運(yùn)行。關(guān)于Runloop更詳細(xì)的知識(shí)及配置
操作在后文中會(huì)有講述。

終止線程

打個(gè)不恰當(dāng)?shù)谋确缴卫妫私K有一死宦焦,或正常生老病死,或非正常出事故意外而亡菇篡,前者尚合情合理后者悲痛欲絕漩符。線程也一樣,有正常終止結(jié)束驱还,也有非正常的強(qiáng)制結(jié)束嗜暴,不管是線程本身還是應(yīng)用程序都希望線程能正常結(jié)束,因?yàn)檎=Y(jié)束也就意味著被執(zhí)行的任務(wù)正常執(zhí)行完成议蟆,從而讓線程處理完后事隨即結(jié)束闷沥,如果在任務(wù)執(zhí)行途中強(qiáng)制終止線程,會(huì)導(dǎo)致線程沒有機(jī)會(huì)處理后事咐容,也就是正常釋放資源對(duì)象等舆逃,這樣會(huì)給應(yīng)用程序帶來例如內(nèi)存溢出這類潛在的問題,所以強(qiáng)烈不推薦強(qiáng)制終止線程的做法戳粒。

如果確實(shí)有在任務(wù)執(zhí)行途中終止線程的需求路狮,那么可以使用Runloop,在任務(wù)執(zhí)行過程中定期查看是否有收到終止任務(wù)的事件消息蔚约,這樣一來可以在任務(wù)執(zhí)行途中判斷出終止任務(wù)的信號(hào)奄妨,然后進(jìn)行終止任務(wù)的相關(guān)處理,比如保存數(shù)據(jù)等苹祟,二來可以讓線程有充分的時(shí)間釋放資源砸抛。

Run Loop

Run Loops是線程中的基礎(chǔ)結(jié)構(gòu),在上文中也提到過苔咪,Run Loops其實(shí)是一個(gè)事件循環(huán)機(jī)制锰悼,用來分配、分派線程接受到的事件任務(wù)团赏,同時(shí)可以讓線程成為一個(gè)常駐線程箕般,即有任務(wù)時(shí)處理任務(wù),沒任務(wù)時(shí)休眠舔清,且不消耗資源丝里。在實(shí)際應(yīng)用時(shí)曲初,Run Loop的生命周期并不全是自動(dòng)完成的,還是需要人工進(jìn)行配置杯聚,不論是Cocoa框架還是Core Foundation框架都提供了Run Loop的相關(guān)對(duì)象對(duì)其進(jìn)行配置和管理臼婆。

注:Core Foundation框架是一組C語(yǔ)言接口,它們?yōu)閕OS應(yīng)用程序提供基本數(shù)據(jù)管理和服務(wù)功能幌绍,比如線程和Run Loop颁褂、端口、Socket傀广、時(shí)間日期等颁独。

在所有的線程中,不論是主線程還是二級(jí)線程伪冰,都不需要顯示的創(chuàng)建Run Loop對(duì)象誓酒,這里的顯示指的是通過任何create打頭的方法創(chuàng)建Run Loop。對(duì)于主線程來說贮聂,當(dāng)應(yīng)用程序通過UIApplicationMain啟動(dòng)時(shí)靠柑,主線程中的Run Loop就已經(jīng)創(chuàng)建并啟動(dòng)了,而且也配置好了吓懈。那么如果是二級(jí)線程歼冰,則需要我們手動(dòng)先獲取Run Loop,然后再手動(dòng)進(jìn)行配置并啟動(dòng)骄瓣。下面的章節(jié)會(huì)向大家詳細(xì)介紹Run Loop的知識(shí)停巷。

注:在二級(jí)線程中獲取Run Loop有兩種方式,通過NSRunloop的類方法currentRunLoop獲取Run Loop對(duì)象(NSRunLoop)榕栏,或者通過Core Foundation框架中的CFRunLoopGetCurrent()函數(shù)獲取當(dāng)前線程的Run Loop對(duì)象(CFRunLoop)畔勤。NSRunLoopCFRunLoop的上層封裝。

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

Run Loop的事件來源

Run Loop有兩個(gè)事件來源扒磁,一個(gè)是Input source庆揪,接收來自其他線程或應(yīng)用程序(進(jìn)程)的異步事件消息,并將消息分派給對(duì)應(yīng)的事件處理方法妨托。另一個(gè)是Timer source缸榛,接收定期循環(huán)執(zhí)行或定時(shí)執(zhí)行的同步事件消息,同樣會(huì)將消息分派給對(duì)應(yīng)的事件處理方法兰伤。

LearnThread-3

上圖展示了Run Loop的兩類事件來源内颗,以及在Input source中的兩種不同的子類型,它們分別對(duì)應(yīng)著Run Loop中不同的處理器敦腔。當(dāng)不同的事件源接收到消息后均澳,通過NSRunLooprunUntilDate:方法啟動(dòng)運(yùn)行Run Loop,將事件消息分派給對(duì)應(yīng)的處理器執(zhí)行,一直到指定的時(shí)間時(shí)退出Run Loop找前。

Run Loop的觀察者

Run Loop的觀察者可以理解為Run Loop自身運(yùn)行狀態(tài)的監(jiān)聽器糟袁,它可以監(jiān)聽Run Loop的下面這些運(yùn)行狀態(tài):

  • Run Loop準(zhǔn)備開始運(yùn)行時(shí)。
  • 當(dāng)Run Loop準(zhǔn)備要執(zhí)行一個(gè)Timer Source事件時(shí)躺盛。
  • 當(dāng)Run Loop準(zhǔn)備要執(zhí)行一個(gè)Input Source事件時(shí)项戴。
  • 當(dāng)Run Loop準(zhǔn)備休眠時(shí)。
  • 當(dāng)Run Loop被進(jìn)入的事件消息喚醒并且還沒有開始讓處理器執(zhí)行事件消息時(shí)槽惫。
  • 退出Run Loop時(shí)周叮。

Run Loop的觀察者在NSRunloop中沒有提供相關(guān)接口,所以我們需要通過Core Foundation框架使用它界斜,可以通過CFRunLoopObserverCreate方法創(chuàng)建Run Loop的觀察者则吟,類型為CFRunLoopObserverRef,它其實(shí)是CFRunLoopObserver的重定義名稱锄蹂。上述的那些可以被監(jiān)聽的運(yùn)行狀態(tài)被封裝在了CFRunLoopActivity結(jié)構(gòu)體中,對(duì)應(yīng)關(guān)系如下:

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

Run Loop的觀察者和Timer事件類似水慨,可以只使用一次得糜,也可以重復(fù)使用,在創(chuàng)建觀察者時(shí)可以設(shè)置晰洒。如果只使用一次朝抖,那么當(dāng)監(jiān)聽到對(duì)應(yīng)的狀態(tài)后會(huì)自行移除,如果是重復(fù)使用的谍珊,那么會(huì)留在Run Loop中多次監(jiān)聽Run Loop相同的運(yùn)行狀態(tài)治宣。

Run Loop Modes

Run Loop Modes可以稱之為Run Loop模式,這個(gè)模式可以理解為對(duì)Run Loop各種設(shè)置項(xiàng)的不同組合砌滞,舉個(gè)例子侮邀,iPhone手機(jī)運(yùn)行的iOS有很多系統(tǒng)設(shè)置項(xiàng),假設(shè)白天我打開蜂窩數(shù)據(jù)贝润,晚上我關(guān)閉蜂窩數(shù)據(jù)绊茧,而打開無線網(wǎng)絡(luò),到睡覺時(shí)我關(guān)閉蜂窩數(shù)據(jù)和無線網(wǎng)絡(luò)打掘,而打開飛行模式华畏。假設(shè)在這三個(gè)時(shí)段中其他的所有設(shè)置項(xiàng)都相同,而只有這三個(gè)設(shè)置項(xiàng)不同尊蚁,那么就可以說我的手機(jī)有三種不同的設(shè)置模式亡笑,對(duì)應(yīng)著不同的時(shí)間段。那么Run Loop的設(shè)置項(xiàng)是什么呢横朋?那自然就是前文中提到的不同的事件來源以及觀察者了仑乌,比如說,Run Loop的模式A(Mode A),只包含接收Timer Source事件源的事件消息以及監(jiān)聽Run Loop運(yùn)行時(shí)的觀察者绝骚,而模式B(Mode B)只包含接收Input Source事件源的事件消息以及監(jiān)聽Run Loop準(zhǔn)備休眠時(shí)和退出Run Loop時(shí)的觀察者耐版,如下圖所示:

LearnThread-4

所以說,Run Loop的模式就是不同類型的數(shù)據(jù)源和不同觀察者的集合压汪,當(dāng)Run Loop運(yùn)行時(shí)要設(shè)置它的模式粪牲,也就是告知Run Loop只需要關(guān)心這個(gè)集合中的數(shù)據(jù)源類型和觀察者,其他的一概不予理會(huì)止剖。那么通過模式腺阳,就可以讓Run Loop過濾掉它不關(guān)心的一些事件,以及避免被無關(guān)的觀察者打擾穿香。如果有不在當(dāng)前模式中的數(shù)據(jù)源發(fā)來事件消息亭引,那只能等Run Loop改為包含有該數(shù)據(jù)源類型的模式時(shí),才能處理事件消息皮获。

在Cocoa框架和Core Foundation框架中焙蚓,已經(jīng)為我們預(yù)定義了一些Run Loop模式:

  • 默認(rèn)模式:在NSRunloop中的定義為NSDefaultRunLoopMode,在CFRunloop中的定義為kCFRunLoopDefaultMode洒宝。該模式包含的事件源囊括了除網(wǎng)絡(luò)鏈接操作的大多數(shù)操作以及時(shí)間事件购公,用于當(dāng)前Run Loop處于空閑狀態(tài)等待事件時(shí),以及Run Loop開始運(yùn)行時(shí)雁歌。
  • NSConnectionReplyMode:該模式用于監(jiān)聽NSConnection相關(guān)對(duì)象的返回結(jié)果和狀態(tài)宏浩,在系統(tǒng)內(nèi)部使用,我們一般不會(huì)使用該模式靠瞎。
  • NSModalPanelRunLoopMode:該模式用于過濾在模態(tài)面板中處理的事件(Mac App)比庄。
  • NSEventTrackingRunLoopMode:該模式用于跟蹤用戶與界面交互的事件。
  • 模式集合:或者叫模式組乏盐,顧名思義就是將多個(gè)模式組成一個(gè)組佳窑,然后將模式組認(rèn)為是一個(gè)模式設(shè)置給Run Loop,在NSRunloop中的定義為NSRunLoopCommonModes父能,在CFRunloop中的定義為kCFRunLoopCommonModes华嘹。系統(tǒng)提供的模式組名為Common Modes,它默認(rèn)包含NSDefaultRunLoopMode法竞、NSModalPanelRunLoopMode耙厚、NSEventTrackingRunLoopMode這三個(gè)模式。

以上五種系統(tǒng)預(yù)定的模式中岔霸,前四種屬于只讀模式薛躬,也就是我們無法修改它們包含的事件源類型和觀察者類型。而模式組我們可以通過Core Foundation框架提供的CFRunLoopAddCommonMode(_ rl: CFRunLoop!, _ mode: CFString!)方法添加新的模式呆细,甚至是我們自定義的模式型宝。這里需要注意的是八匠,既然在使用時(shí),模式組是被當(dāng)作一個(gè)模式使用的趴酣,那么自然可以給它設(shè)置不同類型的事件源或觀察者梨树,當(dāng)給模式組設(shè)置事件源或觀察者時(shí),實(shí)際是給該模式組包含的所有模式設(shè)置岖寞。比如說給模式組設(shè)置了一個(gè)監(jiān)聽Run Loop準(zhǔn)備休眠時(shí)的觀察者抡四,那么該模式組里的所有模式都會(huì)被設(shè)置該觀察者。

Input Source

前文中說過仗谆,Input Sources接收到各種操作輸入事件消息指巡,然后異步的分派給對(duì)應(yīng)事件處理方法。在Input Sources中又分兩大類的事件源隶垮,一類是基于端口事件源(Port-based source)藻雪,在CFRunLoopSourceRef的結(jié)構(gòu)中為source1,主要通過監(jiān)聽?wèi)?yīng)用程序的Mach端口接收事件消息并分派狸吞,該類型的事件源可以主動(dòng)喚醒Run Loop勉耀。另一類是自定義事件源(Custom source),在CFRunLoopSourceRef的結(jié)構(gòu)中為source0蹋偏,一般是接收其他線程的事件消息并分派給當(dāng)前線程的Run Loop瑰排,比如performSwlwctor:onThread:...系列方法,該類型的事件源無法自動(dòng)喚醒Run Loop暖侨,而是需要手動(dòng)將事件源設(shè)置為待執(zhí)行的標(biāo)記,然后再手動(dòng)喚醒Run Loop崇渗。雖然這兩種類型的事件源接收事件消息的方式不一樣字逗,但是當(dāng)接收到消息后,對(duì)消息的分派機(jī)制是完全相同的宅广。

Port-Based Source

Cocoa框架和Core Foundation框架都提供了相關(guān)的對(duì)象和函數(shù)用于創(chuàng)建基于端口的事件源葫掉。在Cocoa框架中,實(shí)現(xiàn)基于端口的事件源主要是通過NSPort類實(shí)現(xiàn)的跟狱,它代表了交流通道俭厚,也就是說在不同的線程的Run Loop中都存在NSPort,那么它們之間就可以通過發(fā)送與接收消息(NSPortMessage)互相通信驶臊。所以我們只需要通過NSPort類的類方法port創(chuàng)建對(duì)象實(shí)例挪挤,然后通過NSRunloop的方法將其添加到Run Loop中,或者在創(chuàng)建二級(jí)線程時(shí)將創(chuàng)建好的NSPort對(duì)象傳入即可关翎,無需我們?cè)僮鱿⒖该拧⑾⑸舷挛摹⑹录吹绕渌渲米萸蓿加蒖un Loop自行配置好了论寨。而在Core Foundation框架中就比較麻煩一些,大多數(shù)配置都需要我們手動(dòng)配置,在后面會(huì)詳細(xì)舉例說明葬凳。

Custom Input Source

Cocoa框架中沒有提供創(chuàng)建自定義事件源的相關(guān)接口绰垂,我們只能通過Core Foundation框架中提供的對(duì)象和函數(shù)創(chuàng)建自定義事件源,手動(dòng)配置事件源各個(gè)階段要處理的邏輯火焰,比如創(chuàng)建CFRunLoopSourceRef事件源對(duì)象劲装,通過CFRunLoopScheduleCallBack回調(diào)函數(shù)配置事件源上下文并注冊(cè)事件源,通過CFRunLoopPerformCallBack回調(diào)函數(shù)處理接收到事件消息后的邏輯荐健,通過CFRunLoopCancelCallBack函數(shù)銷毀事件源等等酱畅,在后文中會(huì)有詳細(xì)舉例說明。

雖然Cocoa框架沒有提供創(chuàng)建自定義事件源的相關(guān)對(duì)象和接口江场,但是它為我們預(yù)定義好了一些事件源纺酸,能讓我們?cè)诋?dāng)前線程、其他二級(jí)線程址否、主線程中執(zhí)行我們希望被執(zhí)行的方法餐蔬,讓我們看看NSObject中的這些方法:

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

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

這兩個(gè)方法允許我們將當(dāng)前線程中對(duì)象的方法讓主線程去執(zhí)行,可以選擇是否阻塞當(dāng)前線程佑附,以及希望被執(zhí)行的方法作為事件消息被何種Run Loop模式監(jiān)聽樊诺。

注:如果在主線程中使用該方法,當(dāng)選擇阻塞當(dāng)前線程音同,那么發(fā)送的方法會(huì)立即被主線程執(zhí)行词爬,若選擇不阻塞當(dāng)前線程,那么被發(fā)送的方法將被排進(jìn)主線程Run Loop的事件隊(duì)列中权均,并等待執(zhí)行顿膨。

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

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

這兩個(gè)方法允許我們給當(dāng)前線程發(fā)送事件消息,當(dāng)前線程接收到消息后會(huì)依次加入Run Loop的事件消息隊(duì)列中叽赊,等待Run Loop迭代執(zhí)行恋沃。該方法還可以指定消息延遲發(fā)送時(shí)間及消息希望被何種Run Loop模式監(jiān)聽。

注:該方法中的延遲時(shí)間并不是延遲Run Loop執(zhí)行事件消息的事件必指,而是延遲向當(dāng)前線程發(fā)送事件消息的時(shí)間囊咏。另外,即便不設(shè)置延遲時(shí)間塔橡,那么發(fā)送的事件消息也不一定立即被執(zhí)行梅割,因?yàn)樵赗un Loop的事件消息隊(duì)列中可以已有若干等待執(zhí)行的消息。

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]?)

這兩個(gè)方法允許我們給其他二級(jí)線程發(fā)送事件消息葛家,前提是要取得目標(biāo)二級(jí)線程的NSThread對(duì)象實(shí)例炮捧,該方法同樣提供了是否阻塞當(dāng)前線程的選項(xiàng)和設(shè)置Run Loop模式的選項(xiàng)。

注:使用該方法給二級(jí)線程發(fā)送事件消息時(shí)要確保目標(biāo)線程正在運(yùn)行惦银,換句話說就是目標(biāo)線程要有啟動(dòng)著的Run Loop咆课。并且保證目標(biāo)線程執(zhí)行的任務(wù)要在應(yīng)用程序代理執(zhí)行applicationDidFinishLaunching:方法前完成末誓,否則主線程就結(jié)束了,目標(biāo)線程自然也就結(jié)束了书蚪。

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

該方法允許我們?cè)诋?dāng)前應(yīng)用程序中創(chuàng)建一個(gè)二級(jí)線程喇澡,并將指定的事件消息發(fā)送給新創(chuàng)建的二級(jí)線程。

class func cancelPreviousPerformRequestsWithTarget(_ aTarget: AnyObject)

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

這兩個(gè)方法是NSObject的類方法殊校,第一個(gè)方法作用是在當(dāng)前線程中取消Run Lop中某對(duì)象通過performSelector:withObject:afterDelay:方法發(fā)送的所有事件消息執(zhí)行請(qǐng)求晴玖。第二個(gè)方法多了兩個(gè)過濾參數(shù),那就是方法名稱和參數(shù)为流,取消指定方法名和參數(shù)的事件消息執(zhí)行請(qǐng)求呕屎。

Timer Source

Timer Source顧名思義就是向Run Loop發(fā)送在將來某一時(shí)間執(zhí)行或周期性重復(fù)執(zhí)行的同步事件消息。當(dāng)某線程不需要其他線程通知而需要自己通知自己執(zhí)行任務(wù)時(shí)就可以用這種事件源敬察。舉個(gè)應(yīng)用場(chǎng)景秀睛,在iOS應(yīng)用中,我們經(jīng)常會(huì)用到搜索功能莲祸,而且一些搜索框具有自動(dòng)搜索的能力蹂安,也就是說不用我們點(diǎn)擊搜索按鈕,只需要輸入完我想要搜索的內(nèi)容就會(huì)自動(dòng)搜索锐帜,大家想一想如果每輸入一個(gè)字就開始立即搜索田盈,不但沒有意義,性能開銷也大缴阎,用戶體驗(yàn)自然也很糟糕允瞧,我們希望當(dāng)輸入完這句話,或至少輸入一部分之后再開始搜索蛮拔,所以我們就可以在開始輸入內(nèi)容時(shí)向執(zhí)行搜索功能的線程發(fā)送定時(shí)搜索的事件消息述暂,讓其在若干時(shí)間后再執(zhí)行搜索任務(wù),這樣就有緩沖時(shí)間輸入搜索內(nèi)容了语泽。

這里需要注意的是Timer Source發(fā)送給Run Loop的周期性執(zhí)行任務(wù)的重復(fù)時(shí)間是相對(duì)時(shí)間。比如說給Run Loop發(fā)送了一個(gè)每隔5秒執(zhí)行一次的任務(wù)视卢,每次執(zhí)行任務(wù)的正常時(shí)間為2秒踱卵,執(zhí)行5次后終止,假設(shè)該任務(wù)被立即執(zhí)行据过,那么當(dāng)該任務(wù)終止時(shí)應(yīng)該歷時(shí)30秒惋砂,但當(dāng)?shù)谝淮螆?zhí)行時(shí)出現(xiàn)了問題,導(dǎo)致任務(wù)執(zhí)行了20秒绳锅,那么該任務(wù)只能再執(zhí)行一次就終止了西饵,執(zhí)行的這一次其實(shí)就是第5次,也就是說不論任務(wù)的執(zhí)行時(shí)間延遲與否鳞芙,Run Loop都會(huì)按照初始的時(shí)間間隔執(zhí)行任務(wù)眷柔,并非按Finish-To-Finish去算的期虾,所以一旦中間任務(wù)有延時(shí)驯嘱,那么就會(huì)丟失任務(wù)執(zhí)行次數(shù)镶苞。關(guān)于Timer Source的使用,在后文中會(huì)有詳細(xì)舉例說明鞠评。

Run Loop內(nèi)部運(yùn)行邏輯

在Run Loop的運(yùn)行生命周期中茂蚓,無時(shí)無刻都伴隨著執(zhí)行等待執(zhí)行的各種任務(wù)以及在不同的運(yùn)行狀態(tài)時(shí)通知不同的觀察者,下面我們看看Run Loop中的運(yùn)行邏輯到底是怎樣的:

  1. 通知對(duì)應(yīng)觀察者Run Loop準(zhǔn)備開始運(yùn)行剃幌。
  2. 通知對(duì)應(yīng)觀察者準(zhǔn)備執(zhí)行定時(shí)任務(wù)聋涨。
  3. 通知對(duì)應(yīng)觀察者準(zhǔn)備執(zhí)行自定義事件源的任務(wù)。
  4. 開始執(zhí)行自定義事件源任務(wù)负乡。
  5. 如果有基于端口事件源的任務(wù)準(zhǔn)備待執(zhí)行牍白,那么立即執(zhí)行該任務(wù)。然后跳到步驟9繼續(xù)運(yùn)轉(zhuǎn)敬鬓。
  6. 通知對(duì)應(yīng)觀察者線程進(jìn)入休眠淹朋。
  7. 如果有下面的事件發(fā)生,則喚醒線程:
  • 接收到基于端口事件源的任務(wù)钉答。
  • 定時(shí)任務(wù)到了該執(zhí)行的時(shí)間點(diǎn)础芍。
  • Run Loop的超時(shí)時(shí)間到期。
  • Run Loop被手動(dòng)喚醒数尿。
  1. 通知對(duì)應(yīng)觀察者線程被喚醒仑性。
  2. 執(zhí)行等待執(zhí)行的任務(wù)。
  • 如果有定時(shí)任務(wù)已啟動(dòng)右蹦,執(zhí)行定時(shí)任務(wù)并重啟Run Loop诊杆。然后跳到步驟2繼續(xù)運(yùn)轉(zhuǎn)。
  • 如果有非定時(shí)器事件源的任務(wù)待執(zhí)行何陆,那么分派執(zhí)行該任務(wù)晨汹。
  • 如果Run Loop被手動(dòng)喚醒,重啟Run Loop贷盲。然后跳轉(zhuǎn)到步驟2繼續(xù)運(yùn)轉(zhuǎn)淘这。
  1. 通知對(duì)應(yīng)觀察者已退出Run Loop。

以上這些Run Loop中的步驟也不是每一步都會(huì)觸發(fā)巩剖,舉一個(gè)例子:
1.對(duì)應(yīng)觀察者接收到通知Run Loop準(zhǔn)備開始運(yùn)行 -> 3.對(duì)應(yīng)觀察者接收到通知Run Loop準(zhǔn)備執(zhí)行自定義事件源任務(wù) -> 4.開始執(zhí)行自定義事件源任務(wù) -> 任務(wù)執(zhí)行完畢且沒有其他任務(wù)待執(zhí)行 -> 6.線程進(jìn)入休眠狀態(tài)铝穷,并通知對(duì)應(yīng)觀察者 -> 7.接收到定時(shí)任務(wù)并喚醒線程 -> 8.通知對(duì)應(yīng)觀察者線程被喚醒 -> 9.執(zhí)行定時(shí)任務(wù)并重啟Run Loop -> 2.通知對(duì)應(yīng)觀察者準(zhǔn)備執(zhí)行定時(shí)任務(wù) -> Run Loop執(zhí)行定時(shí)任務(wù),并在等待下次執(zhí)行任務(wù)的間隔中線程休眠 -> 6.線程進(jìn)入休眠狀態(tài)佳魔,并通知對(duì)應(yīng)觀察者...

這里需要注意的一點(diǎn)是從上面的運(yùn)行邏輯中可以看出曙聂,當(dāng)觀察者接收到執(zhí)行任務(wù)的通知時(shí),Run Loop并沒有真正開始執(zhí)行任務(wù)鞠鲜,所以觀察者接收到通知的時(shí)間與Run Loop真正執(zhí)行任務(wù)的時(shí)間有時(shí)間差宁脊,一般情況下這點(diǎn)時(shí)間差影響不大断国,但如果你需要通過觀察者知道Run Loop執(zhí)行任務(wù)的確切時(shí)間,并根據(jù)這個(gè)時(shí)間要進(jìn)行后續(xù)操作的話朦佩,那么就需要通過結(jié)合多個(gè)觀察者接收到的通知共同確定了并思。一般通過監(jiān)聽準(zhǔn)備執(zhí)行任務(wù)的觀察者、監(jiān)聽線程進(jìn)入休眠的觀察者语稠、監(jiān)聽線程被喚醒的觀察者共同確定執(zhí)行任務(wù)的確切時(shí)間宋彼。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市仙畦,隨后出現(xiàn)的幾起案子输涕,更是在濱河造成了極大的恐慌,老刑警劉巖慨畸,帶你破解...
    沈念sama閱讀 219,589評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件莱坎,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡寸士,警方通過查閱死者的電腦和手機(jī)檐什,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,615評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來弱卡,“玉大人乃正,你說我怎么就攤上這事∩舨” “怎么了瓮具?”我有些...
    開封第一講書人閱讀 165,933評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)凡人。 經(jīng)常有香客問我名党,道長(zhǎng),這世上最難降的妖魔是什么挠轴? 我笑而不...
    開封第一講書人閱讀 58,976評(píng)論 1 295
  • 正文 為了忘掉前任传睹,我火速辦了婚禮,結(jié)果婚禮上岸晦,老公的妹妹穿的比我還像新娘欧啤。我一直安慰自己,他們只是感情好委煤,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,999評(píng)論 6 393
  • 文/花漫 我一把揭開白布堂油。 她就那樣靜靜地躺著修档,像睡著了一般碧绞。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上吱窝,一...
    開封第一講書人閱讀 51,775評(píng)論 1 307
  • 那天讥邻,我揣著相機(jī)與錄音迫靖,去河邊找鬼。 笑死兴使,一個(gè)胖子當(dāng)著我的面吹牛系宜,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播发魄,決...
    沈念sama閱讀 40,474評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼盹牧,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了励幼?” 一聲冷哼從身側(cè)響起汰寓,我...
    開封第一講書人閱讀 39,359評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎苹粟,沒想到半個(gè)月后有滑,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,854評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡嵌削,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,007評(píng)論 3 338
  • 正文 我和宋清朗相戀三年毛好,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片苛秕。...
    茶點(diǎn)故事閱讀 40,146評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡肌访,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出想帅,到底是詐尸還是另有隱情场靴,我是刑警寧澤,帶...
    沈念sama閱讀 35,826評(píng)論 5 346
  • 正文 年R本政府宣布港准,位于F島的核電站旨剥,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏浅缸。R本人自食惡果不足惜轨帜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,484評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望衩椒。 院中可真熱鬧蚌父,春花似錦、人聲如沸毛萌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,029評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)阁将。三九已至膏秫,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間做盅,已是汗流浹背缤削。 一陣腳步聲響...
    開封第一講書人閱讀 33,153評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工窘哈, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人亭敢。 一個(gè)月前我還...
    沈念sama閱讀 48,420評(píng)論 3 373
  • 正文 我出身青樓滚婉,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親帅刀。 傳聞我的和親對(duì)象是個(gè)殘疾皇子让腹,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,107評(píng)論 2 356

推薦閱讀更多精彩內(nèi)容