本文首發(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í)等。
配置線程的??臻g大小
在前文中提到過線程對(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ù)雜和麻煩,但這也說明這種方式更為靈活,更能滿足不同的使用場景和需求。比如當(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í)間長短方面并沒有特別之處。
不論是通過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ù)或一段代碼,除了依照語言語法正常編寫外,還有一些額外需要大家注意的事項(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代碼塊語法來創(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系語言中,程序的入口函數(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提供了新的異??刂铺幚頇C(jī)制,讓我們能像Java中一樣形如流水的捕獲處理異常。所以在線程執(zhí)行的任務(wù)中,我們盡量使用異常處理機(jī)制,提高健壯性。
創(chuàng)建Runloop
大家知道,一個(gè)線程只能執(zhí)行一個(gè)任務(wù),當(dāng)任務(wù)結(jié)束后也就意味著這個(gè)線程也要結(jié)束,頻繁的創(chuàng)建線程也是挺消耗資源的一件事,于是就有了常駐線程,前文介紹線程相關(guān)概念時(shí)也提到過:
簡單的來說,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ù)谋确?,人終有一死,或正常生老病死,或非正常出事故意外而亡,前者尚合情合理后者悲痛欲絕。線程也一樣,有正常終止結(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ō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.EntryCFRunLoopActivity.BeforeTimersCFRunLoopActivity.BeforeSourcesCFRunLoopActivity.BeforeWaitingCFRunLoopActivity.AfterWaitingCFRunLoopActivity.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è)僮鱿?、消息上下文、事件源等其他配置,都由Run 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)用場景,在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í)間。