NSTimer的使用

作者: Laughingg | 来源:发表于2015-12-11 23:08 被阅读27460次

    NSTimer 的头文件

    /*  NSTimer.h
        Copyright (c) 1994-2015, Apple Inc. All rights reserved.
    */
    
    #import <Foundation/NSObject.h>
    #import <Foundation/NSDate.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface NSTimer : NSObject
    
    /**  这下面主要是一些构造方法*/
    
    
    // Use the timerWithTimeInterval:invocation:repeats: or timerWithTimeInterval:target:selector:userInfo:repeats: class method to create the timer object without scheduling it on a run loop. (After creating it, you must add the timer to a run loop manually by calling the addTimer:forMode: method of the corresponding NSRunLoop object.)
    // 创建一个定时器,但是么有添加到运行循环,我们需要在创建定时器后手动的调用 NSRunLoop 对象的 addTimer:forMode: 方法。
    + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
    
    // Creates and returns a new NSTimer object and schedules it on the current run loop in the default mode.
    // 创建一个timer并把它指定到一个默认的runloop模式中,并且在 TimeInterval时间后 启动定时器
    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;   
    
    // Use the timerWithTimeInterval:invocation:repeats: or timerWithTimeInterval:target:selector:userInfo:repeats: class method to create the timer object without scheduling it on a run loop. (After creating it, you must add the timer to a run loop manually by calling the addTimer:forMode: method of the corresponding NSRunLoop object.)
    // 创建一个定时器,但是么有添加到运行循环,我们需要在创建定时器后手动的调用 NSRunLoop 对象的 addTimer:forMode: 方法。
    + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
    
    // Creates and returns a new NSTimer object and schedules it on the current run loop in the default mode.
    // 创建一个timer并把它指定到一个默认的runloop模式中,并且在 TimeInterval时间后 启动定时器
    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
    
    // 默认的初始化方法,(创建定时器后,手动添加到 运行循环,并且手动触发才会启动定时器)
    - (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(nullable id)ui repeats:(BOOL)rep NS_DESIGNATED_INITIALIZER;
    
    
    // You can use this method to fire a repeating timer without interrupting its regular firing schedule. If the timer is non-repeating, it is automatically invalidated after firing, even if its scheduled fire date has not arrived.   
    // 启动 Timer 触发Target的方法调用但是并不会改变Timer的时间设置。 即 time没有到达到,Timer会立即启动调用方法且没有改变时间设置,当时间 time 到了的时候,Timer还是会调用方法。
    - (void)fire;
    
    // 这是设置定时器的启动时间,常用来管理定时器的启动与停止
    @property (copy) NSDate *fireDate;
          // 启动定时器 
              timer.fireDate = [NSDate distantPast];    
          //停止定时器 
              timer.fireDate = [NSDate distantFuture];
          // 开启 
             [time setFireDate:[NSDate  distanPast]]
          // NSTimer   关闭  
            [time  setFireDate:[NSDate  distantFunture]]
          //继续。
            [timer setFireDate:[NSDate date]]; 
          
    
    // 这个是一个只读属性,获取定时器调用间隔时间
    @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.
    
    // 这是7.0之后新增的一个属性,因为NSTimer并不完全精准,通过这个值设置误差范围
    @property NSTimeInterval tolerance NS_AVAILABLE(10_9, 7_0);
    
    // 停止 Timer ---> 唯一的方法将定时器从循环池中移除
    - (void)invalidate;
    
    // 获取定时器是否有效
    @property (readonly, getter=isValid) BOOL valid;
    
    // 获取参数信息---> 通常传入的是 nil
    @property (nullable, readonly, retain) id userInfo;
    
    @end
    
    NS_ASSUME_NONNULL_END
    

    注意:这五种初始化方法的异同:

        1、参数repeats是指定是否循环执行,YES将循环,NO将只执行一次。
        2、timerWithTimeInterval  这两个类方法创建出来的对象如果不用 addTimer: forMode方法手动加入主循环池中,将不会循环执行。
        3、scheduledTimerWithTimeInterval  这两个方法会将定时器添加到当前的运行循环,运行循环的模式为默认模式。
        4、init方法需要手动加入循环池,它会在设定的启动时间启动。
    

    NSTimer 使用过程中的问题:
    1、 内存释放问题
    如果我们启动了一个定时器,在某个界面释放前,将这个定时器停止,甚至置为nil,都不能使这个界面释放,原因是系统的循环池中还保有这个对象。
    (** timer都会对它的target进行retain,我们需要小心对待这个target的生命周期问题,尤其是重复性的timer**)

    所以我们需要这样做:

    -(void)dealloc{
           NSLog(@"dealloc:%@",[self class]);
    }
    
    - (void)viewDidLoad {
          [super viewDidLoad];
          timer= [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(myLog:) userInfo:nil repeats:YES];
    UIButton *btn = [[UIButton alloc]initWithFrame:CGRectMake(0, 0, 100, 100)];
          btn.backgroundColor=[UIColor redColor];
          [btn addTarget:self action:@selector(btn) forControlEvents:UIControlEventTouchUpInside];
          [self.view addSubview:btn];
    }
    
    -(void)btn{    
          if (timer.isValid) {       
              [timer invalidate];  // 从运行循环中移除, 对运行循环的引用进行一次 release
              timer=nil;            // 将销毁定时器
          }
          
          [self dismissViewControllerAnimated:YES completion:nil];
    }
    

    NSTimer为什么要添加到RunLoop中才会有作用

    便利构造器,它其实是做了两件事:
    首先创建一个timer,然后将该timer添加到当前runloop的default mode中。

    也就是这个便利方法给我们造成了只要创建了timer就可以生效的错觉,我们当然可以自己创建timer,然后手动的把它添加到指定runloop的指定mode中去。

    NSTimer其实也是一种资源(事件),如果看过多线程变成指引文档的话,我们会发现所有的source(事件)如果要起作用,就得加到runloop中去。
    同理timer这种资源要想起作用,那肯定也需要加到runloop中才会有效喽。
    如果一个runloop里面不包含任何资源(事件)的话,运行该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];
        
        NSLog(@"invoke release to testObject3");
    }
    
    - (void)applicationWillResignActive:(UIApplication *)application
    {
        NSLog(@"SvTimerSample Will resign Avtive!");
    }
    

    我们新建了一个timer,为它指定了有效的target和selector,并指出了1秒后触发该消息,运行结果如下:


    Snip20151212_4.png

    消息永远也不会触发,原因很简单,我们没有将timer添加到runloop中。
      综上: 必须得把timer添加到runloop中,它才会生效。

    NSTimer加到了RunLoop中但迟迟的不触发事件

    原因主要有以下两个:
    1、runloop是否运行
    每一个线程都有它自己的runloop,程序的主线程会自动的使runloop生效,但对于我们自己新建的线程,它的runloop是不会自己运行起来,当我们需要使用它的runloop时,就得自己启动。

    - (void)applicationDidBecomeActive:(UIApplication *)application
    {
         // NSThread 创建一个子线程
        [NSThread  detachNewThreadSelector:@selector(testTimerSheduleToRunloop1) toTarget:self withObject:nil];
    }
    
    // 测试把timer加到不运行的runloop上的情况
    - (void)testTimerSheduleToRunloop1
    {   
        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]];
    
        NSLog(@"invoke release to testObject4");
    }
    
    - (void)applicationWillResignActive:(UIApplication *)application
    {
        NSLog(@"SvTimerSample Will resign Avtive!");
    }
    

    我们新创建了一个线程,然后创建一个timer,并把它添加当该线程的runloop当中,但是运行结果如下:

    Snip20151212_5.png

    发现这个timer知道执行退出也没有触发我们指定的方法,如果我们把上面测试程序中“

    //[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
    

    这一行的注释去掉,则timer将会正确的掉用我们指定的方法。

    2、mode是否正确
    手动添加runloop的时候,可以看到有一个参数runloopMode,这个参数是干嘛的呢?
      前面提到了要想timer生效,我们就得把它添加到指定runloop的指定mode中去,通常是主线程的defalut mode。但有时我们这样做了,却仍然发现timer还是没有触发事件。
    这是因为timer添加的时候,我们需要指定一个mode,因为同一线程的runloop在运行的时候,任意时刻只能处于一种mode。所以只能当程序处于这种mode的时候,timer才能得到触发事件的机会。

    综上: 要让timer生效,必须保证该线程的runloop已启动,而且其运行的runloopmode也要匹配。

    NSTimer 的使用

    //不重复,只调用一次。timer运行一次就会自动停止运行 
    myTimer = [NSTimer scheduledTimerWithTimeInterval:1.5 target:self selector:@selector(scrollTimer) userInfo:nil repeats:NO];  
     
    
    需要重复调用, repeats参数改为 YES . ---> 定时器的模式是默认的
    //每1秒运行一次function方法。  
    timer =  [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(function:) userInfo:nil repeats:YES];  
    
    注意点:
    将计数器的repeats设置为YES的时候,self的引用计数会加1。
    因此可能会导致self(即viewController)不能release。
    所以,必须在viewWillAppear的时候,将计数器timer停止,否则可能会导致内存泄露。
    
    
    //取消定时器  
    [timer invalidate];   // 将定时器从运行循环中移除,
    timer = nil;    // 销毁定时器 ---》 这样可以避免控制器不死
    
    
    要想实现:先停止,然后再某种情况下再次开启运行timer,可以使用下面的方法:
    首先关闭定时器不能使用上面的方法,应该使用下面的方法:
    //关闭定时器  
    [myTimer setFireDate:[NSDate distantFuture]];
    
    然后就可以使用下面的方法再此开启这个timer了:
    //开启定时器  
    [myTimer setFireDate:[NSDate distantPast]];  
    
    
    例子:比如,在页面消失的时候关闭定时器,然后等页面再次打开的时候,又开启定时器。
    (主要是为了防止它在后台运行,暂用CPU)可以使用下面的代码实现:
    
    //页面将要进入前台,开启定时器  
    -(void)viewWillAppear:(BOOL)animated  
    {  
        //开启定时器  
        [scrollView.myTimer setFireDate:[NSDate distantPast]];  
    }  
      
    //页面消失,进入后台不显示该页面,关闭定时器  
    -(void)viewDidDisappear:(BOOL)animated  
    {  
        //关闭定时器  
        [scrollView.myTimer setFireDate:[NSDate distantFuture]];  
    }  
    

    注意点:
    [timer invalidate]是唯一的方法将定时器从循环池中移除
    NSTimer可以精确到50-100毫秒.
    NSTimeInterval类:是一个浮点数字,用来定义秒
    NSTimer不是绝对准确的,而且中间耗时或阻塞错过下一个点,那么下一个点就pass过去了.

    相关文章

      网友评论

      • 微小的沙土:所以 NSTimer加到了RunLoop中但迟迟的不触发事件到底是为什么没说啊,要使用啥模式也没说了。。。怎么才能用那种方式触发?
      • 攻克乃还_:这样不会造成循环引用吗?你并没有invalidate定时器。
      • SvenDai: 参考别人的文章记得给个参考链接,这是对别人劳动成果的尊重,况且你这个错别字都复制过来了http://blog.csdn.net/yongyinmg/article/details/20384589
        Laughingg:@UnknownError 滚
        微小的沙土:原来又是一个复制黏贴党。。。
      • Limo_:"如果看过多线程变成指引文档的话",有错别字“变成”-编程。
      • Single_CCh:有2个问题:
        1. nstimer不管是不是控制器的属性,只要实例化出来就一定被控制器持有?我测试timer不设为属性而是在一个函数内直接实例化用,结果显示是的...引起循环引用了。
        2.nstimer在iOS10之后可以直接用block定义,那这样就变成了block的循环问题,我测试结果是的,block不使用(复制持有)有控制器的属性,则不会引用循环,一旦用了控制器的属性则变成循环引用。这是对的,问题在于dealloc正常打印了销毁,但是NSTimer依然在销毁后继续打印计时...
        47e2b53b72da:你是不是没在viewDidDisappear中调用[timer invalidate]?
      • 出门右转掘金见:这个markdown模式看代码好麻烦,还要用鼠标拖后面的东西.
      • pengxuyuan:timer = nil; // 销毁定时器 ---》 这样可以避免控制器不死
        这个是为什么呢?
        攻克乃还_:避免控制器不死?你是说循环引用了吧? = nil并不会真正的销毁定时器。它在runloop中还是存在的!
      • 高阿呆:对啊,必须释放掉,不然内存就泄露了
      • Laughingg:你确定有内存泄漏
      • CaptainSirZero:"所以,必须在viewWillAppear的时候,将计数器timer停止,否则可能会导致内存泄露。 "
        这个定时器停止的方法是不是应该在viewWillDisappear: 的时候,是不是打错了?

      本文标题:NSTimer的使用

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