線程與RunLoop
線程一般一次只能執行一個任務,執行完成后線程就會退出;如果需要一個執行任務后不退出的永駐線程,可以利用RunLoop實現;
利用RunLoop實現線程保活(常駐線程),我們需要明確線程與RunLoop的關系:
- 線程和 RunLoop 之間是一一對應的,其關系是保存在一個全局的Dictionary里(key是線程地址, value是RunLoop對象);
- 線程剛創建時并沒有RunLoop,如果不主動獲取,那它一直都不會有(主線程的RunLoop在程序啟動時系統就已經獲取,無需再主動獲取);RunLoop的創建是發生在第一次獲取時,RunLoop的銷毀是發生在線程結束時;
- 線程添加了RunLoop,并運行起來;實際上是添加了一個do,while循環,這樣這個線程的程序一直卡在這個do,while循環上,這樣相當于線程的任務一直沒有執行完,所以線程一直不會退出;
AFNetworking2.x中的實現
基于RunLoop的線程保活,早期的AFN就有經典的實現:
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
方法調用:
+ (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
就是創建的常駐線程,這個線程里獲取了RunLoop并運行了;所以這個線程不會被退出、銷毀,除非RunLoop停止;這樣就實現了線程保活功能;
AFNetworking2.x線程保活的作用
- AFNetworking2.x網絡請求是基于
NSURLConnection
實現的;NSURLConnection是被設計成異步發送的,調用了-start方法后,NSURLConnection 會新建一些線程用底層的CFSocket
去發送和接收請求,在發送和接收的一些事件發生后通知原來線程的RunLoop去回調事件。也就是說NSURLConnection的代理回調,也是通過RunLoop觸發的; - 平常我們自己使用NSURLConnection實現網絡請求時,URLConnection的創建與回調一般都是在主線程,主線程本來一直存在所有回調沒有問題;
- AFN作為網絡層框架,在NSURLConnection回調回來之后,對Response 做了一些諸如序列化、錯誤處理的操作的,這些操作都放在子線程去做,處理后接著回到主線程,再通過AFN自己的代理回調給用戶;
AFN的接收NSURLConnection回調的這個線程,正常情況下在執行[connection start]
發送網絡請求后就立即退出了,后續的回調就調用不了;而線程保活就能確保該線程不退出,回調成功;
AFNetworking3.x不再需要線程保活
AFNetworking3.x是基于NSUrlSession
實現的,NSUrlSession參考了AFN2.x的優點,自己維護了一個線程池,做Request線程的調度與管理;因此AFN3.x無需常駐線程,只是用的時候CFRunLoopRun();
開啟RunLoop,結束的時候CFRunLoopStop(CFRunLoopGetCurrent());
停止RunLoop即可;
線程保活代碼實現細節
參考AFN的實現,似乎我們只要依葫蘆畫瓢也能這樣實現線程保活;但其中很多細節需要探究,接下來一步步分析:
為了監聽線程的生命周期,先創建NSThread的子類;
@interface KeepThread : NSThread
@end
@implementation KeepThread
- (void)dealloc {
NSLog(@"%s",__func__);
}
@end
然后依照AFN代碼,創建線程;(這里使用block的方式代替了target的方式,因為target會對self強引用不利于分析內存問題);在開啟RunLoop前后分別打印,以便查看代碼執行狀態;
- (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
按鈕執行代碼,結果是只打印了start,未輸出end;
<KeepThread: 0x6000022695c0>{number = 3, name = (null)},start
這是因為開啟RunLoop并運行后,代碼一直在[runloop run]
這句代碼循環,不會往下執行;block里的代碼沒有執行完,那么線程就不會退出、銷毀;這樣就達到了線程保活的作用,我們也可以從其他方面驗證該線程一直存在著:
- 退出當前vc,vc銷毀;但是可以發現,KeepThread對象self.thread并未調用
-dealloc
方法,線程并不會銷毀; - 添加一個點擊事件,通過
performSelector:onThread:
在線程中執行代碼:
- (void)dosomething {
NSLog(@"%s",__func__);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self performSelector:@selector(dosomething) onThread:self.thread withObject:nil waitUntilDone:NO];
}
每點擊一次,會發現都能正常執行dosomething方法;這也說明線程一直存活,能被喚醒;
不過,以上代碼,一個會令人疑惑的地方是[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
;runLoop中添加了NSMachPort,但是NSMachPort對象并沒有用到;
NSMachPort的確沒有其他實際用處,只是因為一個RunLoop如果沒有任何要處理的事件時,就會退出;為了保證RunLoop不會一執行就退出就需要加上這段代碼;
如果注釋掉這句代碼,那么就會輸出以下結果,線程正常退出了;
<KeepThread: 0x600003cabfc0>{number = 3, name = (null)},start
<KeepThread: 0x600003cabfc0>{number = 3, name = (null)},end
而且這個也不是一定只能添加port
事件,添加timer事件也能實現同樣效果;只是port事件簡單點;
[runLoop addTimer:timer forMode:NSDefaultRunLoopMode]
可控制的常駐線程
以上代碼雖然實現了線程保活,但是并沒有實現手動退出RunLoop,銷毀線程的功能;而且經過上面的分析,這種方式的線程保活還存在內存泄漏的風險(因為thread釋放不了,AFN的使用場景不同本身設計的就是永不釋放同App生命周期一致);接下來我們就來嘗試實現一個可控制的線程,即可以隨時讓保活的線程"死"去;
原理上講,只要保證該線程的RunLoop停止,那么線程就能正常退出;接下來我們就添加一個按鈕,當點擊按鈕時調用代碼主動停止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方法其實就是開啟了一個無限循環,循環里調用runMode:beforeDate:
運行RunLoop;因此我們調用CFRunLoopStop(CFRunLoopGetCurrent());只能退出exit
一個RunLoop,但是并不能終止terminate
外部的while循環;
也就是說以上代碼其實就類似下面這段代碼:
- (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循環以此控制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