iOS定時器-NSTimer&GCD定時器

在iOS開發(fā)中定時器是我們經(jīng)常遇到的需求,常用到的定時器表示方式有NSTimer、GCD,那么它們之間有什么樣的區(qū)別呢?本文將從兩者的基本使用開始剖析它們之間的區(qū)別。

1、NSTimer

1.1、NSTimer簡介

NSTimer是iOS中最基本的定時器。NSTimer是通過RunLoop來實現(xiàn)的,在一般的情況下NSTimer作為定時器是比較準(zhǔn)確的,但是如果當(dāng)前的耗時操作較多時,可能出現(xiàn)延時問題。同時,因為受到RunLoop的支配,NSTimer會受到RunLoopMode的影響。在創(chuàng)建NSTimer的時候默認是被加到defaultMode的,但是如果在一個滑動的視圖中如tableview,當(dāng)RunLoop的mode發(fā)生變化時,當(dāng)前的NSTimer就不會工作了,這就是我們在開發(fā)中遇到的NSTimer用在tableview中,當(dāng)tableview滾動的時候NSTimer停止工作的原因,所以我們在創(chuàng)建NSTimer的時候?qū)⑵浼拥絉unLoop指定mode為NSRunLoopCommonModes。

1.2、NSTimer基本使用

NSTimer的初始化方式有兩種,分別是invocationselector兩種調(diào)用方式,這兩種方式區(qū)別不大,但是selector的方式更加簡便。

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;

下面我們來看下這兩種方式的使用。

1.2.1、selector方式

使用selector方式初始化NSTimer比較簡單,只需要指定執(zhí)行的方法和是否循環(huán)就可以了。

- (void)selectorType {
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];

    // NSDefaultRunLoopMode模式,切換RunLoop模式,定時器停止工作.
    // [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    
    // UITrackingRunLoopMode模式,切換RunLoop模式,定時器停止工作.
    // [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
    
    // common modes的模式,以下三種模式的組合模式 NSDefaultRunLoopMode & NSModalPanelRunLoopMode & NSEventTrackingRunLoopMode
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
- (void)timerTest {
    NSLog(@"hello");
}

在上一個小節(jié)講過,NSTimer依賴于RunLoop,需要把初始化好的timer添加到RunLoop中,對于RunLoop的幾種模式在上面的代碼注釋中有說明。
這段代碼的運行結(jié)果就是每隔兩秒鐘就會打印一次“hello”

打印結(jié)果:
2020-03-16 17:55:24.123435+0800 ThreadDemo[3845:9977585] hello
2020-03-16 17:55:26.122417+0800 ThreadDemo[3845:9977585] hello
2020-03-16 17:55:28.123599+0800 ThreadDemo[3845:9977585] hello
2020-03-16 17:55:30.122504+0800 ThreadDemo[3845:9977585] hello

1.2.1、invocation方式

通過invocation方式初始化timer相對于來說會稍微復(fù)雜一些,最主要的是invocation參數(shù)。同樣的也需要手動將timer加入到RunLoop中。

- (void)invocationType {
    // 獲取到方法的簽名
    NSMethodSignature *signature = [[self class]instanceMethodSignatureForSelector:@selector(timerTest)];
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
    invocation.target = self;
    invocation.selector = @selector(timerTest);

    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 invocation:invocation repeats:YES];
    [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
}

- (void)timerTest {
    NSLog(@"hello");
}

這段代碼的運行結(jié)果就是每隔兩秒鐘就會打印一次“hello”

打印結(jié)果:
2020-03-16 22:54:48.964318+0800 ThreadDemo[6400:10171057] hello
2020-03-16 22:54:50.964530+0800 ThreadDemo[6400:10171057] hello
2020-03-16 22:54:52.964403+0800 ThreadDemo[6400:10171057] hello
2020-03-16 22:54:54.964780+0800 ThreadDemo[6400:10171057] hello

