iOS定時器-- NSTimer&GCD定時器

在iOS開發中定時器是咱們常常遇到的需求,經常使用到的定時器表示方式有NSTimer、GCD,那麼它們之間有什麼樣的區別呢?本文將從二者的基本使用開始剖析它們之間的區別。ios

一、NSTimer

1.一、NSTimer簡介

NSTimer是iOS中最基本的定時器。NSTimer是經過RunLoop來實現的,在通常的狀況下NSTimer做爲定時器是比較準確的,可是若是當前的耗時操做較多時,可能出現延時問題。同時,由於受到RunLoop的支配,NSTimer會受到RunLoopMode的影響。在建立NSTimer的時候默認是被加到defaultMode的,可是若是在一個滑動的視圖中如tableview,當RunLoop的mode發生變化時,當前的NSTimer就不會工做了,這就是咱們在開發中遇到的NSTimer用在tableview中,當tableview滾動的時候NSTimer中止工做的緣由,因此咱們在建立NSTimer的時候將其加到RunLoop指定mode爲NSRunLoopCommonModes編程

1.二、NSTimer基本使用

NSTimer的初始化方式有兩種,分別是invocationselector兩種調用方式,這兩種方式區別不大,可是selector的方式更加簡便。bash

+ (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;
複製代碼

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

1.2.一、selector方式

使用selector方式初始化NSTimer比較簡單,只須要指定執行的方法和是否循環就能夠了。async

- (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");
}
複製代碼

在上一個小節講過,NSTimer依賴於RunLoop,須要把初始化好的timer添加到RunLoop中,對於RunLoop的幾種模式在上面的代碼註釋中有說明。 這段代碼的運行結果就是每隔兩秒鐘就會打印一次「hello」oop

打印結果: 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] helloatom

1.2.一、invocation方式

經過invocation方式初始化timer相對於來講會稍微複雜一些,最主要的是invocation參數。一樣的也須要手動將timer加入到RunLoop中。spa

- (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");
}
複製代碼

這段代碼的運行結果就是每隔兩秒鐘就會打印一次「hello」線程

打印結果: 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.三、scheduledTimerWithTimeInterval方法

在上面列舉的API中其實有scheduledTimerWithTimeInterval方法能夠建立timer,這個方法和timerWithTimeInterval的區別就在於前者會默認的將timer添加到了RunLoop,而且currentRunLoop是NSDefaultRunLoopMode,然後者是須要開發者手動的將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");
}
複製代碼

這段代碼的運行結果就是每隔兩秒鐘就會打印一次「hello」

打印結果: 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的,因此當在子線程中使用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.三、NSTimer中存在的問題

1.3.一、RunLoop的mode問題

若是在一個滾動的視圖(如tableview)使用NSTimer,在視圖滾動的時候,timer會中止計時,那是由於當視圖滾動的時候RunLoop的mode是UITrackingRunLoopMode模式。解決方式就是把timer 添加到RunLoop的NSRunLoopCommonModes,那麼UITrackingRunLoopModekCFRunLoopDefaultMode都被標記爲了common模式,就能夠在默認模式和追蹤模式都可以運行。

1.3.二、NSTimer的循環引用

當NSTimer的target被強引用了,而target又強引用的timer,這樣就形成了循環引用,致使timer沒法釋放產生內存泄露的問題。這也是在開發中常常遇到的問題。固然不是全部的NSTimer都會產生循環引用。

    1. repeats參數爲NO的狀況下,不會產生循環引用。
    1. ios10後的新的API方法timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block也不會產生循環引用,可是不要忘記了在合適的地方調用invalidate方法中止定時器的運行。

要解決NSTimer的循環引用問題就須要打破NSTimer和target之間的循環條件,有以下幾種方式。

1.3.2.一、NSProxy的方式

建立一箇中間類DSProxy繼承自NSProxy,這個類中對timer的target進行弱引用,再把須要執行的方法都轉發給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.二、NSTimer封裝

這種方式其實和NSProxy的方式很相似,建立一個類對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.二、block實現
@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
複製代碼

二、GCD

2.一、GCD簡介

GCD實現定時器功能,是利用GCD中的Dispatch Source中的一種類型DISPATCH_SOURCE_TYPE_TIMER來實現的。dispatch源(Dispatch Source)監聽系統內核對象並處理,更加的精準。和NSTimer依賴於RunLoop不同,GCD並不依賴於RunLoop,因此即便是在滾動視圖中也不會出現視圖滾動時定時器不起效果的狀況。同時GCD定時器提供了定時器的啓動、暫停、回覆、取消等功能,相對而言更加的貼近開發需求。

2.二、GCD基本使用

GCD定時器調用 dispatch_source_create方法建立一個source源,而後經過dispatch_source_set_timer方法設置定時器,dispatch_source_set_event_handler設置定時器任務,初建立的定時器是暫停的,須要調用dispatch_resume方法啓動定時器,固然也能夠調用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  {
    /** 建立定時器對象
    * para1: DISPATCH_SOURCE_TYPE_TIMER 爲定時器類型
    * para2-3: 中間兩個參數對定時器無用
    * para4: 最後爲在什麼調度隊列中使用
    */
    self.gcdTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    /** 設置定時器
    * para2: 任務開始時間
    * para3: 任務的間隔
    * para4: 可接受的偏差時間,設置0即不容許出現偏差
    * 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.三、GCD定時器的注意事項

一、dispatch_resumedispatch_suspend調用要成對出現。dispatch_suspend 嚴格上只是把timer暫時掛起,dispatch_resumedispatch_suspend分別會減小和增長 dispatch 對象的掛起計數。當這個計數大於 0 的時候,timer就會執行。可是Dispatch Source並無提供用於檢測 source 自己的掛起計數的 API,也就是說外部不能得知一個 source 當前是否是掛起狀態,那麼在二者之間須要設計一個標記變量。 二、source在suspend狀態下,若是直接設置source = nil或者從新建立source都會形成crash。正確的方式是在resume狀態下調用dispatch_source_cancel(source)釋放當前的source。 三、dispatch_source_set_event_handler回調是一個block,在添加到source中後會被source強引用,因此在這裏須要注意循環引用的問題。正確的方法是使用weak+strong或者提早調用dispatch_source_cancel取消timer。

三、NSTimer和GCD定時器的比較

  1. NSTimer依賴於RunLoop運行,因此在子線程中使用NSTimer須要手動啓動RunLoop。而GCD並不依賴於RunLoop,在子線程中能夠正常使用。
  2. NSTimer依賴於RunLoop運行,在某種特定的環境下可能會須要RunLoop模式切換。
  3. NSTimer會存在延時的可能性,因此在定時層面準確性會有所誤差。GCD是監聽系統內核對象並處理,定時更加精確。
  4. NSTimer的容易出現循環引用,GCD相對而言會好不少。固然規範編程合理設計這些都不是問題。
相關文章
相關標籤/搜索