NSTimer

作者: 薛定谔的汪汪 | 来源:发表于2018-12-22 22:24 被阅读0次

    一、创建定时器

    (1)动态方法创建:

    A:Target/Action方式

    - (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(nullable id)ui repeats:(BOOL)rep NS_DESIGNATED_INITIALIZER;
    
    //获取当前时间
    NSDate *date = [NSDate date];//[[NSDate alloc]init];
    NSTimer *timer = [[NSTimer alloc]initWithFireDate:date interval:2 target:self selector:@selector(nextPage) userInfo:nil repeats:YES];
    //从当前时间开始,以后每隔2秒就调用一次当前类(self)的nextpage方法
    

    (1)参数Date:从何时开始(立即调用?3秒以后?还是某个确定的时间点?),也是响应方法的首次调用时刻(添加进mode后是立即调用还是隔多少秒以后掉用);
    (2)参数Repeat:是否重复调用响应方法。如果为NO,表示只调用一次响应方法;

    B:Block方式

    - (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
    
    NSTimer *timer = [[NSTimer alloc]initWithFireDate:[NSDate date] interval:2.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
            NSLog(@"我被调用了!");
    }];
    //从当前时刻开始(先调用一次),以后每隔2秒打印一次“我被调用了!”
    

    (2)静态方法创建:

    A:Target/Action方式

    + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
    
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(nextPage) userInfo:nil repeats:YES];
    /*默认立即开始(立即调用相应方法),当然可以重设首次调用时刻,如:
    timer.fireDate = [NSDate dateWithTimeIntervalSinceNow:3];
    */
    

    B:Block方式

    + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
    
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
            NSLog(@"我被调用了!");
    }];
    

    C:Invocation方式

    + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
    
     不常用,待补充。。。
    

    二、开启定时器

    [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
    
    注册timer

    从这里可以看出,the receiver似乎指的是runloop。

    (1)只有将在RunLoop注册过的NSTimer添加进指定mode中,timer才能被触发(也即你只需要将timer添加进runloop中即可)。所以以上两大类方式创建的timer是不能被触发的。
    (2)为了将“安装”进modes中timer移除,我们需要向timer发送invalidate消息。
    (3)mode:runloop的运行模式,runloop有多种运行模式,它需要知道timer处于哪种模式下才有效。

    三、创建+开启一并执行

    A:Target/Action方式

    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
    
    [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(nextPage) userInfo:nil repeats:YES];
    

    B:Block方式

    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
    
    [NSTimer scheduledTimerWithTimeInterval:2.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
            NSLog(@"我被调用了!");
        }];
    

    C:Invocation方式

    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
    
     不常用,待补充。。。
    

    注意:这三种方式创建的timer并没有指定fireDate,所以它们不是添加进runloop后就立即调用一次的,要想一进runloop就调用,需要立即调用fire方法。

    四、启动与失效

    //timer是否“有效”
    @property (readonly, getter=isValid) BOOL valid;
    
    //schedule方式创建的timer时不需要额外开启的,它是自动添加进runloop中并自动开启的,但注意:它不能保证一进runloop就立即调用,fire方法可以做到。
    - (void)fire;
    

    fire:使添加进NSRunLoop的timer立即执行timerSelector方法。

    //让“有效”的timer变得失效
    - (void)invalidate;
    

    - (void)invalidate;
    (1)Stops the timer from ever firing again and requests its removal from its run loop.(永远阻止timer重启,并且将其从RunLoop中移除)
    (2)《1》This method is the only way to remove a timer from an NSRunLoop object. The NSRunLoop object removes its strong reference to the timer, either just before the invalidate method returns or at some later point.(它是从NSRunLoop对象中移除一个timer的仅有的方法。或者在invalidate方法返回前,或者其它时刻,该NSRunLoop对象会移除对这个timer的引用)《2》If it was configured with target and user info objects, the receiver removes its strong references to those objects as well.(如果timer被绑定了target和info对象,the receiver也会移除对它们的强引用)(额,这个the receiver指的是谁呢?target?不像啊,自己对自己移除强引用?难道是runloop?哪位大神给解读下。。。从注册timer时的解读 The timer to register with the receiver.来看,the receiver似乎指的是RunLoop本身,register with :向...注册/登记)

    所以,当timer执行完invalidate方法后,这个timer就永远不能用了,如果需要定时操作,只能重新创建timer对象。故,invalidate的意义不应该仅仅理解为“暂停”,应该理解为“注销”(从runloop对象中注销)

    五、NSTimer与ViewController的引用

    图片一

    正常情况下,当控制器返回后就销毁了。

    图片二

    如图二,当加入NSTimer后,再返回第一界面时发现SecondViewController没有被销毁!那只能说明定时器的加入引用了SecondViewController,导致其不能被销毁;

    那到底哪里出错了呢?

    target: The object to which to send the message specified by aSelector when the timer fires. The timer maintains a strong reference to this object until it (the timer) is invalidated.
    很显然,创建定时器时指定的target(self)导致了定时器对SecondViewController的引用(我不关心我强引用了谁,大不了最后一块儿销毁你们。劳资关心的是你们谁TM强引用了我,你们引用了我让我怎么销毁。)

    将self.timer置为nil会不会让SecondViewController的销毁恢复正常呢?


    图片三

    很显然嘛,我不能靠“放了你们”而去期待你们也能“放过我”

    那如果让定时器失效呢?能否让SecondViewController的销毁恢复正常呢?


    图片四

    可以!(为了大义,你们自裁吧。。。)当然最好执行self.timer = nil;,将对象置为nil是一种规范和习惯,其实它才是导致timer被真正销毁timer的原因。
    额。。。其实还有ViewController被销毁了还在执行任务的NSTimer,执行任务也是好性能的嘛。

    综上所述,当将timer添加至mode中时,它们的引用关系如下:


    引用关系图

    如理解有误还望大神指正,在此谢过。

    图片五

    其实,它能执行任务是因为自己没有被销毁;ViewController能被销毁,是因为该timer并没有引用ViewController(和控制器没有任何联系的timer又有什么用呢?)

    六、NSTimer与NSRunLoopMode

    线程的RunLoop有五种mode,但常用的NSRunLoopMode有两种:

    (1)NSDefaultRunLoopMode:主线程RunLoop开启后的常驻mode
    (2)UITrackingRunLoopMode:界面跟踪mode,主要用于处理UIScrollView及其子类的滚动事件。

    (1)每个RunLoop有五种mode,它可以很快的在各个mode间完成切换以处理对应mode中的事件。
    (2)如果NSTimer当前处于NSDefaultRunLoopMode中,此时界面上有滚动事件发生,则RunLoop会瞬间切换至UITrackingRunLoopMode模式。这意味着NSTimer会被暂停调用响应方法,直至滚动事件被处理完,当滚动事件被处理完后,RunLoop又会被瞬间切换至NSDefaultRunLoopMode模式,此时线程会查看该mode下是否有相应事件等待处理,有则继续,没有的话,runloop会进入休眠状态,直至被事件再次唤醒。
    (3)占位mode:NSRunLoopCommonModes,它包含以上两种mode,处于该mode下的事件会在两种mode下都有效。也即mode切换过程中不会中断事件的处理。
    (4)如果想让NSTimer同时在两种mode下都有效,该怎么办呢?
    答:要么将该NSTimer单独添加至两种mode下,要么一次性加入两种mode下NSRunLoopCommonModes。
    (5)Mode应用场景:轮播时,如果想拖拽查看某一页时暂停定时器;上下滑动UITableView时,不影响分页滚动界面的自动滚动。
    (6)正常情况下,轮播时,定时器会自动暂停等待,不需要额外的暂停操作,但是当拖拽事件结束后,会发现恢复自动滚动瞬间,滚动的比较快,影响体验;所以正确的做法是先invalidate & nil,拖拽结束后再重新创建NSTimer。

    总结:

    (1)timer的作用域只是决定了timer被使用的范围,也即局部的timer并不会影响响应方法的重复调用(因为走出了方法,局部变量就被销毁了),只是其他方法无法使用到而已。

    (2)timer只有被加入到runloop中才能被触发。

    (3)NSTimer创建的实例timer,响应时刻并不会那么精确,只是大概准确。

    (4)正常情况下,控制器的销毁是不受其他对象的,这时通常我们会在dealloc方法中做一些控制器被销毁前的“临终”操作,比如销毁一些我们自己创建的对象。如果发现控制器没有走dealloc方法,那只能说明控制器还在被强引用,所以平时我们需要清晰的知道对控制器的强引用都有哪些情况。

    (5)定时器的加入,会影响当前控制器的正常销毁流程,因为runloop内部持有了当前控制器,有且仅有定时器被销毁才能结束对控制器的引用。

    (6)当然,并非创建的所有的NSTimer都会强引用ViewController,比如repeats为NO的定时器就不会,还有block方式创建的定时器。

    (7)当然,销毁定时器的操作所放位置也至关重要。如果放在dealloc方法里就完了,因为那样dealloc根本不会被执行!所以我们必须在dealloc方法执行之前要有invalidate操作。

    相关文章

      网友评论

          本文标题:NSTimer

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