定时器

重拾iOS.jpg

关键词:NSTimer、CADisplayLink、GCD、RunLoop

前言

  1. 开发中常用的定时器有哪些,优缺点是什么?
  2. 定时器的循环引用问题怎么解决?
  3. CADisplayLink、NSTimer是否准时?

一、NSTimer和CADisplayLink

1、NSTimer

常用api有:

+ (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;


/// Creates and returns a new NSTimer object initialized with the specified block object. This timer needs to be scheduled on a run loop (via -[NSRunLoop addTimer:]) before it will fire.
/// - parameter:  timeInterval  The number of seconds between firings of the timer. If seconds is less than or equal to 0.0, this method chooses the nonnegative value of 0.1 milliseconds instead
/// - parameter:  repeats  If YES, the timer will repeatedly reschedule itself until invalidated. If NO, the timer will be invalidated after it fires.
/// - parameter:  block  The execution body of the timer; the timer itself is passed as the parameter to this block when executed to aid in avoiding cyclical references
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

/// Creates and returns a new NSTimer object initialized with the specified block object and schedules it on the current run loop in the default mode.
/// - parameter:  ti    The number of seconds between firings of the timer. If seconds is less than or equal to 0.0, this method chooses the nonnegative value of 0.1 milliseconds instead
/// - parameter:  repeats  If YES, the timer will repeatedly reschedule itself until invalidated. If NO, the timer will be invalidated after it fires.
/// - parameter:  block  The execution body of the timer; the timer itself is passed as the parameter to this block when executed to aid in avoiding cyclical references
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

/// Initializes a new NSTimer object using the block as the main body of execution for the timer. This timer needs to be scheduled on a run loop (via -[NSRunLoop addTimer:]) before it will fire.
/// - parameter:  fireDate   The time at which the timer should first fire.
/// - parameter:  interval  The number of seconds between firings of the timer. If seconds is less than or equal to 0.0, this method chooses the nonnegative value of 0.1 milliseconds instead
/// - parameter:  repeats  If YES, the timer will repeatedly reschedule itself until invalidated. If NO, the timer will be invalidated after it fires.
/// - parameter:  block  The execution body of the timer; the timer itself is passed as the parameter to this block when executed to aid in avoiding cyclical references
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(nullable id)ui repeats:(BOOL)rep NS_DESIGNATED_INITIALIZER;

- (void)fire;

在NSTimer的初始化方法中,以scheduled开头的方法,timer默认已经添加到了当前RunLoop中(以default mode形式添加)

2、CADisplayLink

常用api有:

/* Create a new display link object for the main display. It will
 * invoke the method called 'sel' on 'target', the method has the
 * signature '(void)selector:(CADisplayLink *)sender'. */

+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;

/* Adds the receiver to the given run-loop and mode. Unless paused, it
 * will fire every vsync until removed. Each object may only be added
 * to a single run-loop, but it may be added in multiple modes at once.
 * While added to a run-loop it will implicitly be retained. */

- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode;

/* Removes the receiver from the given mode of the runloop. This will
 * implicitly release it when removed from the last mode it has been
 * registered for. */

- (void)removeFromRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode;

/* Removes the object from all runloop modes (releasing the receiver if
 * it has been implicitly retained) and releases the 'target' object. */

- (void)invalidate;

二、定时器循环引用问题的解决方案

1、使用block方式初始化NSTimer;

2、使用中间层WeakContainer;

代码示例:

新建SFWeakContainer类

// .h
@interface SFWeakContainer : NSObject

- (instancetype)initWithTarget:(NSObject *)target;
+ (instancetype)containerWithTarget:(NSObject *)target;

@end

// .m
@interface SFWeakContainer ()
@property (nonatomic, weak) NSObject *target;
@end

@implementation SFWeakContainer

- (instancetype)initWithTarget:(NSObject *)target {
    if (self = [super init]) {
        self.target = target;
    }
    return self;
}
+ (instancetype)containerWithTarget:(NSObject *)target {
    SFWeakContainer *container = [[SFWeakContainer alloc]initWithTarget:target];
    return container;
}

// 备用接受者
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (self.target && [self.target respondsToSelector:aSelector]) {
        return self.target;
    }else{
        return [super forwardingTargetForSelector:aSelector];
    }
}

@end

测试:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    SFWeakContainer *weakContainer = [SFWeakContainer containerWithTarget:self];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:weakContainer selector:@selector(timerEvent:) userInfo:nil repeats:YES];
}

- (void)timerEvent:(NSTimer *)timer {
    NSLog(@"定时器事件");
}

- (void)dealloc {
    [self.timer invalidate];
    self.timer = nil;
    NSLog(@"%s", __func__);
}

3、NSProxy消息转发;

代码示例:

新建SFProxy

