iOS常見三種定時器-NSTimer笛辟、CADisplayLink、GCD定時器

0d7ef50b665d0abbcd5d42df751b7cd3.png

在iOS開發(fā)過程當(dāng)中序苏,我們經(jīng)常會直接或間接地使用到定時器手幢,iOS系統(tǒng)中,帶有延遲性操作的函數(shù)都是基于NSTimer忱详,CADisplayLink或者GCD定時器來實現(xiàn)的围来。本文主要也是圍繞這三種定時器展開,最后封裝一個簡單易用的定時器庫匈睁。

1监透、NSTimer定時器

1.NSTimer是基于NSRunloop的實現(xiàn)定時器,在使用NSTimer過程當(dāng)中航唆,應(yīng)該關(guān)注兩個問題

一胀蛮、直接使用NSTimer定時器,可能存在循環(huán)應(yīng)用問題糯钙。首先粪狼,NSTimer會強引用傳入的target對象, 而此時,如果target又對NSTimer產(chǎn)生強引用超营,那么就會引發(fā)循環(huán)引用問題鸳玩。
二、NSTimer回調(diào)的時間間隔可能會有存在誤差演闭。因為RunLoop每跑完一次圈再去檢查當(dāng)前累計時間是否已經(jīng)達到定時器所設(shè)置的間隔時間不跟,如果未達到,RunLoop將進入下一輪任務(wù)米碰,待任務(wù)結(jié)束之后再去檢查當(dāng)前累計時間窝革,而此時的累計時間可能已經(jīng)超過了定時器的間隔時間,故可能會存在誤差吕座。

2.針對循環(huán)引用問題虐译,我們可以使用中間類來解決。原理大致如下:
1c0e2ff7cfff439f4a74f8463a8f1dea.png

中間類繼承自NSProxy吴趴,基于消息轉(zhuǎn)發(fā)實現(xiàn)的漆诽,目的是為了提高方法調(diào)用效率。 實現(xiàn)代碼如下:
中間類.h聲明文件

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface XBWeakProxy : NSProxy
/** weak target*/
@property (nonatomic, weak) id target;

/** init proxy by target*/
+ (instancetype)timerProxyWithTarget:(id)target;
@end

中間類.m聲明文件

#import "XBWeakProxy.h"

@implementation XBWeakProxy
+ (instancetype)timerProxyWithTarget:(id)target{

    if (!target) return nil;

    XBWeakProxy *proxy = [XBWeakProxy alloc];
    proxy.target = target;

    return proxy;
}

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

- (void)forwardInvocation:(NSInvocation *)invocation{
    [invocation invokeWithTarget:self.target];
}
@end

為了方便調(diào)用NSTimer,我們可以給NSTimer新增一個分類厢拭,給分類擴展類方法兰英,在擴展的方法中使用中間類來解決循環(huán)應(yīng)用問題。 同時可以利用runtime關(guān)聯(lián)技術(shù)供鸠,使用Block代替Selector回調(diào)畦贸。 代碼大致如下:

//.h文件
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

typedef void (^XBTimerCallbackBlock)(NSTimer *timer);

@interface NSTimer (XbTimer)
/** 方法一,與系統(tǒng)同名方法一致楞捂, 需要手動添加到runloop中薄坏,自己控制啟動*/
+ (NSTimer *)xb_timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;

/** 方法二, 與系統(tǒng)同名方法一致寨闹,系統(tǒng)自動添加到runloop中胶坠,創(chuàng)建成功自動啟動*/
+ (NSTimer *)xb_scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;

/** 方法三,block回調(diào)鼻忠, 不限制iOS最低版本涵但, 需要手動添加到runloop中,自己控制啟動*/
+ (NSTimer *)xb_timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(XBTimerCallbackBlock)block;

/** 方法四帖蔓,block回調(diào), 不限制iOS最低版本瞳脓, 系統(tǒng)自動添加到runloop中塑娇,創(chuàng)建成功自動啟動*/
+ (NSTimer *)xb_scheduledTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(XBTimerCallbackBlock)block;
@end
NS_ASSUME_NONNULL_END

//.m文件
#import "NSTimer+XBTimer.h"
#import "XBWeakProxy.h"

#import <objc/runtime.h>

@implementation NSTimer (XbTimer)

#pragma mark - Public
+ (NSTimer *)xb_timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo{

    return [self timerWithTimeInterval:ti target:[XBWeakProxy timerProxyWithTarget:aTarget] selector:aSelector userInfo:userInfo repeats:yesOrNo];
}

+ (NSTimer *)xb_scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo{

    return [self scheduledTimerWithTimeInterval:ti target:[XBWeakProxy timerProxyWithTarget:aTarget] selector:aSelector userInfo:userInfo repeats:yesOrNo];
}

