从属关系:NSTimer:NSObject :Foundation
NSTimer看名字就知道是计时器,定时器要和’运行循环’(run loop)相关联,运行循环到时候会触发任务。把定时器放在运行循环里,才能正常触发任务。
NSTimer计时器
NSTimer的初始化方式有几下几种。我们注意到分为invocation和selector两种调用方式,其实这两种区别不大,一般我们用selector方式较为方便。
初始化
@interface NSTimer : NSObject
// 创建一个定时器,但是没有添加到运行循环,我们需要在创建定时器后手动的调用 NSRunLoop 对象的 addTimer:forMode: 方法
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
// 创建一个定时器,并将定时器的添加到当前的runloop中
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
// 创建一个定时器,但是没有添加到运行循环,我们需要在创建定时器后手动的调用 NSRunLoop 对象的 addTimer:forMode: 方法
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
// 创建一个定时器,并将定时器的添加到当前的runloop中
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
// iOS 10 新添加的方法,timer的创建方式,可以解决与target的循环引用问题,我们需要在创建定时器后手动的调用 NSRunLoop 对象的 addTimer:forMode: 方法
+ (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));
// iOS 10 新添加的方法,timer的创建方式,可以解决与target的循环引用问题,自动添加到当前的runloop中
+ (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));
// iOS 10 新添加的方法,timer的创建方式,可以设置第一次的启动时间
- (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));
// iOS 10 新添加的方法,timer的创建方式,可以设置第一次的启动时间
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(nullable id)ui repeats:(BOOL)rep NS_DESIGNATED_INITIALIZER;
// 启动 Timer 触发Target的方法调用但是并不会改变Timer的时间设置。 即 time没有到达到,Timer会立即启动调用方法且没有改变时间设置,当时间 time 到了的时候,Timer还是会调用方法。
- (void)fire;
/*
这是设置定时器的启动时间,常用来管理定时器的启动与停止
启动定时器
timer.fireDate = [NSDate distantPast];
停止定时器
timer.fireDate = [NSDate distantFuture];
开启
[time setFireDate:[NSDate distanPast]]
NSTimer 关闭
[time setFireDate:[NSDate distantFunture]]
继续。
*/
@property (copy) NSDate *fireDate;
// 这个是一个只读属性,获取定时器调用间隔时间
@property (readonly) NSTimeInterval timeInterval;
// 这是7.0之后新增的一个属性,因为NSTimer并不完全精准,通过这个值设置误差范围
@property NSTimeInterval tolerance API_AVAILABLE(macos(10.9), ios(7.0), watchos(2.0), tvos(9.0));
// 唯一的方法将定时器从循环池中移除,关于这个方法API给的解释是:这个方法调用后runloop会移除对timer的强引用,同事也会移除对target,userinfo的强引用
- (void)invalidate;
// 定时器是否有效
@property (readonly, getter=isValid) BOOL valid;
// 定时器的附件数据信息
@property (nullable, readonly, retain) id userInfo;
@end
已+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
为例
参数
Interval
:每个多长时间调用定时器函数,以秒为单位
target
:表示实现这个定时器函数的对象
selector
:定时器函数对象
userInfo
:可以传入定时器函数中一个参数,无参数传入nil
repeats
:定时器是否重复操作YES表示重复,NO表示只完成一次函数调用
启动–停止–关闭
这是设置定时器的启动时间,常用来管理定时器的启动与停止
关闭定时器
[timer setFireDate:[NSDate distantFuture]];
开启定时器(只有上面关闭的定时器才能再开始)
[timer setFireDate:[NSDate distantPast]];
取消定时器(永久的停止,再想用只能重新初始化)
NSTimer 关闭后,一定要将timer赋空,否则还是没有释放
[timer invalidate];
timer = nil;
注:将计数器的repeats设置为YES的时候,self的引用计数会加1。因此可能会导致self(即viewController)不能release,所以,必须在viewDidDisappear的时候,将计数器timer取消且至空,否则可能会导致内存泄露。
示例
#import "ViewController.h"
@interface ViewController ()
@property(nonatomic,strong)NSTimer *timer1;
@property(nonatomic,assign)NSInteger index1;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//NSTime的类方法创建一个定时器并且启动这个定时器
//p1:每个多长时间调用定时器函数,以秒为单位
//p2:表示实现这个定时器函数的对象
//p3:定时器函数对象
//p4:可以传入定时器函数中一个参数,无参数传入nil
//p5:定时器是否重复操作YES表示重复,NO表示只完成一次函数调用
_timer1 = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerClick:) userInfo:@"我是一个定时器" repeats:YES];
_index1 = 1;
}
//定时器调用方法
-(void)timerClick:(NSTimer*)timer{
NSLog(@"定时器参数为 = %@",timer.userInfo);
_index1++;
if (_index1 == 10) {
//关闭定时器
//[_timer1 setFireDate:[NSDate distantFuture]];
//开启定时器(只有上面关闭的定时器才能再开始)
//[_timer1 setFireDate:[NSDate distantPast]];
//取消定时器(永久的停止,再想用只能重新初始化)
[self stopTimer];
}
}
// 停止定时器
-(void)stopTimer{
//停止后,一定要将timer赋空,否则还是没有释放
[_timer1 invalidate];
_timer1 = nil;
}
通常情况下,初始化完成后,在页面消失的时候关闭定时器,然后等页面再次打开的时候,又开启定时器。(主要是为了防止它在后台运行,暂用CPU)
//页面将要进入前台,开启定时器
-(void)viewWillAppear:(BOOL)animated
{
//开启定时器
[_timer1 setFireDate:[NSDate distantPast]];
}
//页面消失,进入后台不显示该页面,关闭定时器
-(void)viewDidDisappear:(BOOL)animated
{
//关闭定时器
[_timer1 setFireDate:[NSDate distantFuture]];
}
@end
注:NSTimer的循环引用问题
我们创建NSTimer对象之后必须要将其加入到runloop中,API设计的时候就设定runloop会强引用timer以及timer的target以及userInfo资源对象。API解释是我们应当在合适的地方使用 invalidate()的方法。这样就会使runloop移除对timer,target,userInfo的强引用。
循环引用解决的办法:
(1)使用Block使NSTimer类对象成为target,这样避免了timer对象与controller的循环引用问题。虽然解决timer对象与controller的循环引用问题,但timer会一直执行下去。
(2)使用NSProxy抽象类解决循环引用:使用抽象类来弱引用target,将timer的target设置为proxy对象,使用proxy的特性去调用对应的selector,这样就完美的解决了循环引用的问题。
(3)使用GCD的定时器。使用GCD定时器的好处是会比NSTimer更精确,NSTimer是会有误差的。(推荐使用)
什么是 CADisplayLink
CADisplaylink 是一个计时器对象,可以使用这个对象来保持应用中的绘制与显示刷新的同步。更通俗的讲,电子显示屏都是由一个个像素点构成,要让屏幕显示的内容变化,需要以一定的频率刷新这些像素点的颜色值,系统会在每次刷新时触发 CADisplaylink。
CADisplayLink默认每秒运行60次,通过它的 frameInterval 属性改变每秒运行帧数,如设置为2,意味CADisplayLink每隔一帧运行一次,有效的逻辑每秒运行30次
屏幕刷新时调用:CADisplayLink是一个能让我们以和屏幕刷新率同步的频率将特定的内容画到屏幕上的定时器类。CADisplayLink以特定模式注册到runloop后,每当屏幕显示内容刷新结束的时候,runloop就会向CADisplayLink指定的target发送一次指定的selector消息, CADisplayLink类对应的selector就会被调用一次。所以通常情况下,按照iOS设备屏幕的刷新率60次/秒
延迟:iOS设备的屏幕刷新频率是固定的,CADisplayLink在正常情况下会在每次刷新结束都被调用,精确度相当高。但如果调用的方法比较耗时,超过了屏幕刷新周期,就会导致跳过若干次回调调用机会。
如果CPU过于繁忙,无法保证屏幕60次/秒的刷新率,就会导致跳过若干次调用回调方法的机会,跳过次数取决CPU的忙碌程度。
使用场景:从原理上可以看出,CADisplayLink适合做界面的不停重绘,比如视频播放的时候需要不停地获取下一帧用于界面渲染。
调用setPaused
暂停
[displayLink setPaused:YES];
开启
[displayLink setPaused:NO];
取消使用 CADisplayLink,调用 invalidate;
[displayLink invalidate];
_displayLink = nil;
示例
#import "ViewController2.h"
@interface ViewController2 ()
@property(nonatomic,strong)CADisplayLink *displayLink;
@property(nonatomic,assign)NSInteger index1;
@end
@implementation ViewController2
- (void)viewDidLoad {
[super viewDidLoad];
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkAction)];
//设置 CADisplayLink selector 调用时间间隔
[_displayLink setPreferredFramesPerSecond:1];
//添加到 runloop,并指定 RunloopMode;
[_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
_index1 = 1;
}
// CADisplayLink selector 调用
- (void)displayLinkAction {
NSLog(@"CADisplayLink定时器");
_index1++;
if (_index1 == 10) {
//调用setPaused:可以暂停、开启 CADisplayLink;
//[_displayLink setPaused:YES];
//开启
//[_displayLink setPaused:NO];
//取消使用 CADisplayLink,调用 invalidate;
[_displayLink invalidate];
_displayLink = nil;
}
}
@end
使用GCD的定时器。使用GCD定时器的好处是会比NSTimer更精确,NSTimer是会有误差的。
执行队列(获取全局队列)
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
创建定时器,并指定线程,dispatch_source_t 本质上也是一个OC对象;dispatch_source_set_timer自动生成
__block dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
开始时间
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_
//dispatch_time_t start = dispatch_walltime(NULL, 0);
重复间隔
uint64_t interval = (uint64_t)(1.0 * NSEC_PER_SEC);
设置定时器
dispatch_source_set_timer(_timer, start, interval, 0);
综合:设置定时器间隔时间
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
设置需要执行的事件
dispatch_source_set_event_handler(timer, ^{
});
启动定时器
dispatch_resume(timer);
延迟执行一次( 延迟2s)
dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC);
dispatch_after(delayTime, dispatch_get_main_queue(), ^(void){
NSLog(@"延迟2s后执行");
});
停止GCD定时器的方式
(1)关闭定时器
完全销毁定时器, 重新开启的话需要重新创建
全局变量, 关闭后需要置为nil
dispatch_source_cancel(_timer);
(2)暂停定时器
可使用dispatch_resume(_timer)
再次开启
全局变量, 暂停后不能置为nil, 否则不能重新开启
dispatch_suspend(_timer);
示例
#import "ViewController3.h"
@interface ViewController3 ()
@property (nonatomic, strong) dispatch_source_t timer;
@end
@implementation ViewController3
- (void)viewDidLoad {
[super viewDidLoad];
//倒计时数字
__block NSInteger timeIndex = 5;
//执行队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//创建定时器,并指定线程,dispatch_source_t 本质上也是一个OC对象;dispatch_source_set_timer自动生成
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
//设置定时器间隔时间
dispatch_source_set_timer(_timer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
//设置定时器 action
dispatch_source_set_event_handler(_timer, ^{
NSLog(@"timeIndex = %ld",timeIndex);
timeIndex-- ;
if (timeIndex == 0) {
NSLog(@"取消定时器");
// 取消定时器
dispatch_cancel(_timer);
_timer = nil;
}
});
dispatch_resume(_timer);//启动定时器
}
//定时器释放
- (void)dealloc {
dispatch_source_cancel(_timer);
dispatch_cancel(_timer);
_timer = nil;
}
执行结果
2019-05-24 11:08:19.774614+0800 MGNSTimer[3665:776596] timeIndex = 5
2019-05-24 11:08:20.775808+0800 MGNSTimer[3665:776596] timeIndex = 4
2019-05-24 11:08:21.775761+0800 MGNSTimer[3665:776596] timeIndex = 3
2019-05-24 11:08:22.775792+0800 MGNSTimer[3665:776597] timeIndex = 2
2019-05-24 11:08:23.775832+0800 MGNSTimer[3665:776597] timeIndex = 1
2019-05-24 11:08:23.776020+0800 MGNSTimer[3665:776597] 取消定时器
/* NSTimer.h
Copyright (c) 1994-2018, Apple Inc. All rights reserved.
*/
#import
#import
NS_ASSUME_NONNULL_BEGIN
@interface NSTimer : NSObject
+ (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;
@property (copy) NSDate *fireDate;
@property (readonly) NSTimeInterval timeInterval;
// Setting a tolerance for a timer allows it to fire later than the scheduled fire date, improving the ability of the system to optimize for increased power savings and responsiveness. The timer may fire at any time between its scheduled fire date and the scheduled fire date plus the tolerance. The timer will not fire before the scheduled fire date. For repeating timers, the next fire date is calculated from the original fire date regardless of tolerance applied at individual fire times, to avoid drift. The default value is zero, which means no additional tolerance is applied. The system reserves the right to apply a small amount of tolerance to certain timers regardless of the value of this property.
// As the user of the timer, you will have the best idea of what an appropriate tolerance for a timer may be. A general rule of thumb, though, is to set the tolerance to at least 10% of the interval, for a repeating timer. Even a small amount of tolerance will have a significant positive impact on the power usage of your application. The system may put a maximum value of the tolerance.
@property NSTimeInterval tolerance API_AVAILABLE(macos(10.9), ios(7.0), watchos(2.0), tvos(9.0));
- (void)invalidate;
@property (readonly, getter=isValid) BOOL valid;
@property (nullable, readonly, retain) id userInfo;
@end
NS_ASSUME_NONNULL_END