開啟線程需要占用一定的內(nèi)存空間,且每次開辟子線程都會消耗CPU。如果頻繁使用子線程的情況下,頻繁開辟釋放子線程會消耗大量的CPU和內(nèi)存,而且創(chuàng)建的線程中的任務(wù)執(zhí)行完成之后也就釋放了,不能再次利用,所以造成資源和性能的浪費。這種情況下可以通過創(chuàng)建一個常駐線程來解決。
一、首先看下正常子線程
1、首先創(chuàng)建一個NSThread
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.title = @"Runloop常駐子線程";
self.view.backgroundColor = [UIColor whiteColor];
self.port = [NSMachPort port];
self.thread = [[DPFThread alloc]initWithTarget:self selector:@selector(run) object:nil];
self.thread.name = @"常駐子線程";
[self.thread start];
}
- (void)run {
NSLog(@"run -- ");
}
注:上面的DPFThread是一個繼承自NSThread的類,重寫了dealloc析構(gòu)函數(shù),在dealloc的時候打印一下,方便觀察,下面是DPFThread的.m文件
#import "DPFThread.h"
@implementation DPFThread
- (void)dealloc {
NSLog(@"Thread dealloc");
}
@end
看下執(zhí)行之后的打印
2021-01-13 11:19:36.206434+0800 DPF-Demo[21296:1977346] run --
2021-01-13 11:19:36.206614+0800 DPF-Demo[21296:1977346] DPFThread dealloc
從打印中可以看到,thread在執(zhí)行完run方法之后就直接dealloc了,那么如果想要繼續(xù)在子線程中處理新的邏輯就無法做到
嘗試一下:
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
//讓test方法在線程thread上實現(xiàn)
[self performSelector:@selector(test) onThread:_thread withObject:nil waitUntilDone:NO];
}
-(void)test{
NSLog(@"test -- %@",[NSThread currentThread]);
}
在touchesBegan中執(zhí)行一下打印。結(jié)果是瘋狂點擊都沒有用。因為thread已經(jīng)被dealloc,無法處理任何操作
二、常駐子線程
常駐子線程無非就是線程?;睿詈唵尉褪翘砑右粋€runloop
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.title = @"Runloop常駐子線程";
self.view.backgroundColor = [UIColor whiteColor];
self.port = [NSMachPort port];
self.thread = [[DPFThread alloc]initWithTarget:self selector:@selector(run) object:nil];
self.thread.name = @"常駐子線程";
[self.thread start];
}
- (void)run {
NSLog(@"run -- ");
//添加runloop
@autoreleasepool {
//添加Port 實時監(jiān)聽
[[NSRunLoop currentRunLoop] addPort:self.port forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
}
}
這個時候繼續(xù)點擊屏幕,thread就能響應(yīng)方法了,看下打印。thread的dealloc沒有執(zhí)行,點擊屏幕也正常打印,說明對于我們當(dāng)前子線程來說是一個常駐子線程,沒有被釋放
2021-01-13 11:45:50.202508+0800 DPF-Demo[21492:1993683] run --
2021-01-13 11:45:51.562321+0800 DPF-Demo[21492:1993683] test -- <DPFThread: 0x6000000d5cc0>{number = 7, name = 常駐子線程}
2021-01-13 11:45:57.688076+0800 DPF-Demo[21492:1993683] test -- <DPFThread: 0x6000000d5cc0>{number = 7, name = 常駐子線程}
事情到這里就結(jié)束了嗎,沒那么簡單,當(dāng)我頁面返回的時候,controller的dealloc并沒有被執(zhí)行。所以關(guān)鍵點是這個常駐子線程怎么來退出。
那這個常駐子線程本質(zhì)上讓這個runloop對線程?;?,是對這個runloop開啟并添加了一個sources事件。
那首先想到的第一個方法是:移除runloop中的sources事件。
(一)移除runloop中的sources事件?
實踐一下:開啟runloop之前添加任務(wù),在2秒鐘后移除runloop的sources事件。并在[[NSRunLoop currentRunLoop] run];后面添加NSLog打印。如果runloop退出,那么才會執(zhí)行NSLog打印。
原因:runloop是個do while循環(huán),如果循環(huán)不被打破,該條線程后面的代碼將永遠無法執(zhí)行
//添加一個移除port的方法
- (void)removeSourceOrTimer {
NSLog(@"%s",__func__);
[[NSRunLoop currentRunLoop] removePort:self.port forMode:NSDefaultRunLoopMode];
}
//修改開啟runloop之前添加任務(wù),在2秒鐘后移除runloop的sources事件
- (void)run {
NSLog(@"run -- ");
//添加runloop
@autoreleasepool {
//添加Port 實時監(jiān)聽
[[NSRunLoop currentRunLoop] addPort:self.port forMode:NSDefaultRunLoopMode];
//嘗試在2秒后移除port(sources1),如果打印了"runloop 退出了",證明runloop結(jié)束了運行循環(huán)
[self performSelector:@selector(removeSourceOrTimer) withObject:nil afterDelay:2];
[[NSRunLoop currentRunLoop] run];
//如果runloop退出了,這句NSLog才會執(zhí)行,否者這個線程一直在runloop循環(huán)中,不會繼續(xù)執(zhí)行下去
NSLog(@"runloop 退出了");
}
}
看下打印:runloop退出了,此時再去點擊屏幕,touchesBegan方法中的打印無法再響應(yīng)。返回的時候controller也釋放了。(如果退出頁面之前點擊屏幕)
2021-01-13 14:21:55.711389+0800 DPF-Demo[21996:2070754] run --
2021-01-13 14:21:57.713570+0800 DPF-Demo[21996:2070754] -[RunloopResidentThreadVC removeSourceOrTimer]
2021-01-13 14:21:57.713955+0800 DPF-Demo[21996:2070754] runloop 退出了
2021-01-13 14:22:00.409427+0800 DPF-Demo[21996:2070121] RunloopResidentThreadVC dealloc
2021-01-13 14:22:00.409766+0800 DPF-Demo[21996:2070121] DPFThread dealloc
事情到這里就結(jié)束了嗎,并沒有,接下去看。將[self performSelector的waitUntilDone的值改成YES,runloop退出后再點擊屏幕,crash了
Thread 1: EXC_BAD_ACCESS (code=1, address=0x700009cf8100)
注:performSelector方法中的waitUntilDone后面的BOOL參數(shù)。
當(dāng)為yes的時候,先讓線程運行setEnd中的一些操作,之后再進行當(dāng)前線程中的操作。
當(dāng)為no的時候,先進行當(dāng)前線程中的操作,之后讓線程運行setEnd中的一些操作。
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
//讓test方法在線程thread上實現(xiàn)
[self performSelector:@selector(test) onThread:_thread withObject:nil waitUntilDone:YES];
}
其實這種移除方式并不合理,原因是你移除了當(dāng)前的port,但是并不確定runloop中是否還有其他的sources。比如你在runloop退出前,點擊屏幕,結(jié)果又不一樣,runloop無法退出。因此此方法pass。
(二)退出線程?
都知道我們的線程和runloop是一一對應(yīng)的,那我們退出線程能否打破runloop循環(huán),嘗試一下:
- (void)removeSourceOrTimer {
NSLog(@"%s",__func__);
// [[NSRunLoop currentRunLoop] removePort:self.port forMode:NSDefaultRunLoopMode];
[NSThread exit];
}
看下打印:runloop并沒有退出,并且點擊屏幕的時候,直接crash。原因是當(dāng)前線程已經(jīng)不在了。所以這種方法也不行,pass
2021-01-13 14:49:41.553972+0800 DPF-Demo[22170:2089826] run --
2021-01-13 14:49:43.556331+0800 DPF-Demo[22170:2089826] -[RunloopResidentThreadVC removeSourceOrTimer]
那合理的方法是什么呢:
加一個標記exitRunloop判斷runloop是否需要繼續(xù)循環(huán),將run的方法改為 [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate date]];
- (void)run {
NSLog(@"run -- ");
//添加runloop
@autoreleasepool {
//添加Port 實時監(jiān)聽
[[NSRunLoop currentRunLoop] addPort:self.port forMode:NSDefaultRunLoopMode];
//嘗試在2秒后移除port(sources1),如果打印了"runloop 退出了",證明runloop結(jié)束了
[self performSelector:@selector(removeSourceOrTimer) withObject:nil afterDelay:5];
while (!self.exitRunloop) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate date]];
}
// [[NSRunLoop currentRunLoop] run];
NSLog(@"runloop 退出了");
}
}
- (void)removeSourceOrTimer {
NSLog(@"%s",__func__);
// [[NSRunLoop currentRunLoop] removePort:self.port forMode:NSDefaultRunLoopMode];
// [NSThread exit];
self.exitRunloop = YES;
}
當(dāng)然如果不需要退出這個常駐子線程,那直接用run,不退出也??,簡單粗暴
有什么理解有問題的地方歡迎指正!