基于RunLoop進行線程保活的簡單分析

線程與RunLoop

線程一般一次只能執(zhí)行一個任務(wù)煌贴,執(zhí)行完成后線程就會退出御板;如果需要一個執(zhí)行任務(wù)后不退出的永駐線程,可以利用RunLoop實現(xiàn)崔步;
利用RunLoop實現(xiàn)線程蔽人保活(常駐線程),我們需要明確線程與RunLoop的關(guān)系:

  • 線程和 RunLoop 之間是一一對應(yīng)的井濒,其關(guān)系是保存在一個全局的Dictionary里(key是線程地址灶似, value是RunLoop對象);
  • 線程剛創(chuàng)建時并沒有RunLoop瑞你,如果不主動獲取酪惭,那它一直都不會有(主線程的RunLoop在程序啟動時系統(tǒng)就已經(jīng)獲取,無需再主動獲日呒住)春感;RunLoop的創(chuàng)建是發(fā)生在第一次獲取時,RunLoop的銷毀是發(fā)生在線程結(jié)束時;
  • 線程添加了RunLoop鲫懒,并運行起來嫩实;實際上是添加了一個do,while循環(huán),這樣這個線程的程序一直卡在這個do,while循環(huán)上窥岩,這樣相當于線程的任務(wù)一直沒有執(zhí)行完甲献,所以線程一直不會退出;

AFNetworking2.x中的實現(xiàn)

基于RunLoop的線程彼桃恚活晃洒,早期的AFN就有經(jīng)典的實現(xiàn):

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];

        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}

方法調(diào)用:

+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        [_networkRequestThread start];
    });

    return _networkRequestThread;
}

_networkRequestThread就是創(chuàng)建的常駐線程,這個線程里獲取了RunLoop并運行了朦乏;所以這個線程不會被退出球及、銷毀,除非RunLoop停止呻疹;這樣就實現(xiàn)了線程背砸活功能;

AFNetworking2.x線程惫舸福活的作用

  • AFNetworking2.x網(wǎng)絡(luò)請求是基于NSURLConnection實現(xiàn)的际歼;NSURLConnection是被設(shè)計成異步發(fā)送的,調(diào)用了-start方法后姑蓝,NSURLConnection 會新建一些線程用底層的CFSocket去發(fā)送和接收請求鹅心,在發(fā)送和接收的一些事件發(fā)生后通知原來線程的RunLoop去回調(diào)事件。也就是說NSURLConnection的代理回調(diào)纺荧,也是通過RunLoop觸發(fā)的旭愧;
  • 平常我們自己使用NSURLConnection實現(xiàn)網(wǎng)絡(luò)請求時,URLConnection的創(chuàng)建與回調(diào)一般都是在主線程宙暇,主線程本來一直存在所有回調(diào)沒有問題输枯;
  • AFN作為網(wǎng)絡(luò)層框架,在NSURLConnection回調(diào)回來之后占贫,對Response 做了一些諸如序列化桃熄、錯誤處理的操作的,這些操作都放在子線程去做型奥,處理后接著回到主線程瞳收,再通過AFN自己的代理回調(diào)給用戶;
    AFN的接收NSURLConnection回調(diào)的這個線程厢汹,正常情況下在執(zhí)行[connection start]發(fā)送網(wǎng)絡(luò)請求后就立即退出了螟深,后續(xù)的回調(diào)就調(diào)用不了;而線程碧淘幔活就能確保該線程不退出界弧,回調(diào)成功凡蜻;

AFNetworking3.x不再需要線程保活

AFNetworking3.x是基于NSUrlSession實現(xiàn)的垢箕,NSUrlSession參考了AFN2.x的優(yōu)點划栓,自己維護了一個線程池,做Request線程的調(diào)度與管理条获;因此AFN3.x無需常駐線程茅姜,只是用的時候CFRunLoopRun();開啟RunLoop,結(jié)束的時候CFRunLoopStop(CFRunLoopGetCurrent());停止RunLoop即可月匣;

線程保活代碼實現(xiàn)細節(jié)

參考AFN的實現(xiàn)奋姿,似乎我們只要依葫蘆畫瓢也能這樣實現(xiàn)線程背活;但其中很多細節(jié)需要探究称诗,接下來一步步分析:

為了監(jiān)聽線程的生命周期萍悴,先創(chuàng)建NSThread的子類;

@interface KeepThread : NSThread

@end

@implementation KeepThread

- (void)dealloc {
    NSLog(@"%s",__func__);
}

@end

