NSTimer 运行机制2

NSTimer和它调用的函数对象间到底发生什么

timer会在未来的某个时刻执行一次或者多次我们指定的方法,这也就牵扯出一个问题,如何保证timer在未来的某个时刻触发指定事件的时候,我们指定的方法是有效的呢?

解决方法很简单,只要将指定给timer的方法的接收者retain一份就搞定了,实际上系统也是这样做的。不管是重复性的timer还是一次性的timer都会对它的方法的接收者进行retain,这两种timer的区别在于“一次性的timer在完成调用以后会自动将自己invalidate,而重复的timer则将永生,直到你调用invalidate方法终止。”

下面看一个小例子

SvTestObject.h

`#import 

@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的结果如下:


NSTimer 运行机制2_第1张图片
timer单次执行

观察输出,我们会发现53分58秒的时候我们就对测试对象执行了release操作,但是知道54分03秒的时候timer触发完方法以后,该对象才实际的执行了dealloc方法。这就证明一次性的timer也会retain它的方法接收者,直到自己失效为之。

测试重复性的timer的结果如下:


NSTimer 运行机制2_第2张图片
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秒后触发该消息,运行结果如下:

NSTimer 运行机制2_第3张图片
timer 延迟执行

观察发现这个消息永远也不会触发,原因很简单,我们没有将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当中,但是运行结果如下:

NSTimer 运行机制2_第4张图片

观察运行结果,我们发现这个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也要匹配。

你可能感兴趣的:(NSTimer 运行机制2)