NSTimer 运行机制2

作者: 随风__陈坪__ | 来源:发表于2015-08-09 10:42 被阅读62次

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


    timer单次执行

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

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


    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 延迟执行

    观察发现这个消息永远也不会触发,原因很简单,我们没有将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也要匹配。

    相关文章

      网友评论

        本文标题:NSTimer 运行机制2

        本文链接:https://www.haomeiwen.com/subject/wlqiqttx.html