NSTimer和它调用的函数对象间到底发生什么
timer会在未来的某个时刻执行一次或者多次我们指定的方法,这也就牵扯出一个问题,如何保证timer在未来的某个时刻触发指定事件的时候,我们指定的方法是有效的呢?
解决方法很简单,只要将指定给timer的方法的接收者retain一份就搞定了,实际上系统也是这样做的。不管是重复性的timer还是一次性的timer都会对它的方法的接收者进行retain,这两种timer的区别在于“一次性的timer在完成调用以后会自动将自己invalidate,而重复的timer则将永生,直到你调用invalidate方法终止。”
下面看一个小例子
SvTestObject.h
`#import <Foundation/Foundation.h>
@interface SvTestObject : NSObject
/*
* @brief timer响应函数,只是用来做测试
*/
- (void)timerAction:(NSTimer*)timer;
@end
SvTestObject.m
`#import "SvTestObject.h"
@implementation SvTestObject
- (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
SvTimerAppDelegate.m
- (void)applicationDidBecomeActive:(UIApplication *)application
{
// test Timer retain target
[self testNonRepeatTimer];
// [self testRepeatTimer];
}
- (void)testNonRepeatTimer
{
NSLog(@"Test retatin target for non-repeat timer!");
SvTestObject *testObject = [[SvTestObject alloc] init];
[NSTimer scheduledTimerWithTimeInterval:5 target:testObject selector:@selector(timerAction:) userInfo:nil repeats:NO];
[testObject release];
NSLog(@"Invoke release to testObject!");
}
- (void)testRepeatTimer
{
NSLog(@"Test retain target for repeat Timer");
SvTestObject *testObject2 = [[SvTestObject alloc] init];
[NSTimer scheduledTimerWithTimeInterval:5 target:testObject2 selector:@selector(timerAction:) userInfo:nil repeats:YES];
[testObject2 release];
NSLog(@"Invoke release to testObject2!");
}
上面的简单例子中,我们自定义了一个继承自NSObject的类SvTestObject,在这个类的init,dealloc和它的timerAction三个方法中分别打印信息。然后在appDelegate中分别测试一个单次执行的timer和一个重复执行的timer对方法接受者是否做了retain操作,因此我们在两种情况下都是shedule完timer之后立马对该测试对象执行release操作。
测试单次执行的timer的结果如下:
观察输出,我们会发现53分58秒的时候我们就对测试对象执行了release操作,但是知道54分03秒的时候timer触发完方法以后,该对象才实际的执行了dealloc方法。这就证明一次性的timer也会retain它的方法接收者,直到自己失效为之。
测试重复性的timer的结果如下:
观察输出我们发现,这个重复性的timer一直都在周期性的调用我们为它指定的方法,而且测试的对象也一直没有真正的被释放。
通过以上小例子,我们可以发现在timer对它的接收者进行retain,从而保证了timer调用时的正确性,但是又引入了接收者的内存管理问题。特别是对于重复性的timer,它所引用的对象将一直存在,将会造成内存泄露。
有问题就有应对方法,NSTimer提供了一个方法invalidate,让我们可以解决这种问题。不管是一次性的还是重复性的timer,在执行完invalidate以后都会变成无效,因此对于重复性的timer我们一定要有对应的invalidate。
突然想起一种自欺欺人的写法,不知道你们有没有这么写过
·#import "SvCheatYourself.h"
@interface SvCheatYourself () {
NSTimer *_timer;
}
@end
@implementation SvCheatYourself
- (id)init
{
self = [super init];
if (self) {
_timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(testTimer:) userInfo:nil repeats:YES];
}
return self;
}
- (void)dealloc
{
// 自欺欺人的写法,永远都不会执行到,除非你在外部手动invalidate这个timer
[_timer invalidate];
[super dealloc];
}
- (void)testTimer:(NSTimer*)timer
{
NSLog(@"haha!");
}
@end
总结:
timer都会对它的target进行retain,我们需要小心对待这个target的生命周期问题,尤其是重复性的timer。
NSTimer为什么要添加到RunLoop中才会有作用
前面的例子中我们使用的是一种便利方法,它其实是做了两件事:首先创建一个timer,然后将该timer添加到当前runloop的default mode中。也就是这个便利方法给我们造成了只要创建了timer就可以生效的错觉,我们当然可以自己创建timer,然后手动的把它添加到指定runloop的指定mode中去。
NSTimer其实也是一种资源,如果看过多线程变成指引文档的话,我们会发现所有的source如果要起作用,就得加到runloop中去。同理timer这种资源要想起作用,那肯定也需要加到runloop中才会又效喽。如果一个runloop里面不包含任何资源的话,运行该runloop时会立马退出。你可能会说那我们APP的主线程的runloop我们没有往其中添加任何资源,为什么它还好好的运行。我们不添加,不代表框架没有添加,如果有兴趣的话你可以打印一下main thread的runloop,你会发现有很多资源。
下面我们看一个小例子:
- (void)applicationDidBecomeActive:(UIApplication *)application
{
[self testTimerWithOutShedule];
}
- (void)testTimerWithOutShedule
{
NSLog(@"Test timer without shedult to runloop");
SvTestObject *testObject3 = [[SvTestObject alloc] init];
NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:1] interval:1 target:testObject3 selector:@selector(timerAction:) userInfo:nil repeats:NO];
[testObject3 release];
NSLog(@"invoke release to testObject3");
}
- (void)applicationWillResignActive:(UIApplication *)application
{
NSLog(@"SvTimerSample Will resign Avtive!");
}
这个小例子中我们新建了一个timer,为它指定了有效的target和selector,并指出了1秒后触发该消息,运行结果如下:
观察发现这个消息永远也不会触发,原因很简单,我们没有将timer添加到runloop中。
总结: 必须得把timer添加到runloop中,它才会生效。
NSTimer加到了RunLoop中但迟迟的不触发事件
为什么明明添加了,但是就是不按照预先的逻辑触发事件呢???原因主要有以下两个:
1、runloop是否运行
每一个线程都有它自己的runloop,程序的主线程会自动的使runloop生效,但对于我们自己新建的线程,它的runloop是不会自己运行起来,当我们需要使用它的runloop时,就得自己启动。
那么如果我们把一个timer添加到了非主线的runloop中,它还会按照预期按时触发吗?下面请看一段测试程序:
- (void)applicationDidBecomeActive:(UIApplication *)application
{
[NSThread detachNewThreadSelector:@selector(testTimerSheduleToRunloop1) toTarget:self withObject:nil];
}
// 测试把timer加到不运行的runloop上的情况
- (void)testTimerSheduleToRunloop1
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSLog(@"Test timer shedult to a non-running runloop");
SvTestObject *testObject4 = [[SvTestObject alloc] init];
NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:1] interval:1 target:testObject4 selector:@selector(timerAction:) userInfo:nil repeats:NO];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
// 打开下面一行输出runloop的内容就可以看出,timer却是已经被添加进去
//NSLog(@"the thread's runloop: %@", [NSRunLoop currentRunLoop]);
// 打开下面一行, 该线程的runloop就会运行起来,timer才会起作用
//[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
[testObject4 release];
NSLog(@"invoke release to testObject4");
[pool release];
}
- (void)applicationWillResignActive:(UIApplication *)application
{
NSLog(@"SvTimerSample Will resign Avtive!");
}
上面的程序中,我们新创建了一个线程,然后创建一个timer,并把它添加当该线程的runloop当中,但是运行结果如下:
观察运行结果,我们发现这个timer知道执行退出也没有触发我们指定的方法,如果我们把上面测试程序中“//[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];”这一行的注释去掉,则timer将会正确的掉用我们指定的方法。
2、mode是否正确
我们前面自己动手添加runloop的时候,可以看到有一个参数runloopMode,这个参数是干嘛的呢?
前面提到了要想timer生效,我们就得把它添加到指定runloop的指定mode中去,通常是主线程的defalut mode。但有时我们这样做了,却仍然发现timer还是没有触发事件。这是为什么呢?
这是因为timer添加的时候,我们需要指定一个mode,因为同一线程的runloop在运行的时候,任意时刻只能处于一种mode。所以只能当程序处于这种mode的时候,timer才能得到触发事件的机会。
举个不恰当的例子,我们说兄弟几个分别代表runloop的mode,timer代表他们自己的才水桶,然后一群人去排队打水,只有一个水龙头,那么同一时刻,肯定只能有一个人处于接水的状态。也就是说你虽然给了老二一个桶,但是还没轮到它,那么你就得等,只有轮到他的时候你的水桶才能碰上用场。
综上: 要让timer生效,必须保证该线程的runloop已启动,而且其运行的runloopmode也要匹配。