多線程的價(jià)值無需贅述,對(duì)于App性能和用戶體驗(yàn)都有著至關(guān)重要的意義,在iOS開發(fā)中,Apple提供了不同的技術(shù)支持多線程編程,除了跨平臺(tái)的pthread之外,還提供了NSThread、NSOperationQueue、GCD等多線程技術(shù),從本篇Blog開始介紹這幾種多線程技術(shù)的細(xì)節(jié)。
對(duì)于pthread這種跨平臺(tái)的多線程技術(shù),這本PRogramming with POSIX Threads做了詳細(xì)介紹,不再提及。
使用NSThead創(chuàng)建線程有很多方法:
1 | |
12 | |
+performSelectorInBackground:withObject:方法生成子線程。1 | |
創(chuàng)建線程也是有開銷的,iOS下主要成本包括構(gòu)造內(nèi)核數(shù)據(jù)結(jié)構(gòu)(大約1KB)、棧空間(子線程512KB、主線程1MB,不過可以使用方法-setStackSize:自己設(shè)置,注意必須是4K的倍數(shù),而且最小是16K),創(chuàng)建線程大約需要90毫秒的創(chuàng)建時(shí)間。
第二種和第四種方法創(chuàng)建的線程有個(gè)好處是擁有線程的對(duì)象,因此可以使用performSelector:onThread:withObject:waitUntilDone:在該線程上執(zhí)行方法,這是一種非常方便的線程間通訊的方法(相對(duì)于設(shè)置麻煩的NSPort用于通訊),所要執(zhí)行的方法可以直接添加到目標(biāo)線程的Runloop中執(zhí)行。Apple建議使用這個(gè)接口運(yùn)行的方法不要是耗時(shí)或者頻繁的操作,以免子線程的負(fù)載過重。
第三種方法其實(shí)與第一種方法是一樣的,都會(huì)直接生成一個(gè)子線程。
上面四種方法生成的子線程都是detached狀態(tài),即主線程結(jié)束時(shí)這些線程都會(huì)被直接殺死;如果要生成joinable狀態(tài)的子線程,只能使用pthread接口啦。
如果需要,可以設(shè)置線程的優(yōu)先級(jí)(-setThreadPriority:);如果要在線程中保存一些狀態(tài)信息,還可以使用到-threadDictionary得到一個(gè)NSMutableDictionary,以key-value的方式保存信息用于線程內(nèi)讀寫。
要寫一個(gè)有效的子線程入口方法需要注意很多問題,示例代碼:
12345678910111213141516171819202122232425 | |
Run Loop本身并不具備并發(fā)執(zhí)行的功能,但是和多線程開發(fā)息息相關(guān),而且概念令人迷惑,相關(guān)的介紹資料也很少,它的主要的特性如下:
- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate 方法運(yùn)行在某個(gè)特定模式mode。要操作Run Loop,F(xiàn)oundation層和Core Foundation層都有對(duì)應(yīng)的接口可以操作Run Loop。
Foundation層對(duì)應(yīng)的是NSRunLoop:
Core Foundation層對(duì)應(yīng)的是CFRunLoopRef:
兩組接口差不多,不過功能上還是有許多區(qū)別的,例如CF層可以添加自定義Input Source事件源(CFRunLoopSourceRef)和Run Loop觀察者Observer(CFRunLoopObserverRef),很多類似功能的接口特性也是不一樣的。
Run Loop如何運(yùn)行呢?在上一節(jié)NSThread的入口函數(shù)中使用了一種NSRunLoop的使用場景,再看一例:
12345678910111213141516171819202122232425 | |
我們看到入口方法里創(chuàng)建了一個(gè)NSTimer,并且以NSDefaultRunLoopMode模式加入到當(dāng)前子線程的NSRunLoop中。進(jìn)入循環(huán)后肯定會(huì)執(zhí)行-doOtherTask方式法一次,然后再以NSDefaultRunLoopMode模式運(yùn)行NSRunLoop,如果一次Timer事件觸發(fā)處理后,這個(gè)Run Loop會(huì)返回嗎?答案是不會(huì),Why?
NSRunLoop的底層是由CFRunLoopRef實(shí)現(xiàn)的,你可以想象成一個(gè)循環(huán)或者類似linux下select或者epoll,當(dāng)沒有事件觸發(fā)時(shí),你調(diào)用的Run Loop運(yùn)行方法不會(huì)立刻返回,它會(huì)持續(xù)監(jiān)聽其他事件源,如果需要Run Loop會(huì)讓子線程進(jìn)入sleep等待狀態(tài)而不是空轉(zhuǎn),只有當(dāng)Timer Source或者Input Source事件發(fā)生時(shí),子線程才會(huì)被喚醒,然后處理觸發(fā)的事件,然而由于Timer source比較特殊,Timer Source事件發(fā)生處理后,Run Loop運(yùn)行方法- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;也不會(huì)返回;而其他非Timer事件的觸發(fā)處理會(huì)讓這個(gè)Run Loop退出并返回YES。當(dāng)Run Loop運(yùn)行在一個(gè)特定模式時(shí),如果該模式下沒有事件源,運(yùn)行Run Loop會(huì)立刻返回NO。
NSRunLoop的運(yùn)行接口:
12345678 | |
CFRunLoopRef的運(yùn)行接口:
1234567891011 | |
詳細(xì)講解下NSRunLoop的三個(gè)運(yùn)行接口:
- (void)run; 無條件運(yùn)行不建議使用,因?yàn)檫@個(gè)接口會(huì)導(dǎo)致Run Loop永久性的運(yùn)行在NSDefaultRunLoopMode模式,即使使用CFRunLoopStop(runloopRef);也無法停止Run Loop的運(yùn)行,那么這個(gè)子線程就無法停止,只能永久運(yùn)行下去。
- (void)runUntilDate:(NSDate *)limitDate; 有一個(gè)超時(shí)時(shí)間限制比上面的接口好點(diǎn),有個(gè)超時(shí)時(shí)間,可以控制每次Run Loop的運(yùn)行時(shí)間,也是運(yùn)行在NSDefaultRunLoopMode模式。這個(gè)方法運(yùn)行Run Loop一段時(shí)間會(huì)退出給你檢查運(yùn)行條件的機(jī)會(huì),如果需要可以再次運(yùn)行Run Loop。注意CFRunLoopStop(runloopRef);也無法停止Run Loop的運(yùn)行,因此最好自己設(shè)置一個(gè)合理的Run Loop運(yùn)行時(shí)間。示例:
123456 | |
- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate; 有一個(gè)超時(shí)時(shí)間限制,而且設(shè)置運(yùn)行模式這個(gè)接口在非Timer事件觸發(fā)、顯式的用CFRunLoopStop停止Run Loop、到達(dá)limitDate后會(huì)退出返回。如果僅是Timer事件觸發(fā)并不會(huì)讓Run Loop退出返回;如果是PerfromSelector***事件或者其他Input Source事件觸發(fā)處理后,Run Loop會(huì)退出返回YES。示例:
123456 | |
那么如何知道一個(gè)Run Loop是因?yàn)槭裁丛騟xit退出的呢?NSRunLoop中沒有接口可以知道,而需要通過Core Foundation的接口來運(yùn)行CFRunLoopRef,NSRunLoop其實(shí)就是CFRunLoopRef的二次封裝。使用CFRunLoop的接口(C的接口)來運(yùn)行Run Loop,有兩個(gè)接口:
void CFRunLoopRun(void);運(yùn)行在默認(rèn)的kCFRunLoopDefaultMode模式下,直到使用CFRunLoopStop接口停止這個(gè)Run Loop,或者Run Loop的所有事件源都被刪除。
SInt32 CFRunLoopRunInMode(CFStringRef mode, CFTimeInterval seconds, Boolean returnAfterSourceHandled);第一個(gè)參數(shù)是指RunLoop運(yùn)行的模式(例如kCFRunLoopDefaultMode或者kCFRunLoopCommonModes),第二個(gè)參數(shù)是運(yùn)行時(shí)間,第三個(gè)參數(shù)是是否在處理事件后讓Run Loop退出返回。 示例:
1234567891011 | |
如果Run Loop退出返回后,返回值是SInt32類型(signed long),表明Run Loop返回的原因,目前有四種:
123456 | |
注意:Run Loop是可以嵌套調(diào)用的(就像NSAutoreleasePool),例如一個(gè)Run Loop運(yùn)行過程中一個(gè)事件觸發(fā)后,那么在觸發(fā)方法里可以再運(yùn)行當(dāng)前子線程的Run Loop,然后由這個(gè)Run Loop等待其他事件觸發(fā)。不過這種嵌套R(shí)un Loop調(diào)用方式我用的比較少。
以上Run Loop運(yùn)行方法參考本文最后的Sample Code自行嘗試。
iOS下Run Loop的主要運(yùn)行模式mode有:
1) NSDefaultRunLoopMode: 默認(rèn)的運(yùn)行模式,除了NSConnection對(duì)象的事件。
2) NSRunLoopCommonModes: 是一組常用的模式集合,將一個(gè)input source關(guān)聯(lián)到這個(gè)模式集合上,等于將input source關(guān)聯(lián)到這個(gè)模式集合中的所有模式上。在iOS系統(tǒng)中NSRunLoopCommonMode包含NSDefaultRunLoopMode、NSTaskDeathCheckMode、UITrackingRunLoopMode,我有個(gè)timer要關(guān)聯(lián)到這些模式上,一個(gè)個(gè)注冊(cè)很麻煩,我可以用CFRunLoopAddCommonMode([[NSRunLoop currentRunLoop] getCFRunLoop],(__bridge CFStringRef) NSEventTrackingRunLoopMode)將NSEventTrackingRunLoopMode或者其他模式添加到這個(gè)NSRunLoopCommonModes模式中,然后只需要將Timer關(guān)聯(lián)到NSRunLoopCommonModes,即可以實(shí)現(xiàn)Run Loop運(yùn)行在這個(gè)模式集合中任何一個(gè)模式時(shí),這個(gè)Timer都可以被觸發(fā)。默認(rèn)情況下NSRunLoopCommonModes包含了NSDefaultRunLoopMode和UITrackingRunLoopMode。注意:讓Run Loop運(yùn)行在NSRunLoopCommonModes下是沒有意義的,因?yàn)橐粋€(gè)時(shí)刻Run Loop只能運(yùn)行在一個(gè)特定模式下,而不可能是個(gè)模式集合。
3) UITrackingRunLoopMode: 用于跟蹤觸摸事件觸發(fā)的模式(例如UIScrollView上下滾動(dòng)),主線程當(dāng)觸摸事件觸發(fā)時(shí)會(huì)設(shè)置為這個(gè)模式,可以用來在控件事件觸發(fā)過程中設(shè)置Timer。
4) GSEventReceiveRunLoopMode: 用于接受系統(tǒng)事件,屬于內(nèi)部的Run Loop模式。
5) 自定義Mode:可以設(shè)置自定義的運(yùn)行模式Mode,你也可以用CFRunLoopAddCommonMode添加到NSRunLoopCommonModes中。
Run Loop運(yùn)行時(shí)只能以一種固定的模式運(yùn)行,只會(huì)監(jiān)控這個(gè)模式下添加的Timer Source和Input Source,如果這個(gè)模式下沒有相應(yīng)的事件源,Run Loop的運(yùn)行也會(huì)立刻返回的。注意Run Loop不能在運(yùn)行在NSRunLoopCommonModes模式,因?yàn)镹SRunLoopCommonModes其實(shí)是個(gè)模式集合,而不是一個(gè)具體的模式,我可以在添加事件源的時(shí)候使用NSRunLoopCommonModes,只要Run Loop運(yùn)行在NSRunLoopCommonModes中任何一個(gè)模式,這個(gè)事件源都可以被觸發(fā)。
歸根結(jié)底,Run Loop就是個(gè)處理事件的Loop,可以添加Timer和其他Input Source等各種事件源,如果事件源沒有發(fā)生時(shí),Run Loop就可能讓線程進(jìn)入asleep狀態(tài),而事件源發(fā)生時(shí)就會(huì)喚醒休眠的(asleep)的子線程來處理事件。Run Loop的事件源事件源分兩類:Timer Source和Input Source(包括-performSelector:***API調(diào)用簇,Port Input Source、自定義Input Source)。
從上圖可以看出Run Loop就是處理事件的一個(gè)循環(huán),不同的是Timer Source事件處理后不會(huì)使Run Loop結(jié)束,而Input Source事件處理后會(huì)讓Run Loop退出。因此你需要自己的一個(gè)Loop去不斷運(yùn)行Run Loop來處理事件,就像本文開頭的示例那樣。
細(xì)分下Run Loop的事件源:
1) Timer Souce就是創(chuàng)建Timer添加到Run Loop中,沒啥好說的,Cocoa或者Core Foundation都有相應(yīng)接口實(shí)現(xiàn)。需要注意的是scheduledTimerWith****開頭生成的Timer會(huì)自動(dòng)幫你以默認(rèn)NSDefaultRunLoopMode模式加載到當(dāng)前的Run Loop中,而其他接口生成的Timer則需要你手動(dòng)使用-addTimer:forMode添加到Run Loop中。需要額外注意的是Timer的觸發(fā)不會(huì)讓Run Loop返回。(Timer sources deliver events to their handler routines but do not cause the run loop to exit.) 具體實(shí)驗(yàn)可以看下面的Sample Code。
2) Input Source中的-performSelector:***API調(diào)用簇方法,有以下這些接口:
1234567891011 | |
這些API最后兩個(gè)是取消當(dāng)前線程中調(diào)用,其他API是在主線程或者當(dāng)前線程下的Run Loop中執(zhí)行指定的@selector。
3) Port Input Source:概念上也比較簡單,可以用NSMachPort作為線程之間的通訊通道。例如在主線程創(chuàng)建子線程時(shí)傳入一個(gè)NSPort對(duì)象,這樣主線程就可以和這個(gè)子線程通訊啦,如果要實(shí)現(xiàn)雙向通訊,那么子線程也需要回傳給主線程一個(gè)NSPort。
NSPort的子類除了NSMachPort,還可以使用NSMessagePort或者Core Foundation中的CFMessagePortRef。
'NSPortMessage' for instance message is a forward declaration。4) 自定義Input Source:
向Run Loop添加自定義Input Source只能使用Core Foundation的接口:CFRunLoopSourceCreate創(chuàng)建一個(gè)source,CFRunLoopAddSource向Run Loop中添加source,CFRunLoopRemoveSource從Run Loop中刪除source,CFRunLoopSourceSignal通知source,CFRunLoopWakeUp喚醒Run Loop。
Apple官方文檔提供了一個(gè)自定義Input Source使用模式。
主線程持有包含子線程的Run Loop和Source的context對(duì)象,還有一個(gè)用于保存需要運(yùn)行操作的數(shù)據(jù)buffer。主線程需要子線程干活時(shí),首先將需要的操作數(shù)據(jù)添加到數(shù)據(jù)buffer,然后通知source,喚醒子線程Run Loop(因?yàn)樽泳€程可能正在sleep狀態(tài),CFRunLoopWakeUp喚醒Run Loop可以通知線程醒來干活),由于子線程也持有這個(gè)source和數(shù)據(jù)buffer,因此在觸發(fā)喚醒時(shí)可以使用這個(gè)數(shù)據(jù)buffer的數(shù)據(jù)來執(zhí)行相關(guān)操作(需要注意數(shù)據(jù)buffer訪問時(shí)的同步)。
具體實(shí)現(xiàn)參見本文最后的Sample Code。
Core Foundation層的接口可以定義一個(gè)Run Loop的觀察者在Run Loop進(jìn)入以下某個(gè)狀態(tài)時(shí)得到通知:
Observer的創(chuàng)建以及添加到Run Loop中需要使用Core Foundation的接口:
1234567 | |
首先創(chuàng)建Observer的context,然后調(diào)用Core Foundation方法CFRunLoopObserverCreate創(chuàng)建Observer,再加入到當(dāng)前線程的Run Loop中,注意CFRunLoopObserverCreate方法的第二個(gè)參數(shù)是Observer觀察類型,有如下幾種:
12345678910 | |
對(duì)應(yīng)Run Loop的各種事件,kCFRunLoopAllActivities比較特殊,可以觀察所有事件。具體樣例代碼請(qǐng)參考Sample Code。
Run Loop就是一個(gè)處理事件源的循環(huán),你可以控制這個(gè)Run Loop運(yùn)行多久,如果當(dāng)前沒有事件發(fā)生,Run Loop會(huì)讓這個(gè)線程進(jìn)入睡眠狀態(tài)(避免再浪費(fèi)CPU時(shí)間),如果有事件發(fā)生,Run Loop就處理這個(gè)事件。Run Loop處理事件和發(fā)送給Observer通知的流程如下:
什么時(shí)候需要用到Run Loop?官方文檔的建議是:
我個(gè)人在開發(fā)中遇到的需要使用Run Loop的情況有:
RunLoop剛開始用確實(shí)坑很多,理解概念最好的方式還是動(dòng)手寫代碼,寫了個(gè)例子放在GitHub上(工程N(yùn)SThreadExample),歡迎大家討論。
Apple官方也有一個(gè)基于Run Loop的異步網(wǎng)絡(luò)請(qǐng)求示例程序SimpleURLConnections。
新聞熱點(diǎn)
疑難解答
圖片精選
網(wǎng)友關(guān)注