然后依照AFN代碼寓免,創(chuàng)建線程癣诱;(這里使用block的方式代替了target的方式,因為target會對self強引用不利于分析內(nèi)存問題);在開啟RunLoop前后分別打印袜香,以便查看代碼執(zhí)行狀態(tài)撕予;

- (IBAction)start:(id)sender {
    self.thread = [[KeepThread alloc] initWithBlock:^{
        NSLog(@"%@,start", [NSThread currentThread]);
        
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
        
        NSLog(@"%@,end", [NSThread currentThread]);
    }];
    [self.thread start];
}

然后點擊vc的start按鈕執(zhí)行代碼,結(jié)果是只打印了start蜈首,未輸出end实抡;

<KeepThread: 0x6000022695c0>{number = 3, name = (null)},start

這是因為開啟RunLoop并運行后,代碼一直在[runloop run]這句代碼循環(huán)欢策,不會往下執(zhí)行吆寨;block里的代碼沒有執(zhí)行完,那么線程就不會退出踩寇、銷毀啄清;這樣就達到了線程保活的作用俺孙,我們也可以從其他方面驗證該線程一直存在著:

  • 退出當前vc辣卒,vc銷毀;但是可以發(fā)現(xiàn)睛榄,KeepThread對象self.thread并未調(diào)用-dealloc方法添寺,線程并不會銷毀;
  • 添加一個點擊事件懈费,通過performSelector:onThread:在線程中執(zhí)行代碼:
- (void)dosomething {
    NSLog(@"%s",__func__);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self performSelector:@selector(dosomething) onThread:self.thread withObject:nil waitUntilDone:NO];
}

每點擊一次计露,會發(fā)現(xiàn)都能正常執(zhí)行dosomething方法;這也說明線程一直存活,能被喚醒票罐;

不過叉趣,以上代碼,一個會令人疑惑的地方是[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];该押;runLoop中添加了NSMachPort疗杉,但是NSMachPort對象并沒有用到;
NSMachPort的確沒有其他實際用處蚕礼,只是因為一個RunLoop如果沒有任何要處理的事件時烟具,就會退出;為了保證RunLoop不會一執(zhí)行就退出就需要加上這段代碼奠蹬;
如果注釋掉這句代碼朝聋,那么就會輸出以下結(jié)果,線程正常退出了囤躁;

<KeepThread: 0x600003cabfc0>{number = 3, name = (null)},start
<KeepThread: 0x600003cabfc0>{number = 3, name = (null)},end

而且這個也不是一定只能添加port事件冀痕,添加timer事件也能實現(xiàn)同樣效果;只是port事件簡單點狸演;

[runLoop addTimer:timer forMode:NSDefaultRunLoopMode]
可控制的常駐線程

以上代碼雖然實現(xiàn)了線程毖陨撸活,但是并沒有實現(xiàn)手動退出RunLoop宵距,銷毀線程的功能腊尚;而且經(jīng)過上面的分析,這種方式的線程甭模活還存在內(nèi)存泄漏的風險(因為thread釋放不了跟伏,AFN的使用場景不同本身設(shè)計的就是永不釋放同App生命周期一致);接下來我們就來嘗試實現(xiàn)一個可控制的線程翩瓜,即可以隨時讓笔馨猓活的線程"死"去;
原理上講兔跌,只要保證該線程的RunLoop停止勘高,那么線程就能正常退出;接下來我們就添加一個按鈕坟桅,當點擊按鈕時調(diào)用代碼主動停止RunLoop:

- (IBAction)stop:(id)sender {
    [self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:NO];
}

- (void)stopThread {
    CFRunLoopStop(CFRunLoopGetCurrent());
}

令人意外的是华望,當點擊停止后,沒有任何輸出仅乓,線程還是沒有退出赖舟;

這個其實可以從RunLoop的run方法官方文檔中找到答案:

If no input sources or timers are attached to the run loop, this method exits immediately; otherwise, it runs the receiver in the NSDefaultRunLoopMode by repeatedly invoking runMode:beforeDate:. In other words, this method effectively begins an infinite loop that processes data from the run loop’s input sources and timers.
Manually removing all known input sources and timers from the run loop is not a guarantee that the run loop will exit. macOS can install and remove additional input sources as needed to process requests targeted at the receiver’s thread. Those sources could therefore prevent the run loop from exiting.
If you want the run loop to terminate, you shouldn't use this method. Instead, use one of the other run methods and also check other arbitrary conditions of your own, in a loop. A simple example would be:

NSRunLoop *theRL = [NSRunLoop currentRunLoop];
while (shouldKeepRunning && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);

大概意思是,run方法其實就是開啟了一個無限循環(huán)夸楣,循環(huán)里調(diào)用runMode:beforeDate:運行RunLoop宾抓;因此我們調(diào)用CFRunLoopStop(CFRunLoopGetCurrent());只能退出exit一個RunLoop子漩,但是并不能終止terminate外部的while循環(huán);
也就是說以上代碼其實就類似下面這段代碼:

- (void)threadRun {
    @autoreleasepool {
        NSLog(@"%@,start", [NSThread currentThread]);
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        while (YES) {
            [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        }

        NSLog(@"%@,end", [NSThread currentThread]);
    }
}

因此通過run方法開啟的RunLoop無法終止石洗;如果想終止幢泼,就需要使用 runMode:beforeDate:.方式,并使用一個BOOL變量控制while循環(huán)以此控制RunLoop讲衫;

可控制的線程甭瓶茫活的最終代碼如下:

- (void)stopThread {
    CFRunLoopStop(CFRunLoopGetCurrent());
    self.isStop = YES;
}

- (IBAction)start:(id)sender {
    __weak typeof (self) weakSelf = self;
    self.thread = [[KeepThread alloc] initWithBlock:^{
        NSLog(@"%@,start", [NSThread currentThread]);

        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        while (!weakSelf.isStop) {
            [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        }
        
        NSLog(@"%@,end", [NSThread currentThread]);
    }];
    [self.thread start];
}

參考:
AFNetworking3.0后為什么不再需要常駐線程?
深入研究 Runloop 與線程鄙媸蓿活
深入理解RunLoop

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末招驴,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子枷畏,更是在濱河造成了極大的恐慌别厘,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,378評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件矿辽,死亡現(xiàn)場離奇詭異,居然都是意外死亡郭厌,警方通過查閱死者的電腦和手機袋倔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,970評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來折柠,“玉大人宾娜,你說我怎么就攤上這事∩仁郏” “怎么了前塔?”我有些...
    開封第一講書人閱讀 168,983評論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長承冰。 經(jīng)常有香客問我华弓,道長,這世上最難降的妖魔是什么困乒? 我笑而不...
    開封第一講書人閱讀 59,938評論 1 299
  • 正文 為了忘掉前任寂屏,我火速辦了婚禮,結(jié)果婚禮上娜搂,老公的妹妹穿的比我還像新娘迁霎。我一直安慰自己,他們只是感情好百宇,可當我...
    茶點故事閱讀 68,955評論 6 398
  • 文/花漫 我一把揭開白布考廉。 她就那樣靜靜地躺著,像睡著了一般携御。 火紅的嫁衣襯著肌膚如雪昌粤。 梳的紋絲不亂的頭發(fā)上既绕,一...
    開封第一講書人閱讀 52,549評論 1 312
  • 那天,我揣著相機與錄音婚苹,去河邊找鬼岸更。 笑死,一個胖子當著我的面吹牛膊升,可吹牛的內(nèi)容都是我干的怎炊。 我是一名探鬼主播,決...
    沈念sama閱讀 41,063評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼廓译,長吁一口氣:“原來是場噩夢啊……” “哼评肆!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起非区,我...
    開封第一講書人閱讀 39,991評論 0 277
  • 序言:老撾萬榮一對情侶失蹤瓜挽,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后征绸,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體久橙,經(jīng)...
    沈念sama閱讀 46,522評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,604評論 3 342
  • 正文 我和宋清朗相戀三年管怠,在試婚紗的時候發(fā)現(xiàn)自己被綠了淆衷。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,742評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡渤弛,死狀恐怖祝拯,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情她肯,我是刑警寧澤佳头,帶...
    沈念sama閱讀 36,413評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站晴氨,受9級特大地震影響康嘉,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜籽前,卻給世界環(huán)境...
    茶點故事閱讀 42,094評論 3 335
  • 文/蒙蒙 一凄鼻、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧聚假,春花似錦块蚌、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,572評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至瘪贱,卻和暖如春纱控,著一層夾襖步出監(jiān)牢的瞬間辆毡,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,671評論 1 274
  • 我被黑心中介騙來泰國打工甜害, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留舶掖,地道東北人。 一個月前我還...
    沈念sama閱讀 49,159評論 3 378
  • 正文 我出身青樓尔店,卻偏偏與公主長得像眨攘,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子嚣州,可洞房花燭夜當晚...
    茶點故事閱讀 45,747評論 2 361

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