iOS常见三种定时器-NSTimer、CADisplayLink、GCD定时器

链接:https://juejin.cn/post/6863452695144071175
 

在iOS开发过程当中,我们经常会直接或间接地使用到定时器,iOS系统中,带有延迟性操作的函数都是基于NSTimer,CADisplayLink或者GCD定时器来实现的。本文主要也是围绕这三种定时器展开,最后封装一个简单易用的定时器库。

1、NSTimer定时器

  1. NSTimer是基于NSRunloop的实现定时器,在使用NSTimer过程当中,应该关注两个问题

一、直接使用NSTimer定时器,可能存在循环应用问题。首先,NSTimer会强引用传入的target对象, 而此时,如果target又对NSTimer产生强引用,那么就会引发循环引用问题。 二、NSTimer回调的时间间隔可能会有存在误差。因为RunLoop每跑完一次圈再去检查当前累计时间是否已经达到定时器所设置的间隔时间,如果未达到,RunLoop将进入下一轮任务,待任务结束之后再去检查当前累计时间,而此时的累计时间可能已经超过了定时器的间隔时间,故可能会存在误差。

  1. 针对循环引用问题,我们可以使用中间类来解决。原理大致如下:

中间类继承自NSProxy,基于消息转发实现的,目的是为了提高方法调用效率。 实现代码如下:
中间类.h声明文件

有想要领取免费资料的可以进裙或者加好友领取,这里有一个iOS交流圈:[891 488 181] 可以来了解,分享BAT,阿里面试题、面试经验,讨论技术,裙里资料直接下载就行, 大家一起交流学习!

#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

NS_ASSUME_NONNULL_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

为了方便调用NSTimer,我们可以给NSTimer新增一个分类,给分类扩展类方法,在扩展的方法中使用中间类来解决循环应用问题。 同时可以利用runtime关联技术,使用Block代替Selector回调。 代码大致如下:

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

NS_ASSUME_NONNULL_BEGIN

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

@interface NSTimer (XbTimer)
/** 方法一,与系统同名方法一致, 需要手动添加到runloop中,自己控制启动*/
+ (NSTimer *)xb_timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;

/** 方法二, 与系统同名方法一致,系统自动添加到runloop中,创建成功自动启动*/
+ (NSTimer *)xb_scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;

/** 方法三,block回调, 不限制iOS最低版本, 需要手动添加到runloop中,自己控制启动*/
+ (NSTimer *)xb_timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(XBTimerCallbackBlock)block;

/** 方法四,block回调, 不限制iOS最低版本, 系统自动添加到runloop中,创建成功自动启动*/
+ (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
  1. 关于NSTimer时间误差问题,可以使用GCD定时来代替NSTimer定时器,后面讲GCD定时器部分会讲到。

2、CADisplayLink定时器

CADisplayLink 依托于设备屏幕刷新频率触发事件,所以其触发时间比NSTimer较准确,也是最适合做UI不断刷新的事件,过渡相对流畅,无卡顿感。 而CADisplayLink定时器也是依赖于NSRunLoop, 所以,CADisplayLink定时器也一样会存在NSTimer的两个问题。
针对解决循环引用问题,直接上代码了:

//.h文件

#import <QuartzCore/QuartzCore.h>

NS_ASSUME_NONNULL_BEGIN

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

@interface CADisplayLink (XBDisplayLink)
/** 同系统方法,仅解决循环引用问题*/
+ (CADisplayLink *)xb_displayLinkWithTarget:(id)target selector:(SEL)sel;

/** 同系统方法,自动添加到当前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定时器是这三种定时器中,时间最为准确的。因为GCD定时器不依赖与NSRunLoop, GCD定时器实际上是使用了dispatch源(dispatch source),dispatch源监听系统内核对象并处理,通过系统级调用,更加精准。
以下是对GCD定时器的封装,支持block和selector两种回调方式

//.h

#import <Foundation/Foundation.h>

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

@interface XBGCDTimer : NSObject

/// Create GCDTimer, but not fire(定时器创建但未启动)
/// @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 (定时器创建后马上启动)
/// @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(定时器创建但未启动)
/// @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 (定时器创建后马上启动)
/// @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、总结

  • NSTimer和CADisplayLink依赖于RunLoop,如果RunLoop的任务过于繁重,可能会导致NSTimer不准时,相比之下GCD的定时器会更加准时,因为GCD不是依赖RunLoop,而是由内核决定

  • CADisplayLink和NSTimer会对target产生强引用,如果target又对它们产生强引用,那么就会引发循环引用

文章到这里就结束了,你也可以私信我及时获取最新资料。如果你有什么意见和建议欢迎给我留言。

如果你对本文感兴趣,麻烦点个赞~~ 谢谢

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容