// .h
@interface SFProxy : NSProxy
- (instancetype)initWithTarget:(id)target;
+ (instancetype)proxyWithTarget:(id)target;
@end

// .m
@interface SFProxy ()
@property (nonatomic, weak) NSObject *target;
@end

@implementation SFProxy
- (instancetype)initWithTarget:(id)target {
    _target = target;
    return self;
}
+ (instancetype)proxyWithTarget:(id)target {
    return [[self alloc] initWithTarget:target];
}

// 消息转发
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if (self.target && [self.target respondsToSelector:aSelector]) {
        return [self.target methodSignatureForSelector:aSelector];
    }
    return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    SEL aSelector = [anInvocation selector];
    if (self.target && [self.target respondsToSelector:aSelector]) {
        [anInvocation invokeWithTarget:self.target];
    } else {
        [super forwardInvocation:anInvocation];
    }
}
@end

测试:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    SFProxy *proxy = [SFProxy proxyWithTarget:self];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:proxy selector:@selector(timerEvent:) userInfo:nil repeats:YES];
}

- (void)timerEvent:(NSTimer *)timer {
    NSLog(@"定时器事件");
}

- (void)dealloc {
    [self.timer invalidate];
    self.timer = nil;
    NSLog(@"%s", __func__);
}

具体怎么做,根据个人喜好选择,我这里有一个写好的方案:Crash防护(4)-NSTimer

三、CADisplayLink、NSTimer是否准时?

CADisplayLink、NSTimer底层都是靠RunLoop来实现的,也就是可以把它们理解成RunLoop所需要处理的事件。我们知道RunLoop可以拿来刷新UI,处理定时器(CADisplayLink、NSTimer),处理点击滑动事件等非常多的事情。这里,就需要来了解一下RunLoop是如何触发NSTimer任务的。RunLoop每循环一圈,都会处理一定的事件,会消耗一定的时间,但是具体耗时多少这个是无法确定的。

假如你开启一个timer,隔1秒触发定时器事件,RunLoop会开始累计每一圈循环的用时,当时间累计够1秒,就会触发定时器事件。你有兴趣的话,是可以在RunLoop的源码里面找到时间累加相关代码的。可以借助下图来加深理解:

image

如果RunLoop在某一圈任务过于繁重,就可能出现如下情况

image

所以CADisplayLink、NSTimer是无法保证准时性的。

四、GCD Timer

1、GCD Timer 的简单使用

@interface ViewController ()
@property (nonatomic, strong) dispatch_source_t timer;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // 初始化定时器
    self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
    // 间隔时间
    uint64_t intervalTime = 1.0;
    //误差时间
    uint64_t leewayTime = 0;
    // 延迟时间
    uint64_t delayTime = 0;
    // 开始时间
    dispatch_time_t startTime = dispatch_time(DISPATCH_TIME_NOW, delayTime*NSEC_PER_SEC);
    // 设置定时器时间
    dispatch_source_set_timer(self.timer, startTime, intervalTime * NSEC_PER_SEC, leewayTime * NSEC_PER_SEC);
    // 设置定时器回调事件
    dispatch_source_set_event_handler(self.timer, ^{
        // 定时器事件代码
        NSLog(@"GCD定时器事件");
        // 如果定时器不需要重复,可以在这里取消定时器
        dispatch_source_cancel(self.timer);
    });
    // 运行定时器
    dispatch_resume(self.timer);
    
}

2、GCD Timer 的封装

代码如下:

#import "SFGcdTimer.h"

@interface SFGcdTimer ()
@property (nonatomic, strong) dispatch_source_t timer;
@property (nonatomic, strong) dispatch_queue_t queue;
/**
 * 提问:苹果为什么要把NSTimer中的target设计成强引用关系,既然他会导致循环引用问题,为什么苹果不直接将NSTimer的target设计成弱引用关系?
 * 所以这里保留跟NSTimer类似的设计
 */
@property (nonatomic, strong) NSObject *target;
@property (nullable, retain) id userInfo;
@property (nonatomic, assign) NSTimeInterval timeInterval;
@end

@implementation SFGcdTimer

// MARK: target方式
/// 初始化方法(target)
/// @param interval 时间间隔
/// @param delay 延迟时间
/// @param aTarget 执行对象
/// @param aSelector 执行方法
/// @param userInfo 附带信息
/// @param repeats 是否重复
+ (SFGcdTimer *)timerWithTimeInterval:(NSTimeInterval)interval delay:(NSTimeInterval)delay target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)repeats  {
    SFGcdTimer *timer = [[SFGcdTimer alloc] initWithTimeInterval:interval delay:delay target:aTarget selector:aSelector userInfo:userInfo repeats:repeats queue:nil];
    return timer;
}

