iOS定時器-NSTimer&GCD定時器

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

1余黎、NSTimer

1.1重窟、NSTimer簡介

NSTimer是iOS中最基本的定時器。NSTimer是通過RunLoop來實現(xiàn)的惧财,在一般的情況下NSTimer作為定時器是比較準確的亲族,但是如果當前的耗時操作較多時,可能出現(xiàn)延時問題可缚。同時霎迫,因為受到RunLoop的支配,NSTimer會受到RunLoopMode的影響帘靡。在創(chuàng)建NSTimer的時候默認是被加到defaultMode的知给,但是如果在一個滑動的視圖中如tableview,當RunLoop的mode發(fā)生變化時,當前的NSTimer就不會工作了涩赢,這就是我們在開發(fā)中遇到的NSTimer用在tableview中戈次,當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的,所以當在子線程中使用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會停止計時艇挨,那是因為當視圖滾動的時候RunLoop的mode是UITrackingRunLoopMode模式。解決方式就是把timer 添加到RunLoop的NSRunLoopCommonModes韭赘,那么UITrackingRunLoopModekCFRunLoopDefaultMode都被標記為了common模式缩滨,就可以在默認模式和追蹤模式都能夠運行。

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

當NSTimer的target被強引用了脉漏,而target又強引用的timer,這樣就造成了循環(huán)引用瓦灶,導(dǎo)致timer無法釋放產(chǎn)生內(nèi)存泄露的問題鸠删。這也是在開發(fā)中經(jī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)核對象并處理絮宁,更加的精準梆暮。和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方法啟動定時器,當然也可以調(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 嚴格上只是把timer暫時掛起饱狂,dispatch_resumedispatch_suspend分別會減少和增加 dispatch 對象的掛起計數(shù)曹步。當這個計數(shù)大于 0 的時候,timer就會執(zhí)行休讳。但是Dispatch Source并沒有提供用于檢測 source 本身的掛起計數(shù)的 API讲婚,也就是說外部不能得知一個 source 當前是不是掛起狀態(tài),那么在兩者之間需要設(shè)計一個標記變量俊柔。
2筹麸、source在suspend狀態(tài)下,如果直接設(shè)置source = nil或者重新創(chuàng)建source都會造成crash雏婶。正確的方式是在resume狀態(tài)下調(diào)用dispatch_source_cancel(source)釋放當前的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會存在延時的可能性缸血,所以在定時層面準確性會有所偏差。GCD是監(jiān)聽系統(tǒng)內(nèi)核對象并處理械筛,定時更加精確捎泻。
  4. NSTimer的容易出現(xiàn)循環(huán)引用,GCD相對而言會好很多埋哟。當然規(guī)范編程合理設(shè)計這些都不是問題笆豁。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市赤赊,隨后出現(xiàn)的幾起案子闯狱,更是在濱河造成了極大的恐慌,老刑警劉巖抛计,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件哄孤,死亡現(xiàn)場離奇詭異,居然都是意外死亡吹截,警方通過查閱死者的電腦和手機瘦陈,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來饭弓,“玉大人双饥,你說我怎么就攤上這事媒抠〉芏希” “怎么了?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵趴生,是天一觀的道長阀趴。 經(jīng)常有香客問我,道長苍匆,這世上最難降的妖魔是什么刘急? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮浸踩,結(jié)果婚禮上叔汁,老公的妹妹穿的比我還像新娘。我一直安慰自己检碗,他們只是感情好据块,可當我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著折剃,像睡著了一般另假。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上怕犁,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天边篮,我揣著相機與錄音己莺,去河邊找鬼。 笑死戈轿,一個胖子當著我的面吹牛凌受,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播思杯,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼胁艰,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了智蝠?” 一聲冷哼從身側(cè)響起腾么,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎杈湾,沒想到半個月后解虱,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡漆撞,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年殴泰,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片浮驳。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡悍汛,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出至会,到底是詐尸還是另有隱情离咐,我是刑警寧澤,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布奉件,位于F島的核電站宵蛀,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏县貌。R本人自食惡果不足惜术陶,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望煤痕。 院中可真熱鬧梧宫,春花似錦、人聲如沸摆碉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽兆解。三九已至馆铁,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間锅睛,已是汗流浹背埠巨。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工历谍, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人辣垒。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓望侈,卻偏偏與公主長得像,于是被迫代替她去往敵國和親勋桶。 傳聞我的和親對象是個殘疾皇子脱衙,可洞房花燭夜當晚...
    茶點故事閱讀 44,700評論 2 354

推薦閱讀更多精彩內(nèi)容