1.2.3、scheduledTimerWithTimeInterval方法

在上面列舉的API中其實有scheduledTimerWithTimeInterval方法可以創(chuàng)建timer,這個方法和timerWithTimeInterval的區(qū)別就在于前者會默認的將timer添加到了RunLoop,并且currentRunLoop是NSDefaultRunLoopMode,而后者是需要開發(fā)者手動的將timer添加到RunLoop中。

- (void)scheduledTimer {
//    NSTimer *timer1 = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];

    NSMethodSignature *signature = [[self class] instanceMethodSignatureForSelector:@selector(timerTest)];
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
    invocation.target = self;
    invocation.selector = @selector(timerTest);

    NSTimer *timer2 = [NSTimer scheduledTimerWithTimeInterval:2.0 invocation:invocation repeats:YES];
}
- (void)timerTest {
    NSLog(@"hello");
}

這段代碼的運行結(jié)果就是每隔兩秒鐘就會打印一次“hello”

打印結(jié)果:
2020-03-16 23:05:30.717027+0800 ThreadDemo[6581:10181270] hello
2020-03-16 23:05:32.715849+0800 ThreadDemo[6581:10181270] hello

2020-03-16 23:05:34.716522+0800 ThreadDemo[6581:10181270] hello

如上代碼所示,并沒有將timer添加到RunLoop,timer照樣可以正常運行。

1.2.4 NSTimer在線程中使用

上面所列舉的例子都是在主線程中運行的,那是因為主線程默認是啟動RunLoop的,但是在線程是沒有默認開啟RunLoop的,所以當(dāng)在子線程中使用NSTimer的時候就需要手動開啟RunLoop了。

- (void)timerInThread {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];

        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
        [[NSRunLoop currentRunLoop]run];
    });
}
- (void)timerTest {
    NSLog(@"hello");
}

1.3、NSTimer中存在的問題

1.3.1、RunLoop的mode問題

如果在一個滾動的視圖(如tableview)使用NSTimer,在視圖滾動的時候,timer會停止計時,那是因為當(dāng)視圖滾動的時候RunLoop的mode是UITrackingRunLoopMode模式。解決方式就是把timer 添加到RunLoop的NSRunLoopCommonModes,那么UITrackingRunLoopModekCFRunLoopDefaultMode都被標(biāo)記為了common模式,就可以在默認模式和追蹤模式都能夠運行。

1.3.2、NSTimer的循環(huán)引用

當(dāng)NSTimer的target被強引用了,而target又強引用的timer,這樣就造成了循環(huán)引用,導(dǎo)致timer無法釋放產(chǎn)生內(nèi)存泄露的問題。這也是在開發(fā)中經(jīng)常遇到的問題。當(dāng)然不是所有的NSTimer都會產(chǎn)生循環(huán)引用。

    1. repeats參數(shù)為NO的情況下,不會產(chǎn)生循環(huán)引用。
    1. ios10后的新的API方法timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block也不會產(chǎn)生循環(huán)引用,但是不要忘記了在合適的地方調(diào)用invalidate方法停止定時器的運行。

要解決NSTimer的循環(huán)引用問題就需要打破NSTimer和target之間的循環(huán)條件,有如下幾種方式。

1.3.2.1、NSProxy的方式

創(chuàng)建一個中間類DSProxy繼承自NSProxy,這個類中對timer的target進行弱引用,再把需要執(zhí)行的方法都轉(zhuǎn)發(fā)給timer的target。

@interface DSProxy : NSProxy

@property (weak, nonatomic) id target;
+ (instancetype)proxyWithTarget:(id)target;
@end

@implementation DSProxy

