关键词:NSTimer、CADisplayLink、GCD、RunLoop
前言
- 开发中常用的定时器有哪些,优缺点是什么?
- 定时器的循环引用问题怎么解决?
- 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的源码里面找到时间累加相关代码的。可以借助下图来加深理解:
如果RunLoop在某一圈任务过于繁重,就可能出现如下情况
所以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
[相关参考]
- 比较一下iOS中的三种定时器
- 内存管理——定时器问题
[相关思考]
- NSTimer和线程的关系
- 苹果为什么要把NSTimer中的target设计成强引用关系,既然他会导致循环引用问题,为什么苹果不直接将NSTimer的target设计成弱引用关系?