以下引用一位博主的话:
很多人看到NSTimer,咋一看,谁不会用,不就是一个定时完成任务的东西吗?
一、什么是NSTimer
官方给出解释是
“A timer provides a way to perform a delayed action or a periodic action. The timer waits until a certain time interval has elapsed and then fires, sending a specified message to a specified object. ”
翻译过来就是timer就是一个能在从现在开始的后面的某一个时刻或者周期性的执行我们指定的方法的对象。
二、NSTimer和它调用的函数对象间到底发生了什么
从前面官方给出的解释可以看出timer会在未来的某个时刻执行一次或者多次我们指定的方法,这也就牵扯出一个问题,如何保证timer在未来的某个时刻触发指定事件的时候,我们指定的方法是有效的呢?
解决方法很简单,只要将指定给timer的方法的接收者retain一份就搞定了,实际上系统也是这样做的。不管是重复性的timer还是一次性的timer都会对它的方法的接收者进行retain,这两种timer的区别在于“一次性的timer在完成调用以后会自动将自己invalidate,而重复的timer则将永生,直到你显示的invalidate它为止”。
如果我们启动了一个定时器,在某个界面释放前,将这个定时器停止,甚至置为nil,都不能使这个界面释放,原因是系统的循环池中还保有这个对象。需要以下操作
[timer invalidate]; // 从运行循环中移除, 对运行循环的引用进行一次 release
timer=nil; // 将销毁定时器
请看下面demo:
KSTestTimer.h文件:
//
// KSTestTimer.h
// KSenTimer
//
// Created by ehsy-pc on 2017/10/31.
// Copyright © 2017年 ehsy-pc. All rights reserved.
//
#import
@interface KSTestTimer : NSObject
/*
* @brief timer响应函数,只是用来做测试
*/
- (void)timerAction:(NSTimer*)timer;
@end
KSTestTimer.m文件:
//
// KSTestTimer.m
// KSenTimer
//
// Created by ehsy-pc on 2017/10/31.
// Copyright © 2017年 ehsy-pc. All rights reserved.
//
#import "KSTestTimer.h"
@implementation KSTestTimer
-(id)init{
self = [super init];
if(self){
NSLog(@"instance %@ has been created",self);
}
return self;
}
-(void)dealloc
{
NSLog(@"instance %@ has been dealloced!",self);
// [super dealloc];
}
-(void)timerAction:(NSTimer *)timer
{
NSLog(@"Hi, Timer Action for instance %@", self);
}
@end
FirstViewController.m文件:
/**
@param sender 点击不重复执行的timerbtn
*/
- (IBAction)cilckNoRepeatBtn:(UIButton *)sender {
NSLog(@"Test retatin target for non-repeat timer!");
KSTestTimer *testObject = [[KSTestTimer alloc] init];
[NSTimer scheduledTimerWithTimeInterval:5 target:testObject selector:@selector(timerAction:) userInfo:nil repeats:NO];
NSLog(@"Invoke release to testObject!");
}
/**
@param sender 点击重复执行的timerbtn
*/
- (IBAction)clickRepeatBtn:(UIButton *)sender {
NSLog(@"Test retain target for repeat Timer");
KSTestTimer *testObject2 = [[KSTestTimer alloc] init];
[NSTimer scheduledTimerWithTimeInterval:5 target:testObject2 selector:@selector(timerAction:) userInfo:nil repeats:YES];
NSLog(@"Invoke release to testObject2!");
}
上面的简单例子中,我们自定义了一个继承自NSObject的类KSTestTimer,在这个类的init,dealloc和它的timerAction三个方法中分别打印信息。
然后在FirstViewController.m中分别测试一个单次执行的timer和一个重复执行的timer对方法接受者是否做了retain操作,因此我们在两种情况下都是shedule完timer之后立马对该测试对象执行release操作(ARC下,该代码中定时器方法调用完,该对象就被release了)。
点击不重复执行的timer,结果如下:
观察输出结果,发现13:39:57.529的时候 我们就对该对象进行了release操作,但是 13:39:57.529的时候定时器触发并执行完方法后,才进行了实际的dealloc操作,这就说明一次性的timer也会retain它的方法接受者,直到自己消失为止。
点击可以重复执行的timer,结果如下:
观察输出我们发现,这个重复性的timer一直都在周期性的调用我们为它指定的方法,而且测试的对象也一直没有真正的被释放。
通过以上小例子,我们可以发现在timer对它的接收者进行retain,从而保证了timer调用时的正确性,但是又引入了接收者的内存管理问题。特别是对于重复性的timer,它所引用的对象将一直存在,将会造成内存泄露。
有问题就有应对方法,NSTimer提供了一个方法invalidate,让我们可以解决这种问题。不管是一次性的还是重复性的timer,在执行完invalidate以后都会变成无效,因此对于重复性的timer我们一定要有对应的invalidate。
下面我们看一个案例:
我们新建了一个类 KSTestSelf:
KSTestSelf.h文件:
//
// KSTestSelf.h
// KSenTimer
//
// Created by ehsy-pc on 2017/10/31.
// Copyright © 2017年 ehsy-pc. All rights reserved.
//
#import
@interface KSTestSelf : NSObject
@property(nonatomic,strong) NSTimer *timer;
@end
KSTestSelf.m文件:
//
// KSTestSelf.m
// KSenTimer
//
// Created by ehsy-pc on 2017/10/31.
// Copyright © 2017年 ehsy-pc. All rights reserved.
//
#import "KSTestSelf.h"
@implementation KSTestSelf
- (id)init
{
self = [super init];
if (self) {
NSLog(@"Test retatin target for non-repeat timer!");
_timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(testTimer:) userInfo:nil repeats:YES];
}
return self;
}
- (void)dealloc
{
NSLog(@"测试是不是执行了这个方法!");
// 自欺欺人的写法,永远都不会执行到,除非你在外部手动invalidate这个timer
[_timer invalidate];
}
- (void)testTimer:(NSTimer*)timer
{
NSLog(@"hahahaha!");
}
@end
然后我们在FirstViewController.m里这样调用
/**
@param sender 测试 KSTestSelf类里 dealloc方法有没用执行
*/
- (IBAction)clickTestBtn:(UIButton *)sender {
KSTestSelf *testObject = [[KSTestSelf alloc] init];
}
运行可以发现 KSTestSelf.m里的dealloc方法并没有调用
然后我们稍作修改
/**
@param sender 测试 KSTestSelf类里 dealloc方法有没用执行
*/
- (IBAction)clickTestBtn:(UIButton *)sender {
KSTestSelf *testObject = [[KSTestSelf alloc] init];
[testObject.timer invalidate];//增加这一句
}
发现运行结果:
综上: timer都会对它的target进行retain,我们需要小心对待这个target的生命周期问题,尤其是重复性的timer。(NSTimer初始化后,self的retainCount加1。 那么,我们需要在释放这个类之前,执行[timer invalidate];否则,不会执行该类的dealloc方法。)
三、NSTimer 会是准时触发事件吗
答案是否定的,而且有时候你会发现实际的触发时间跟你想象的差距还比较大。
NSTimer不是一个实时系统,因此不管是一次性的还是周期性的timer的实际触发事件的时间可能都会跟我们预想的会有出入。差距的大小跟当前我们程序的执行情况有关系,比如可能程序是多线程的,而你的timer只是添加在某一个线程的runloop的某一种指定的runloopmode中,由于多线程通常都是分时执行的,而且每次执行的mode也可能随着实际情况发生变化。
假设你添加了一个timer指定2秒后触发某一个事件,但是恰好那个时候当前线程在执行一个连续运算(例如大数据块的处理等),这个时候timer就会延迟到该连续运算执行完以后才会执行。
原文如下:
“A repeating timer reschedules itself based on the scheduled firing time, not the actual firing time. For example, if a timer is scheduled to fire at a particular time and every 5 seconds after that, the scheduled firing time will always fall on the original 5 second time intervals, even if the actual firing time gets delayed. If the firing time is delayed so far that it passes one or more of the scheduled firing times, the timer is fired only once for that time period; the timer is then rescheduled, after firing, for the next scheduled firing time in the future.”
由于本人英语水平有限,所以使用有道翻译了下:
"一个重复的计时器根据预定的发射时间重新安排自己的时间,而不是实际的射击时间。例如,如果一个定时器在一个特定的时间和每隔5秒就发射一次,那么预定的发射时间将总是在最初的5秒间隔内,即使实际的发射时间被延迟。如果发射时间延迟到超过预定发射时间的一个或多个,计时器在那个时间段内只发射一次;然后,计时器将在发射后重新安排,用于下一次预定的发射时间。"
下面我用一段代码及运行的截图加图示 来说明我的看法,希望你们能看明白。
我们在FirstViewController里添加了一个按钮用来测试,并增加相应方法.
/**
@param sender 测试定时器等待某个耗时任务执行以后 再次执行定时任务时时间间隔和周期规律
*/
- (IBAction)clickDelayBtn:(UIButton *)sender {
[self testTimerDelayPlay];
}
-(void)testTimerDelayPlay
{
KSTestTimer *testObject2 = [[KSTestTimer alloc] init];
[NSTimer scheduledTimerWithTimeInterval:1 target:testObject2 selector:@selector(timerAction:) userInfo:nil repeats:YES];
NSLog(@"Simulate busy");
[self performSelector:@selector(simulateBusy) withObject:nil afterDelay:3];
}
// 模拟当前线程正好繁忙的情况
- (void)simulateBusy
{
NSLog(@"start simulate busy!");
NSUInteger caculateCount = 0x0FFFFFFF;
CGFloat uselessValue = 0;
for (NSUInteger i = 0; i < caculateCount; ++i) {
uselessValue = i / 0.3333;
}
NSLog(@"finish simulate busy!");
}
例子中首先开启了一个timer,这个timer每隔1秒调用一次target的timerAction方法,紧接着我们在3秒后调用了一个模拟线程繁忙的方法(其实就是一个大的循环)。运行程序后输出结果如下:
我们运行两次:
第一次:
第二次:
上图根据定时器方法执行的时间点,大家应该能看出端倪了吧。
从第二个图来说可以发现,当线程空闲的时候timer的消息触发还是比较准确的,但是在36分04秒678 开始线程一直忙着做大量运算,直到36分07秒455 该运算才结束,这个时候timer才触发消息,这个线程繁忙的过程 超过了一个周期(1s),但是timer并没有连着触发2次消息,而仅仅只是在 时间点 36:07:455 触发了一次。
此图可以看出后面的消息触发的时间与开始时我们执行的时间点 之间的间隔还是 等于 计时器设置的执行间隔(1s)的整数倍,这也从侧面证明,定时器的执行,其实并不是按照时间段额间隔进行调用方法,而是在定时器注册到RunLoop中后,RunLoop会设置一个一个的 时间点 进行调用,例如,5,10,15,20等等。如果错过了某个时间点,定时器并不会延迟调用,而是直接等待下一个时间点调用。
注:timer不是一种实时的机制,会存在延迟,而且延迟的程度跟当前线程的执行情况有关。
不知道本人以上描述帖友们是否能看的清晰明白,有问题请直接留言。
代码下载地址:https://github.com/kinsen17/NSTimer
下面一篇文章,我会介绍定时器设置的几种方法,以及和runloop的关系。
OC_定时器2