+ (NSTimer *)xb_timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(nonnull XBTimerCallbackBlock)block{
    if (!block) return nil;

    NSTimer *timer = [self timerWithTimeInterval:interval   target:[XBWeakProxy timerProxyWithTarget:self] selector:@selector(_blockAction:) userInfo:nil repeats:repeats];

    if (!timer) return timer;

    objc_setAssociatedObject(timer, @selector(_blockAction:), block, OBJC_ASSOCIATION_COPY);

    return timer;
}

+ (NSTimer *)xb_scheduledTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(nonnull XBTimerCallbackBlock)block{
    if (!block) return nil;

    NSTimer *timer = [self scheduledTimerWithTimeInterval:interval   target:[XBWeakProxy timerProxyWithTarget:self] selector:@selector(_blockAction:) userInfo:nil repeats:repeats];

    if (!timer) return timer;

    objc_setAssociatedObject(timer, @selector(_blockAction:), block, OBJC_ASSOCIATION_COPY);

    return timer;
}

#pragma mark - Privite
+ (void)_blockAction:(NSTimer *)timer{
    XBTimerCallbackBlock block = objc_getAssociatedObject(timer, _cmd);

    !block?:block(timer);
}
@end
3.關(guān)于NSTimer時間誤差問題,可以使用GCD定時來代替NSTimer定時器劫侧,后面講GCD定時器部分會講到埋酬。

2、CADisplayLink定時器

CADisplayLink 依托于設(shè)備屏幕刷新頻率觸發(fā)事件烧栋,所以其觸發(fā)時間比NSTimer較準(zhǔn)確写妥,也是最適合做UI不斷刷新的事件,過渡相對流暢审姓,無卡頓感珍特。 而CADisplayLink定時器也是依賴于NSRunLoop, 所以CADisplayLink定時器也一樣會存在NSTimer的兩個問題。
針對解決循環(huán)引用問題魔吐,直接上代碼了:

//.h文件

#import <QuartzCore/QuartzCore.h>

NS_ASSUME_NONNULL_BEGIN

typedef void (^XBDisplayLinkCallbackBlock)(CADisplayLink *link);

@interface CADisplayLink (XBDisplayLink)
/** 同系統(tǒng)方法扎筒,僅解決循環(huán)引用問題*/
+ (CADisplayLink *)xb_displayLinkWithTarget:(id)target selector:(SEL)sel;

/** 同系統(tǒng)方法,自動添加到當(dāng)前runloop中酬姆,Mode: NSRunLoopCommonModes*/
+ (CADisplayLink *)xb_scheduledDisplayLinkWithTarget:(id)target selector:(SEL)sel;

/** Block callback嗜桌,auto run, runloop mode: NSRunLoopCommonModes*/
+ (CADisplayLink *)xb_scheduledDisplayLinkWithBlock:(XBDisplayLinkCallbackBlock)block;
@end

NS_ASSUME_NONNULL_END

//.m文件

#import "CADisplayLink+XBDisplayLink.h"

#import "XBWeakProxy.h"

#import <objc/runtime.h>

@implementation CADisplayLink (XBDisplayLink)
#pragma mark - Public
+ (CADisplayLink *)xb_displayLinkWithTarget:(id)target selector:(SEL)sel{

    return [self displayLinkWithTarget:[XBWeakProxy timerProxyWithTarget:target] selector:sel];
}

+ (CADisplayLink *)xb_scheduledDisplayLinkWithTarget:(id)target selector:(SEL)sel{

    CADisplayLink *link = [self displayLinkWithTarget:[XBWeakProxy timerProxyWithTarget:target] selector:sel];

    [link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];

    return link;

}

+ (CADisplayLink *)xb_scheduledDisplayLinkWithBlock:(XBDisplayLinkCallbackBlock)block{
    if (!block) return nil;
    CADisplayLink *link = [self xb_displayLinkWithTarget:self selector:@selector(displayLinkAction:)];

    objc_setAssociatedObject(link, @selector(displayLinkAction:), block, OBJC_ASSOCIATION_COPY);

    [link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];

    return link;
}

#pragma mark - Privite
+ (void)displayLinkAction:(CADisplayLink *)link{

    XBDisplayLinkCallbackBlock block = objc_getAssociatedObject(link, _cmd);
    !block?:block(link);
}
@end

3、GCD定時器

GCD定時器是這三種定時器中辞色,時間最為準(zhǔn)確的骨宠。因為GCD定時器不依賴與NSRunLoop, GCD定時器實際上是使用了dispatch源(dispatch source),dispatch源監(jiān)聽系統(tǒng)內(nèi)核對象并處理,通過系統(tǒng)級調(diào)用层亿,更加精準(zhǔn)桦卒。
以下是對GCD定時器的封裝,支持block和selector兩種回調(diào)方式

//.h

