RunLoop的應(yīng)用場(chǎng)景剖析

1、場(chǎng)景一

需要保證線程的長(zhǎng)時(shí)間存活

在iOS開發(fā)過程中,有時(shí)間我們不希望某些花費(fèi)很長(zhǎng)時(shí)間的操作長(zhǎng)時(shí)間占用并阻塞主線程,造成界面卡頓線程的存在,我們就需要把此類操作放在子線程來完成。但是當(dāng)子線程操作完成后,子線程就會(huì)銷毀。當(dāng)我們?cè)俅涡枰褂玫阶泳€程的時(shí)候就必須重新創(chuàng)建子線程,當(dāng)次操作頻繁時(shí)就會(huì)頻繁創(chuàng)建頻繁消費(fèi)線程,造成資源浪費(fèi)。如何保證線程的長(zhǎng)時(shí)間存活呢?

①創(chuàng)建NSThread子類

@interface CHThread : NSThread

@end

@implementation CHThread

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

@end

②在viewController中測(cè)試

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    // 1.測(cè)試線程的銷毀
    [self threadTest];
}

// 線程啟動(dòng)后執(zhí)行touch事件使用已經(jīng)創(chuàng)建好的子線程
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [self performSelector:@selector(subThreadOpetion) onThread:self.subThread withObject:nil waitUntilDone:NO];
}

- (void)threadTest
{
    CHThread *subThread = [[CHThread alloc] initWithTarget:self selector:@selector(subThreadEntryPoint) object:nil];
    [subThread setName:@"CHThread"];
    [subThread start];
    self.subThread = subThread;
}

/**
 子線程啟動(dòng)后,啟動(dòng)runloop
 */
- (void)subThreadEntryPoint
{
    @autoreleasepool {
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        //如果注釋了下面這一行,子線程中的任務(wù)并不能正常執(zhí)行
        [runLoop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];
        NSLog(@"啟動(dòng)RunLoop前--%@",runLoop.currentMode);
        [runLoop run];
    }
}

/**
 子線程任務(wù)
 */
- (void)subThreadOpetion
{
    NSLog(@"啟動(dòng)RunLoop后--%@",[NSRunLoop currentRunLoop].currentMode);
    NSLog(@"%@----子線程任務(wù)開始",[NSThread currentThread]);
    [NSThread sleepForTimeInterval:3.0];
    NSLog(@"%@----子線程任務(wù)結(jié)束",[NSThread currentThread]);
}

@end

有幾點(diǎn)需要注意:
1.獲取RunLoop只能使用 [NSRunLoop currentRunLoop] 或 [NSRunLoop mainRunLoop];
2.即使RunLoop開始運(yùn)行,如果RunLoop 中的 modes 為空,或者要執(zhí)行的mode里沒有item,那么RunLoop會(huì)直接在當(dāng)前l(fā)oop中返回,并進(jìn)入睡眠狀態(tài)。
3.自己創(chuàng)建的Thread中的任務(wù)是在kCFRunLoopDefaultMode這個(gè)mode中執(zhí)行的。
4.在子線程創(chuàng)建好后,最好所有的任務(wù)都放在AutoreleasePool中。

在AFNetworking庫中有此應(yīng)用場(chǎng)景的經(jīng)典使用。

+ (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;
}

AFNetworking都是通過調(diào)用 [NSObject performSelector:onThread:..] 將這個(gè)任務(wù)扔到了后臺(tái)線程的 RunLoop 中。

- (void)start {
    [self.lock lock];
    if ([self isCancelled]) {
        [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    } else if ([self isReady]) {
        self.state = AFOperationExecutingState;
        [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    }
    [self.lock unlock];
}

2、場(chǎng)景二:

  • ①我們經(jīng)常會(huì)在應(yīng)用中看到tableView 的header 上是一個(gè)橫向ScrollView,一般我們使用NSTimer,每隔幾秒切換一張圖片??墒钱?dāng)我們滑動(dòng)tableView的時(shí)候,頂部的scollView并不會(huì)切換圖片,這可怎么辦呢?
  • ②.界面上除了有tableView,還有顯示倒計(jì)時(shí)的Label,當(dāng)我們?cè)诨瑒?dòng)tableView時(shí),倒計(jì)時(shí)就停止了,這又該怎么辦呢?
    先看看常用寫法:
// 第一種寫法
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[timer fire];
// 第二種寫法
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];
[timer fire];

上面的兩種寫法其實(shí)是等價(jià)的。第二種寫法,默認(rèn)也是將timer添加到NSDefaultRunLoopMode下的。
然后,我們?cè)诨瑒?dòng)tableView的時(shí)候timerUpdate方法,并不會(huì)調(diào)用。
原因是啥呢?
原因是當(dāng)我們滑動(dòng)scrollView時(shí),主線程的RunLoop 會(huì)切換到UITrackingRunLoopMode這個(gè)Mode,執(zhí)行的也是UITrackingRunLoopMode下的任務(wù)(Mode中的item),而timer 是添加在NSDefaultRunLoopMode下的,所以timer任務(wù)并不會(huì)執(zhí)行,只有當(dāng)UITrackingRunLoopMode的任務(wù)執(zhí)行完畢,runloop切換到NSDefaultRunLoopMode后,才會(huì)繼續(xù)執(zhí)行timer。

要如何解決這一問題呢?
解決方法很簡(jiǎn)單。

解決方案一:我們只需要在添加timer 時(shí),將mode 設(shè)置為NSRunLoopCommonModes即可。
// 第一種寫法修改
NSTimer *timer = [NSTimer timerWithInterval:1.0 target: self selector:@selector(timeUpdate) userInfo: nil repeates: YES];
[[NSRunLoop currentRunLoop] addTimer: timer forModel: NSRunLoopCommonModes];
[timer fire];

// 第二種寫法,因?yàn)槭枪潭ㄌ砑拥絛efaultMode中,就不要用了
解決方案二:將Timer添加在子線程中,但需要注意當(dāng)timer添加到當(dāng)前runloop中后要啟動(dòng)runloop,否則timer只會(huì)執(zhí)行一次。
/首先是創(chuàng)建一個(gè)子線程
- (void)createThread
{
   NSThread *subThread = [[NSThread alloc] initWithTarget:self selector:@selector(timerTest) object:nil];
   [subThread start];
   self.subThread = subThread;
}

// 創(chuàng)建timer,并添加到runloop的mode中
- (void)timerTest
{
   NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
   NSLog(@"啟動(dòng)RunLoop前--%@",runLoop.currentMode);
   NSLog(@"currentRunLoop:%@",[NSRunLoop currentRunLoop]);
       // 第一種寫法,改正前
//    NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];
//    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
//    [timer fire];
   // 第二種寫法
   NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];
   [timer fire];
   [[NSRunLoop currentRunLoop] run];
}

// 倒計(jì)時(shí)更新label
- (void)timerUpdate
{
   NSLog(@"當(dāng)前線程:%@",[NSThread currentThread]);
   NSLog(@"啟動(dòng)RunLoop后--%@",[NSRunLoop currentRunLoop].currentMode);
   NSLog(@"currentRunLoop:%@",[NSRunLoop currentRunLoop]);
   dispatch_async(dispatch_get_main_queue(), ^{
       self.count ++;
       NSString *timerText = [NSString stringWithFormat:@"計(jì)時(shí)器:%ld",self.count];
       self.timerLabel.text = timerText;
   });
}

總結(jié):
1、如果是在主線程中運(yùn)行timer,想要timer在某界面有視圖滾動(dòng)時(shí),依然能正常運(yùn)轉(zhuǎn),那么將timer添加到RunLoop中時(shí),就需要設(shè)置mode 為NSRunLoopCommonModes。
2、如果是在子線程中運(yùn)行timer,那么將timer添加到RunLoop中后,Mode設(shè)置為NSDefaultRunLoopMode或NSRunLoopCommonModes均可,但是需要保證RunLoop在運(yùn)行,且其中有任務(wù)。

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

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

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