深入研究 Runloop 與線程泵旰祝活

在討論 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í)沒有注意到exitterminate的微小差異就很容易掉進(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ì)有問題冰单?

參考資料

Run Loops 官方文檔

Runloop not being stopped by CFRunLoopStop?

深入理解 RunLoop

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末幌缝,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子诫欠,更是在濱河造成了極大的恐慌涵卵,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件荒叼,死亡現(xiàn)場(chǎng)離奇詭異轿偎,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)被廓,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門坏晦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人嫁乘,你說我怎么就攤上這事昆婿。” “怎么了蜓斧?”我有些...
    開封第一講書人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵仓蛆,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我挎春,道長(zhǎng)看疙,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任直奋,我火速辦了婚禮狼荞,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘帮碰。我一直安慰自己相味,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開白布殉挽。 她就那樣靜靜地躺著丰涉,像睡著了一般拓巧。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上一死,一...
    開封第一講書人閱讀 48,970評(píng)論 1 284
  • 那天肛度,我揣著相機(jī)與錄音,去河邊找鬼投慈。 笑死承耿,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的伪煤。 我是一名探鬼主播加袋,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼抱既!你這毒婦竟也來了职烧?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤防泵,失蹤者是張志新(化名)和其女友劉穎蚀之,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體捷泞,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡足删,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了锁右。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片失受。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖骡湖,靈堂內(nèi)的尸體忽然破棺而出贱纠,到底是詐尸還是另有隱情,我是刑警寧澤响蕴,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布谆焊,位于F島的核電站,受9級(jí)特大地震影響浦夷,放射性物質(zhì)發(fā)生泄漏辖试。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一劈狐、第九天 我趴在偏房一處隱蔽的房頂上張望罐孝。 院中可真熱鬧,春花似錦肥缔、人聲如沸莲兢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)改艇。三九已至收班,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間谒兄,已是汗流浹背摔桦。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留承疲,地道東北人邻耕。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像燕鸽,于是被迫代替她去往敵國(guó)和親兄世。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

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