在討論 runloop 相關(guān)的文章瓤檐,以及分析 AFNetworking(2.x) 源碼的文章中,我們經(jīng)常會(huì)看到關(guān)于利用 runloop 進(jìn)行線程庇榻冢活的分析挠蛉,但如果不求甚解的話,極有可能因此學(xué)會(huì)了一個(gè)錯(cuò)誤的用法肄满,本文就來分析一下其中常見的誤區(qū)谴古。
我提供了一個(gè) Demo质涛,可以在我的Github上下載并運(yùn)行一遍,文章中只提供了部分代碼掰担。
AFN 中的實(shí)現(xiàn)
首先我們知道在舊版本的AFN中使用了 NSURLConnection 來發(fā)起并處理網(wǎng)絡(luò)連接汇陆。AFN 的做法是把網(wǎng)絡(luò)請(qǐng)求的發(fā)起和解析都放在同一個(gè)子線程中進(jìn)行,但由于子線程默認(rèn)不開啟 runloop带饱,它會(huì)向一個(gè) C語(yǔ)言程序那樣在運(yùn)行完所有代碼后退出線程毡代。而網(wǎng)絡(luò)請(qǐng)求是異步的,這會(huì)導(dǎo)致獲取到請(qǐng)求數(shù)據(jù)時(shí)勺疼,線程已經(jīng)退出月趟,代理方法沒有機(jī)會(huì)執(zhí)行。因此恢口,AFN 的做法是使用一個(gè) runloop 來保證線程不死孝宗,也就是下面這段被講爛了的代碼:
+ (void)networkRequestThreadEntryPoint:(id)__unused object {@autoreleasepool{? ? ? ? [[NSThreadcurrentThread] setName:@"AFNetworking"];NSRunLoop*runLoop = [NSRunLoopcurrentRunLoop];? ? ? ? [runLoop addPort:[NSMachPortport] forMode:NSDefaultRunLoopMode];? ? ? ? [runLoop run];? ? }}
當(dāng)然,單獨(dú)看這一個(gè)方法意義不大耕肩,我們稍微結(jié)合一下上下文因妇,看看這個(gè)方法在哪里被調(diào)用:
+ (NSThread*)networkRequestThread {staticNSThread*_networkRequestThread =nil;staticdispatch_once_toncePredicate;dispatch_once(&oncePredicate, ^{? ? ? ? _networkRequestThread = [[NSThreadalloc] initWithTarget:selfselector:@selector(networkRequestThreadEntryPoint:) object:nil];? ? ? ? [_networkRequestThread start];? ? });return_networkRequestThread;}
似乎這種寫法提供了一種思路:“如果需要在子線程中異步執(zhí)行操作,可以利用 runloop 進(jìn)行線程痹持睿活”婚被。但準(zhǔn)確的來說,AFN 的這種寫法并不能實(shí)現(xiàn)我們的需求梳虽,它只是在 AFN 這個(gè)特殊場(chǎng)景下可以工作址芯。
不信你可以嘗試閱讀一下第二段代碼,看看它和平時(shí)使用NSThread時(shí)有什么區(qū)別窜觉,如果沒看出來也無妨谷炸,先記住這段代碼,我們稍后分析禀挫。
NSThread 與內(nèi)存泄漏
這種寫法的第一個(gè)問題就是存在內(nèi)存泄漏旬陡。我們構(gòu)造以下用例,其實(shí)就是把 AFN 的線程創(chuàng)建放在一個(gè)循環(huán)里:
- (void)memoryTest {for(inti =0; i <100000; ++i) {NSThread*thread = [[NSThreadalloc] initWithTarget:selfselector:@selector(run) object:nil];? ? ? ? [thread start];? ? }}- (void)run {@autoreleasepool{NSLog(@"current thread = %@", [NSThreadcurrentThread]);NSRunLoop*runLoop = [NSRunLoopcurrentRunLoop];if(!self.emptyPort) {self.emptyPort= [NSMachPortport];? ? ? ? }? ? ? ? [runLoop addPort:self.emptyPortforMode:NSDefaultRunLoopMode];? ? ? ? [runLoop run];? ? }}
奇怪的事情出現(xiàn)了语婴,盡管是在 ARC 環(huán)境下描孟,內(nèi)存依然不停的上漲。如果我們把run方法中和 runloop 相關(guān)的代碼刪除則不會(huì)出現(xiàn)上述問題砰左,顯然匿醒,開啟 runloop 導(dǎo)致了內(nèi)存泄漏,也就是thread對(duì)象無法釋放缠导。
這里的 emptyPort 用來維持 runloop 的運(yùn)行廉羔,根據(jù)官方文檔的描述,如果 runloop 中沒有任何
modeItem酬核,就不會(huì)啟動(dòng)蜜另,而是立刻退出。之所以選擇作為屬性而不是臨時(shí)變量嫡意,是因?yàn)槲野l(fā)現(xiàn)每次調(diào)用 [NSMachPort port]
方法都會(huì)占用內(nèi)存举瑰,原因暫時(shí)不清楚。
我們可以嘗試手動(dòng)結(jié)束 runloop 并關(guān)閉線程:
- (void)memoryTest {for(inti =0; i <100000; ++i) {NSThread*thread = [[NSThreadalloc] initWithTarget:selfselector:@selector(run) object:nil];? ? ? ? [thread start];? ? ? ? [selfperformSelector:@selector(stopThread) onThread:thread withObject:nilwaitUntilDone:YES];? ? }}- (void)stopThread {CFRunLoopStop(CFRunLoopGetCurrent());NSThread*thread = [NSThreadcurrentThread];? ? [thread cancel];}
很遺憾蔬螟,這依然沒有任何效果此迅。而且不難猜測(cè)是我們沒有能正確的結(jié)束 runloop 的運(yùn)行。
Runloop 的啟動(dòng)與退出
考驗(yàn)英文水平的時(shí)候到了旧巾,首先來看一段官方文檔對(duì)于如何啟動(dòng) runloop 的介紹耸序,它的啟動(dòng)方式一共有三種:
Unconditionally
With a set time limit
In a particular mode
這三種進(jìn)入方式分別對(duì)應(yīng)了三種方法,其中第一種就是我們目前使用的:
run
runUntilDate
runMode:beforeDate:
接下來分別是對(duì)三種方式的介紹鲁猩,文字比較啰嗦坎怪,這里我簡(jiǎn)單總結(jié)一下,有興趣的讀者可以直接看原文廓握。
無條件進(jìn)入是最簡(jiǎn)單的做法搅窿,但也最不推薦。這會(huì)使線程進(jìn)入死循環(huán)隙券,從而不利于控制 runloop男应,結(jié)束 runloop 的唯一方式是 kill 它。
如果我們?cè)O(shè)置了超時(shí)時(shí)間娱仔,那么 runloop 會(huì)在處理完事件或超時(shí)后結(jié)束沐飘,此時(shí)我們可以選擇重新開啟 runloop。這種方式要優(yōu)于前一種
這是相對(duì)來說最優(yōu)秀的方式牲迫,相比于第二種啟動(dòng)方式耐朴,我們可以指定 runloop 以哪種模式運(yùn)行。
查看run方法的文檔還可以知道盹憎,它的本質(zhì)就是無限調(diào)用runMode:beforeDate:方法隔箍,同樣地,runUntilDate:也會(huì)重復(fù)調(diào)用runMode:beforeDate:脚乡,區(qū)別在于它超時(shí)后就不會(huì)再調(diào)用蜒滩。
總結(jié)來說,runMode:beforeDate:表示的是 runloop 的單次調(diào)用奶稠,另外兩者則是循環(huán)調(diào)用俯艰。
相比于 runloop 的啟動(dòng),它的退出就比較簡(jiǎn)單了锌订,只有兩種方法:
設(shè)置超時(shí)時(shí)間
手動(dòng)結(jié)束
如果你使用方法二或三來啟動(dòng) runloop竹握,那么在啟動(dòng)的時(shí)候就可以設(shè)置超時(shí)時(shí)間。然而考慮到目標(biāo)是:“利用 runloop 進(jìn)行線程绷酒活”啦辐,所以我們希望對(duì)線程和它的 runloop 有最精確的控制谓传,比如在完成任務(wù)后立刻結(jié)束,而不是依賴于超時(shí)機(jī)制芹关。
好在根據(jù)文檔的描述续挟,我們還可以使用CFRunLoopStop()方法來手動(dòng)結(jié)束一個(gè) runloop。注意文檔中在介紹利用CFRunLoopStop()手動(dòng)退出時(shí)有下面這句話:
The difference is that you can use this technique on run loops you started unconditionally.
這里的解釋非常容易產(chǎn)生誤會(huì)侥衬,如果在閱讀時(shí)沒有注意到exit和terminate的微小差異就很容易掉進(jìn)坑里诗祸,因?yàn)樵趓un方法的文檔中還有這句話:
If you want the run loop to terminate, you shouldn't use this method
總的來說,如果你還想從 runloop 里面退出來,就不能用run方法。根據(jù)實(shí)踐結(jié)果和文檔糯彬,另外兩種啟動(dòng)方法也無法手動(dòng)退出巫橄。
正確的做法
難道子線程中開啟了 runloop 就無法結(jié)束并釋放了么?這顯然是一個(gè)不合理的結(jié)論,經(jīng)過一番查找,終于在這篇文章里找到了答案,它給出了使用CFRunLoopStop()無效的原因:
CFRunLoopStop() 方法只會(huì)結(jié)束當(dāng)前的 runMode:beforeDate: 調(diào)用脖含,而不會(huì)結(jié)束后續(xù)的調(diào)用。
這也就是為什么 Runloop 的文檔中說CFRunLoopStop()可以exit(退出)一個(gè) runloop投蝉,而在run等方法的文檔中又說這樣會(huì)導(dǎo)致 runloop 無法terminate(終結(jié))养葵。
文章中給出的方案是使用CFRunLoopRun()啟動(dòng) runloop,這樣就可以通過CFRunLoopStop()方法結(jié)束瘩缆。而文檔則推薦了另一種方法:
BOOLshouldKeepRunning =YES;// globalNSRunLoop*theRL = [NSRunLoopcurrentRunLoop];while(shouldKeepRunning && [theRL runMode:NSDefaultRunLoopModebeforeDate:[NSDatedistantFuture]]);
我嘗試了文檔提供的方法关拒,確實(shí)不會(huì)導(dǎo)致內(nèi)存泄漏,但不方便驗(yàn)證 runloop 是否真的開啟庸娱,然后又被終止着绊。所以我實(shí)際采用的是第一種方案:
- (void)memoryTest {for(inti =0; i <100000; ++i) {NSThread*thread = [[NSThreadalloc] initWithTarget:selfselector:@selector(run) object:nil];? ? ? ? [thread start];? ? ? ? [selfperformSelector:@selector(stopThread) onThread:thread withObject:nilwaitUntilDone:YES];? ? }}- (void)stopThread {CFRunLoopStop(CFRunLoopGetCurrent());NSThread*thread = [NSThreadcurrentThread];? ? [thread cancel];}- (void)run {@autoreleasepool{NSLog(@"current thread = %@", [NSThreadcurrentThread]);NSRunLoop*runLoop = [NSRunLoopcurrentRunLoop];if(!self.emptyPort) {self.emptyPort= [NSMachPortport];? ? ? ? }? ? ? ? [runLoop addPort:self.emptyPortforMode:NSDefaultRunLoopMode];? ? ? ? [runLoop runMode:NSRunLoopCommonModesbeforeDate:[NSDatedistantFuture]];? ? }}
驗(yàn)證
采用上述方案后,確實(shí)可以觀察到不會(huì)再出現(xiàn)內(nèi)存泄漏問題熟尉,但這并不是終點(diǎn)归露。因?yàn)槲覀冞€需要驗(yàn)證 runloop 確實(shí)在啟動(dòng)后被關(guān)閉。
為了證明 runloop 確實(shí)啟動(dòng)斤儿,我設(shè)計(jì)了如下方法:
- (void)printSomething {NSLog(@"current thread = %@", [NSThreadcurrentThread]);? ? [selfperformSelector:@selector(printSomething) withObject:nilafterDelay:1];}
我們知道performSelector:withObject:afterDelay依賴于線程的 runloop剧包,因?yàn)樗举|(zhì)上是由一個(gè)定時(shí)器負(fù)責(zé)定期加入到 runloop 中執(zhí)行。所以如果這個(gè)方法可以成功執(zhí)行往果,說明當(dāng)前線程的 runloop 已經(jīng)開啟疆液,否則則說明沒有啟動(dòng)。
為了證明 runloop 可以被終止陕贮,我創(chuàng)建了一個(gè)按鈕堕油,在點(diǎn)擊按鈕時(shí)執(zhí)行以下方法:
- (void)stopButtonDidClicked:(id)sender {? ? [selfperformSelector:@selector(stopRunloop) onThread:self.threadwithObject:nilwaitUntilDone:YES];}- (void)stopRunloop {CFRunLoopStop(CFRunLoopGetCurrent());}
成功的觀察到點(diǎn)擊按鈕后,控制臺(tái)不再有日志輸出,因此證明 runloop 確實(shí)已經(jīng)停止掉缺。
總結(jié)
啰嗦了這么多卜录,其實(shí)是為了研究如何利用 runloop 實(shí)現(xiàn)線程保活眶明。要注意的地方主要有以下點(diǎn):
了解 runloop 實(shí)現(xiàn)線程奔瓒荆活的原理,注意添加的那個(gè)空 port
了解 runloop 導(dǎo)致的線程對(duì)象內(nèi)存泄漏問題
了解 runloop 的幾種啟動(dòng)方式以及彼此之間的關(guān)聯(lián)
了解 runloop 的釋放方式和原理
由于相關(guān)資料的匱乏以及個(gè)人水平有限赘来,雖然竭力研究但仍不保證絕對(duì)的正確性现喳,歡迎交流指正凯傲。
最后犬辰,文章開頭對(duì) AFN 的分析留作一個(gè)簡(jiǎn)單的思考題,為什么 AFN 中的用法不會(huì)有問題冰单?
參考資料