線程與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