+ (instancetype)proxyWithTarget:(id)target {
    DSProxy* proxy = [[self class] alloc];
    proxy.target = target;
    return proxy;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
    return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation{
    SEL sel = [invocation selector];
    if ([self.target respondsToSelector:sel]) {
        [invocation invokeWithTarget:self.target];
    }
}

@interface ProxyTimer : NSObject

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval target:(id)target selector:(SEL)selector userInfo:(nullable id)userInfo repeats:(BOOL)repeats;

@end

@implementation ProxyTimer

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval target:(id)target selector:(SEL)selector userInfo:(nullable id)userInfo repeats:(BOOL)repeats{
    NSTimer* timer = [NSTimer scheduledTimerWithTimeInterval:timeInterval target:[DSProxy proxyWithTarget: target] selector:selector userInfo:userInfo repeats:repeats];
    return timer;
}

@end
1.3.2.2、NSTimer封裝

這種方式其實和NSProxy的方式很類似,創(chuàng)建一個類對NSTimer進行封裝,將taget弱引用,

@interface DSTimer : NSObject

@property (nonatomic, weak) id target;
@property (nonatomic) SEL selector;

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval target:(id)target selector:(SEL)selector userInfo:(nullable id)userInfo repeats:(BOOL)repeats;

@end

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval target:(id)target selector:(SEL)selector userInfo:(nullable id)userInfo repeats:(BOOL)repeats {
    DSTimer *dsTimer = [[DSTimer alloc] init];
    dsTimer.target = target;
    dsTimer.selector = selector;
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:timeInterval target:dsTimer selector:@selector(timered:) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
    return timer;
}

- (void)timered:(NSTimer *)timer {
    if ([self.target respondsToSelector:self.selector]) {
        #pragma clang diagnostic push
        #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        [self.target performSelector:self.selector withObject:timer];
        #pragma clang diagnostic pop
    }
}
1.3.2.2、block實現(xiàn)
@interface NSTimer (DSTimer)

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval repeats:(BOOL)repeats blockTimer:(void (^)(NSTimer *))block;

@end

@implementation NSTimer (DSTimer)

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval repeats:(BOOL)repeats blockTimer:(void (^)(NSTimer *))block {
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:timeInterval target:self selector:@selector(timered:) userInfo:[block copy] repeats:repeats];
    [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
    return timer;
}

+ (void)timered:(NSTimer *)timer {
    void (^ block)(NSTimer *timer)  = timer.userInfo;
    block(timer);
}

@end

2、GCD

2.1、GCD簡介

GCD實現(xiàn)定時器功能,是利用GCD中的Dispatch Source中的一種類型DISPATCH_SOURCE_TYPE_TIMER來實現(xiàn)的。dispatch源(Dispatch Source)監(jiān)聽系統(tǒng)內(nèi)核對象并處理,更加的精準(zhǔn)。和NSTimer依賴于RunLoop不一樣,GCD并不依賴于RunLoop,所以即使是在滾動視圖中也不會出現(xiàn)視圖滾動時定時器不起效果的情況。同時GCD定時器提供了定時器的啟動、暫停、回復(fù)、取消等功能,相對而言更加的貼近開發(fā)需求。

2.2、GCD基本使用

GCD定時器調(diào)用 dispatch_source_create方法創(chuàng)建一個source源,然后通過dispatch_source_set_timer方法設(shè)置定時器,dispatch_source_set_event_handler設(shè)置定時器任務(wù),初創(chuàng)建的定時器是暫停的,需要調(diào)用dispatch_resume方法啟動定時器,當(dāng)然也可以調(diào)用dispatch_suspend或者dispatch_source_cancel停止定時器。

下面是對于GCD的簡單封裝。

typedef enum : NSUInteger {
    Status_Running,
    Status_Pause,
    Status_Cancle,
} TimerStatus;

@interface GCDTimer ()

@property (nonatomic, strong) dispatch_source_t gcdTimer;
@property (nonatomic, assign) TimerStatus currentStatus;

@end

@implementation GCDTimer

- (void)scheduledTimerWithTimeInterval:(NSTimeInterval)interval runNow:(BOOL)runNow afterTime:(NSTimeInterval)afterTime repeats:(BOOL)repeats queue:(dispatch_queue_t)queue block:(void (^)(void))block  {
    /** 創(chuàng)建定時器對象
    * para1: DISPATCH_SOURCE_TYPE_TIMER 為定時器類型
    * para2-3: 中間兩個參數(shù)對定時器無用
    * para4: 最后為在什么調(diào)度隊列中使用
    */
    self.gcdTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    /** 設(shè)置定時器
    * para2: 任務(wù)開始時間
    * para3: 任務(wù)的間隔
    * para4: 可接受的誤差時間,設(shè)置0即不允許出現(xiàn)誤差
    * Tips: 單位均為納秒
    */
    dispatch_time_t when;
    if (runNow) {
        when = DISPATCH_TIME_NOW;
    } else {
        when = dispatch_walltime(NULL, (int64_t)(afterTime * NSEC_PER_SEC));
    }
    dispatch_source_set_timer(self.gcdTimer, dispatch_time(when, interval * NSEC_PER_SEC), interval * NSEC_PER_SEC, 0);
    dispatch_source_set_event_handler(self.gcdTimer, ^{
        if (!repeats) {
            dispatch_source_cancel(self.gcdTimer);
        }
        block();
    });
    dispatch_resume(self.gcdTimer);
    self.currentStatus = Status_Running;
}

- (void)pauseTimer {
    if (self.currentStatus == Status_Running && self.gcdTimer) {
        dispatch_suspend(self.gcdTimer);
        self.currentStatus = Status_Pause;
    }
}

- (void)resumeTimer {
    if (self.currentStatus == Status_Pause && self.gcdTimer) {
        dispatch_resume(self.gcdTimer);
        self.currentStatus = Status_Running;
    }
}

- (void)stopTimer {
    if (self.gcdTimer) {
        dispatch_source_cancel(self.gcdTimer);
        self.currentStatus = Status_Cancle;
        self.gcdTimer = nil;
    }
}


@end

2.3、GCD定時器的注意事項

1、dispatch_resumedispatch_suspend調(diào)用要成對出現(xiàn)。dispatch_suspend 嚴(yán)格上只是把timer暫時掛起,dispatch_resumedispatch_suspend分別會減少和增加 dispatch 對象的掛起計數(shù)。當(dāng)這個計數(shù)大于 0 的時候,timer就會執(zhí)行。但是Dispatch Source并沒有提供用于檢測 source 本身的掛起計數(shù)的 API,也就是說外部不能得知一個 source 當(dāng)前是不是掛起狀態(tài),那么在兩者之間需要設(shè)計一個標(biāo)記變量。
2、source在suspend狀態(tài)下,如果直接設(shè)置source = nil或者重新創(chuàng)建source都會造成crash。正確的方式是在resume狀態(tài)下調(diào)用dispatch_source_cancel(source)釋放當(dāng)前的source。
3、dispatch_source_set_event_handler回調(diào)是一個block,在添加到source中后會被source強引用,所以在這里需要注意循環(huán)引用的問題。正確的方法是使用weak+strong或者提前調(diào)用dispatch_source_cancel取消timer。

3、NSTimer和GCD定時器的比較

  1. NSTimer依賴于RunLoop運行,所以在子線程中使用NSTimer需要手動啟動RunLoop。而GCD并不依賴于RunLoop,在子線程中可以正常使用。
  2. NSTimer依賴于RunLoop運行,在某種特定的環(huán)境下可能會需要RunLoop模式切換。
  3. NSTimer會存在延時的可能性,所以在定時層面準(zhǔn)確性會有所偏差。GCD是監(jiān)聽系統(tǒng)內(nèi)核對象并處理,定時更加精確。
  4. NSTimer的容易出現(xiàn)循環(huán)引用,GCD相對而言會好很多。當(dāng)然規(guī)范編程合理設(shè)計這些都不是問題。
最后編輯于
?著作權(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)容