#import <Foundation/Foundation.h>

@class XBGCDTimer;
typedef void (^XBGCDTimerCallbackBlock)(XBGCDTimer *timer);

@interface XBGCDTimer : NSObject

/// Create GCDTimer, but not fire(定時器創(chuàng)建但未啟動)
/// @param start The number of seconds between timer first times callback since  fire
/// @param interval The number of seconds between firings of the timer
/// @param repeats  If YES, the timer will repeatedly reschedule itself until invalidated
/// @param queue Queue for timer run and callback,  default is in  main queue
/// @param block Timer callback handler
+ (XBGCDTimer *)xb_GCDTimerWithSartTime:(NSTimeInterval)start
                             interval:(NSTimeInterval)interval
                                queue:(dispatch_queue_t)queue
                              repeats:(BOOL)repeats
                                block:(XBGCDTimerCallbackBlock)block;

/// Create GCDTimer and fire immdiately (定時器創(chuàng)建后馬上啟動)
/// @param start The number of seconds between timer first times callback since  fire
/// @param interval The number of seconds between firings of the timer
/// @param repeats  If YES, the timer will repeatedly reschedule itself until invalidated
/// @param queue Queue for timer run and callback,  default is in  main queue
/// @param block Timer callback handler
+ (XBGCDTimer *)xb_scheduledGCDTimerWithSartTime:(NSTimeInterval)start
                                      interval:(NSTimeInterval)interval
                                         queue:(dispatch_queue_t)queue
                                       repeats:(BOOL)repeats
                                         block:(XBGCDTimerCallbackBlock)block;

/// Create GCDTimer, but not fire(定時器創(chuàng)建但未啟動)
/// @param target target description
/// @param selector selector description
/// @param start The number of seconds between firings of the timer
/// @param interval The number of seconds between firings of the timer
/// @param queue Queue for timer run and callback,  default is in  main queue
/// @param repeats If YES, the timer will repeatedly reschedule itself until invalidated
+ (XBGCDTimer *)xb_GCDTimerWithTarget:(id)target
                           selector:(SEL)selector
                           SartTime:(NSTimeInterval)start
                           interval:(NSTimeInterval)interval
                              queue:(dispatch_queue_t)queue
                            repeats:(BOOL)repeats;

/// Create GCDTimer and fire immdiately (定時器創(chuàng)建后馬上啟動)
/// @param target target description
/// @param selector selector description
/// @param start The number of seconds between timer first times callback since  fire
/// @param interval The number of seconds between firings of the timer
/// @param repeats  If YES, the timer will repeatedly reschedule itself until invalidated
/// @param queue Queue for timer run and callback,  default is in  main queue
+ (XBGCDTimer *)xb_scheduledGCDTimerWithTarget:(id)target
                                    selector:(SEL)selector
                                    SartTime:(NSTimeInterval)start
                                    interval:(NSTimeInterval)interval
                                       queue:(dispatch_queue_t)queue
                                     repeats:(BOOL)repeats;

/** start*/
- (void)fire;

/** stop*/
- (void)invalidate;
@end

//.m

#import "XBGCDTimer.h"

#import "XBWeakProxy.h"

#import <objc/runtime.h>

@implementation XBGCDTimer

#pragma mark - Public
+ (XBGCDTimer *)xb_GCDTimerWithSartTime:(NSTimeInterval)start interval:(NSTimeInterval)interval queue:(dispatch_queue_t)queue repeats:(BOOL)repeats block:(XBGCDTimerCallbackBlock)block{

  if (!block || start < 0 || (interval <= 0 && repeats)) return nil;

  XBGCDTimer *gcdTimer = [[XBGCDTimer alloc] init];

  // queue
  dispatch_queue_t queue_t = queue ?: dispatch_get_main_queue();

  // create
  dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue_t);

  // set time
  dispatch_source_set_timer(timer,
                            dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC),
                            interval * NSEC_PER_SEC, 0);

  objc_setAssociatedObject(gcdTimer, @selector(fire), timer, OBJC_ASSOCIATION_RETAIN);

  // callback
  dispatch_source_set_event_handler(timer, ^{
      block(gcdTimer);
      if (!repeats) { // no repeats
          [gcdTimer invalidate];
      }
  });

  return gcdTimer;
}

+ (XBGCDTimer *)xb_scheduledGCDTimerWithSartTime:(NSTimeInterval)start interval:(NSTimeInterval)interval queue:(dispatch_queue_t)queue repeats:(BOOL)repeats block:(XBGCDTimerCallbackBlock)block{

  XBGCDTimer *gcdTimer = [self xb_GCDTimerWithSartTime:start interval:interval queue:queue repeats:repeats block:block];

  [gcdTimer fire];

  return gcdTimer;
}