/// 初始化方法(target)
/// @param interval 时间间隔
/// @param delay 延迟时间
/// @param aTarget 执行对象
/// @param aSelector 执行方法
/// @param userInfo 附带信息
/// @param repeats 是否重复
/// @param queue 指定队列(默认主队列)
+ (SFGcdTimer *)timerWithTimeInterval:(NSTimeInterval)interval delay:(NSTimeInterval)delay target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)repeats queue:(dispatch_queue_t)queue {
    SFGcdTimer *timer = [[SFGcdTimer alloc] initWithTimeInterval:interval delay:delay target:aTarget selector:aSelector userInfo:userInfo repeats:repeats queue:queue];
    return timer;
}

/// 初始化方法(target)
/// @param interval 时间间隔
/// @param delay 延迟时间
/// @param aTarget 执行对象
/// @param aSelector 执行方法
/// @param userInfo 附带信息
/// @param repeats 是否重复
/// @param queue 指定队列(默认主队列)
- (instancetype)initWithTimeInterval:(NSTimeInterval)interval delay:(NSTimeInterval)delay target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)repeats queue:(dispatch_queue_t)queue {
    if (self = [super init]) {
        self.timeInterval = interval;
        self.queue = queue;
        self.target = aTarget;
        self.userInfo = userInfo;
        self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.queue);
        dispatch_source_set_timer(self.timer,
                                  dispatch_time(DISPATCH_TIME_NOW, delay * NSEC_PER_SEC), // 开始时间
                                  interval * NSEC_PER_SEC, // 间隔
                                  0 // 误差
                                  );
        dispatch_source_set_event_handler(self.timer, ^{
            if ([self.target respondsToSelector:aSelector]) {
                [self.target performSelector:aSelector withObject:self];
            }
            if (!repeats) {
                [self invalidate];
            }
        });
    }
    return self;
}


// MARK: block方式
/// 初始化方法(block)
/// @param interval 时间间隔
/// @param delay 延迟时间
/// @param repeats 是否重复
/// @param block 执行block
+ (SFGcdTimer *)timerWithTimeInterval:(NSTimeInterval)interval delay:(NSTimeInterval)delay repeats:(BOOL)repeats block:(void (^)(SFGcdTimer *timer))block {
    SFGcdTimer *timer = [[SFGcdTimer alloc]initWithTimeInterval:interval delay:delay repeats:repeats block:block queue:nil];
    return timer;
}


/// 初始化方法(block)
/// @param interval 时间间隔
/// @param delay 延迟时间
/// @param repeats 是否重复
/// @param block 执行block
/// @param queue 执行队列(默认主队列)
+ (SFGcdTimer *)timerWithTimeInterval:(NSTimeInterval)interval delay:(NSTimeInterval)delay repeats:(BOOL)repeats block:(void (^)(SFGcdTimer *timer))block queue:(dispatch_queue_t)queue {
    SFGcdTimer *timer = [[SFGcdTimer alloc]initWithTimeInterval:interval delay:delay repeats:repeats block:block queue:queue];
    return timer;
}


/// 初始化方法(block)
/// @param interval 时间间隔
/// @param delay 延迟时间
/// @param repeats 是否重复
/// @param block 执行block
/// @param queue 执行队列(默认主队列)
- (instancetype)initWithTimeInterval:(NSTimeInterval)interval delay:(NSTimeInterval)delay repeats:(BOOL)repeats block:(void (^)(SFGcdTimer *timer))block queue:(dispatch_queue_t)queue {
    if (self = [super init]) {
        self.timeInterval = interval;
        self.queue = queue;
        self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.queue);
        dispatch_source_set_timer(self.timer,
                                  dispatch_time(DISPATCH_TIME_NOW, delay * NSEC_PER_SEC), // 开始时间
                                  interval * NSEC_PER_SEC, // 间隔
                                  0 // 误差
                                  );
        dispatch_source_set_event_handler(self.timer, ^{
            if (block) {
                block(self);
            }
            if (!repeats) {
                [self invalidate];
            }
        });
    }
    return self;
}

/// 开启
- (void)fire {
    dispatch_resume(self.timer);
}

/// 暂停
- (void)pause {
    dispatch_suspend(self.timer);
}

/// 销毁
- (void)invalidate {
    dispatch_source_cancel(self.timer);
}


#pragma mark - lazy load
// 默认主队列
- (dispatch_queue_t)queue {
    if (!_queue) {
        _queue = dispatch_get_main_queue();
    }
    return _queue;
}

@end

[代码链接]

GitHub:https://github.com/jack110530/SFCrash


[相关参考]

  1. 比较一下iOS中的三种定时器
  2. 内存管理——定时器问题

[相关思考]

  1. NSTimer和线程的关系
  2. 苹果为什么要把NSTimer中的target设计成强引用关系,既然他会导致循环引用问题,为什么苹果不直接将NSTimer的target设计成弱引用关系?

你可能感兴趣的:(定时器)