本文首發(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í)婶希,不能使用
NSThread
的detachNewThreadSelector: 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_setspecific
和pthread_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í)代掩宜,代碼里滿是retain
和release
的方法蔫骂,所以那個(gè)時(shí)候,被線程執(zhí)行的任務(wù)中牺汤,為了能自動(dòng)處理大量對(duì)象的retain
和release
操作辽旋,都會(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
文件:
在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í)代,除了retain
和release
方法外苹威,還有一個(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
)畔勤。NSRunLoop
是CFRunLoop
的上層封裝。
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)的事件處理方法兰伤。
上圖展示了Run Loop的兩類事件來源内颗,以及在Input source中的兩種不同的子類型,它們分別對(duì)應(yīng)著Run Loop中不同的處理器敦腔。當(dāng)不同的事件源接收到消息后均澳,通過NSRunLoop
的runUntilDate:
方法啟動(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í)的觀察者耐版,如下圖所示:
所以說,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)行邏輯到底是怎樣的:
- 通知對(duì)應(yīng)觀察者Run Loop準(zhǔn)備開始運(yùn)行剃幌。
- 通知對(duì)應(yīng)觀察者準(zhǔn)備執(zhí)行定時(shí)任務(wù)聋涨。
- 通知對(duì)應(yīng)觀察者準(zhǔn)備執(zhí)行自定義事件源的任務(wù)。
- 開始執(zhí)行自定義事件源任務(wù)负乡。
- 如果有基于端口事件源的任務(wù)準(zhǔn)備待執(zhí)行牍白,那么立即執(zhí)行該任務(wù)。然后跳到步驟9繼續(xù)運(yùn)轉(zhuǎn)敬鬓。
- 通知對(duì)應(yīng)觀察者線程進(jìn)入休眠淹朋。
- 如果有下面的事件發(fā)生,則喚醒線程:
- 接收到基于端口事件源的任務(wù)钉答。
- 定時(shí)任務(wù)到了該執(zhí)行的時(shí)間點(diǎn)础芍。
- Run Loop的超時(shí)時(shí)間到期。
- Run Loop被手動(dòng)喚醒数尿。
- 通知對(duì)應(yīng)觀察者線程被喚醒仑性。
- 執(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)淘这。
- 通知對(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í)間宋彼。