runloop常駐子線程

開啟線程需要占用一定的內(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,不退出也??,簡單粗暴
有什么理解有問題的地方歡迎指正!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容