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

3,466 阅读6分钟

    

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

1、NSTimer定时器

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

  2. 针对循环引用问题,我们可以使用中间类来解决。原理大致如下: 中间类继承自NSProxy,基于消息转发实现的,目的是为了提高方法调用效率。 实现代码如下:
    中间类.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

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又对它们产生强引用,那么就会引发循环引用

上面所有代码已经封装成一个定时器库XBSTimer,欢迎下载体验。 GitHub地址

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