+ (XBGCDTimer *)xb_GCDTimerWithTarget:(id)target selector:(SEL)selector SartTime:(NSTimeInterval)start interval:(NSTimeInterval)interval queue:(dispatch_queue_t)queue repeats:(BOOL)repeats{

  XBWeakProxy *proxy = [XBWeakProxy timerProxyWithTarget:target];

  return [self xb_GCDTimerWithSartTime:start interval:interval queue:queue repeats:repeats block:^(XBGCDTimer * _Nonnull timer) {
      #pragma clang diagnostic push
      #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
      [proxy performSelector:selector];
      #pragma clang diagnostic pop
  }];
}

+ (XBGCDTimer *)xb_scheduledGCDTimerWithTarget:(id)target selector:(SEL)selector SartTime:(NSTimeInterval)start interval:(NSTimeInterval)interval queue:(dispatch_queue_t)queue repeats:(BOOL)repeats{

  XBWeakProxy *proxy = [XBWeakProxy timerProxyWithTarget:target];

  XBGCDTimer * gcdTimer = [self xb_GCDTimerWithSartTime:start interval:interval queue:queue repeats:repeats block:^(XBGCDTimer * _Nonnull timer) {
      #pragma clang diagnostic push
      #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
      [proxy performSelector:selector];
      #pragma clang diagnostic pop
  }];

  [gcdTimer fire];

  return gcdTimer;
}

/** start*/
- (void)fire{

  dispatch_source_t timer = objc_getAssociatedObject(self, _cmd);

  if (timer) dispatch_resume(timer);
}

/** stop*/
- (void)invalidate{

  dispatch_source_t timer = objc_getAssociatedObject(self, @selector(fire));

  if (timer) dispatch_source_cancel(timer);

  objc_removeAssociatedObjects(self);
}

@end

4棕所、總結(jié)

  • NSTimer和CADisplayLink依賴于RunLoop闸盔,如果RunLoop的任務(wù)過于繁重,可能會導(dǎo)致NSTimer不準(zhǔn)時琳省,相比之下GCD的定時器會更加準(zhǔn)時迎吵,因為GCD不是依賴RunLoop,而是由內(nèi)核決定
  • CADisplayLink和NSTimer會對target產(chǎn)生強引用针贬,如果target又對它們產(chǎn)生強引用击费,那么就會引發(fā)循環(huán)引用

如果你有什么意見和建議歡迎給我留言。
如果你對本文感興趣桦他,麻煩點個贊~~ 謝謝

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蔫巩,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子快压,更是在濱河造成了極大的恐慌圆仔,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,657評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蔫劣,死亡現(xiàn)場離奇詭異坪郭,居然都是意外死亡,警方通過查閱死者的電腦和手機脉幢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評論 3 394
  • 文/潘曉璐 我一進店門歪沃,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人嫌松,你說我怎么就攤上這事沪曙。” “怎么了萎羔?”我有些...
    開封第一講書人閱讀 164,057評論 0 354
  • 文/不壞的土叔 我叫張陵液走,是天一觀的道長。 經(jīng)常有香客問我外驱,道長育灸,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,509評論 1 293
  • 正文 為了忘掉前任昵宇,我火速辦了婚禮磅崭,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘瓦哎。我一直安慰自己砸喻,他們只是感情好柔逼,可當(dāng)我...
    茶點故事閱讀 67,562評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著割岛,像睡著了一般愉适。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上癣漆,一...
    開封第一講書人閱讀 51,443評論 1 302
  • 那天维咸,我揣著相機與錄音,去河邊找鬼惠爽。 笑死癌蓖,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的婚肆。 我是一名探鬼主播租副,決...
    沈念sama閱讀 40,251評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼较性!你這毒婦竟也來了用僧?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,129評論 0 276
  • 序言:老撾萬榮一對情侶失蹤赞咙,失蹤者是張志新(化名)和其女友劉穎责循,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體攀操,經(jīng)...
    沈念sama閱讀 45,561評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡沼死,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,779評論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了崔赌。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,902評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡耸别,死狀恐怖健芭,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情秀姐,我是刑警寧澤慈迈,帶...
    沈念sama閱讀 35,621評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站省有,受9級特大地震影響痒留,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蠢沿,卻給世界環(huán)境...
    茶點故事閱讀 41,220評論 3 328
  • 文/蒙蒙 一伸头、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧舷蟀,春花似錦恤磷、人聲如沸面哼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽魔策。三九已至,卻和暖如春河胎,著一層夾襖步出監(jiān)牢的瞬間闯袒,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評論 1 269
  • 我被黑心中介騙來泰國打工游岳, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留政敢,地道東北人。 一個月前我還...
    沈念sama閱讀 48,025評論 2 370
  • 正文 我出身青樓吭历,卻偏偏與公主長得像堕仔,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子晌区,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,843評論 2 354

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