定时器

  • 定时器的的概念

一个在确定时间间隔内执行一个或者多个我们制定对象的方法的对象
先抛出一个问题,如果多次执行对象的方法,它怎么保证执行的是否对象不被释放呢
先给出几个问题

1.你知道NSTimer会retain你添加调用方法的对象吗
2.你知道NSTimer并不是每次都准备的按照你设定的时间来执行吗
3.NSTimer需要和NSRunloop结合起来使用,你知道需要怎么结合使用吗
4.除了NSTimer,还有哪几种定时器吗

带着问题开始上代码测试

  • 定时器的引用场景

应用场景自然不用说,太多了,购物的限时抢购,寻车里的倒计时等等

  • 定时器的常用方式

-(void)startTimerOne{

    _timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
}

-(void)startTimerTwo{

    _timer= [NSTimer timerWithTimeInterval:1.f target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
}

-(void)timerAction{

    NSLog(@"timerAction");
}

如上两个开启定时器的方式,分别调用测试,发现startTimerOne会开启定时会有打印,而startTimerTwo没有打印
好的,那么这两个方法有什么区别,按住option键查看下官方怎么说的

E92826F4-2F84-4D5F-8B4B-5D30213F13A5.png
D1FD62EE-C3D7-414D-BADB-490409D06A5F.png

文档上介绍的这两种方法的区别
scheduledTimerWithTimeInterval:创建并且返回一个NSTimer对象,并且把这个对象提交到当前的runloop上以默认的runloop模式

timerWithTimeInterval:创建并且返回一个NSTimer的对象,你自己必须把这个新创建的timer对象加入到runloop中,用addTimer:forMode:方法,在一个周期之后开始触发你设定的方法(这是什么意思呢)

_timer= [NSTimer timerWithTimeInterval:5.f target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSDefaultRunLoopMode];

比如创建了timer,也添加了到runloop中,但是它不会马上执行,而是会等一个周期,这里的周期是5秒,那么如果想从当前时间马上开始,就得

 _timer= [NSTimer timerWithTimeInterval:5.f target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
 [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSDefaultRunLoopMode];
 [_timer fire];//添加个方法
  • fire方法的正确理解

上面提到了这个方法,这个方法用来干什么的呢:使接收方的消息被发送到其目标。
您可以使用此方法触发重复计时器,而不中断其常规触发调度周期。如果计时器不重复,则在fire自动失效,即使它的预定fire日期还没有到达。

  • NSRunloopMode对定时器的影响

如果你的定时器的mode是NSDefaultRunLoopMode ,那么滑动UIScrollView的话,那么定时器就会停止
因为runloop同一时间只会处理一种mode,而UIScrollView滑动是trackingMode,而trackingMode比NSDefaultRunLoopMode的优先级要高,而另一种NSRunloopCommonModes包含了这两种mode,所以只要把定时器的mode设置成这个就解决了UIScrollView滑动的问题
如果把定时器加入到trackingMode模式的话会怎么样呢,就是有滑动UI的时候,定时器打印就在,滑动停止,打印没了,因为UI模式只能被UI事件唤醒

如果把定时器加入到NSRunloopCommonModes模式其实就是这两句话代码

 [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
  • 定时器一定会在规定时间间隔内事实触发事件吗

- (void)viewDidLoad {
    [super viewDidLoad];
  
    [self startTimerTwo];

 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        
        [self doBusyThings];
    });
    
}

-(void)doBusyThings{

    NSUInteger count = 0xFFFFFFF;
    CGFloat num =0.f;
    for (int i =0; i<count; i++) {
        num=i/count;
    }
}

-(void)startTimerTwo{

    _timer= [NSTimer timerWithTimeInterval:1.f target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
    [_timer fire];
}

代码意思是在执行定时器三秒给一个耗时操作

C1801049-1C1C-4BCA-88A5-519A25461ABF.png

有个耗时的操作,所以导致了定时器没有按周期规律性的在进行
所以,在定时器的线程里不应该有太耗时的操作

定时器的任务里也不应该有耗时操作,一定有耗时操作,建议在子线程里做

  • 定时器引起的循环引用

我们新建一个控制器TestViewController,点击ViewController里的listTableView跳转到TestViewController,在这里开启一个定时器,并且在delloc里invalidate定时器

- (void)viewDidLoad {
    [super viewDidLoad];

    [self startTimerOne];
}

-(void)dealloc{
    [_timer invalidate];
    NSLog(@"dealloc:%s",__func__);
}

- (IBAction)goBack:(UIButton *)sender {
    [self dismissViewControllerAnimated:YES completion:nil];
}

-(void)timerAction{
  
    NSLog(@"timerAction");
}


-(void)startTimerOne{
    
    _timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
}

然后点击返回到上一个界面发现定时器仍然在进行,并且没有进入delloc方法,说明TestViewController控制器也没有销毁

DE7AAB31-A98F-4538-B68C-86D90CC58668.png

看圈起的这段解释:该对象发送消息时指定的aSeltetor定时器。定时器保持对目标的强引用,直到定时器(定时器)失效为止
简单的说:_timer 定时器会对self进行强引用,知道定时器销毁
而现在的引用关系为:
self->_timer :self强引用_timer
_timer->self:_timer又强引用了self
而_timer的销毁又依赖于delloc方法里的[_timer invalidate];
但是self的销毁又依赖于_timer的销毁,也就是如果_timer不销毁,又不会跑入delloc,所以形成了循环引用

其实这种问题的解决方法大家都知道,但是我们先不直接说怎么解决,我们多做几种假设

假设1:既然知道_timer对于self有了强引用,那我们参数block的方法,那我们能不能把self改成weakself,那么就不是强引用了呢,代码上来

-(void)startTimerOne{
    
    __weak typeof(self)weakSelf=self;
    _timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:weakSelf selector:@selector(timerAction) userInfo:nil repeats:YES];
}

测试结果:然后点击返回到上一个界面发现定时器仍然在进行,并且没有进入delloc方法,说明TestViewController控制器也没有销毁
为什么会这样呢,不是说好的弱引用吗,原因是这样的:牵扯到内存,self就是个指针,它指向viewcontroller,引用计数加1,为weakself也指向viewcontroller,它只是弱引用,计数不加1,但是_timer对weakself强引用了,因为为weakself也指向viewcontroller,所以_timer对viewcontroller也计数加1了,也就说强引用一个弱引用也是强引用,那很多人说为什么block只需要weakself就行了,那是因为如果用self,那么block就强引用self,如果用weak self那么就弱引用self,但是如上图黑色圈出的部分所说,不管你是强还是弱,跟我没关系,我都会强引用你

假设2:既然weakself不行,那我把_timer设置成__weak呢

@interface TestViewController ()

@property(nonatomic,weak)NSTimer * timer;

@end

-(void)startTimerTwo{
    
    _timer= [NSTimer timerWithTimeInterval:1.f target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSDefaultRunLoopMode];
    [_timer fire];
}

这样肯定不行嘛

345F674C-F24F-4294-84BD-4455579E1FEB.png

因为_timer在创建后就释放了为nil,你要添加一个已经释放的内存nil就是奔溃

假设3:换一种创建timer的方法,这先说明下结果,单纯了换种方式,只是不会奔溃,但是返回上一控制器的时候,定时器还是跑,那么干脆就说下循环引用的解决方式,不是self -> _timer 并且 _timer->self吗,好了,那我们在viewWillDisappear把定时器销毁不就好了嘛,这就是答案

@interface TestViewController ()

@property(nonatomic,weak)NSTimer * timer;

@end

@implementation TestViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    [self startTimerOne];
}

-(void)viewWillDisappear:(BOOL)animated{

    [super viewWillDisappear:animated];
    [_timer invalidate];
    _timer=nil;
}

-(void)dealloc{
    [_timer invalidate];
    NSLog(@"dealloc:%s",__func__);
}

- (IBAction)goBack:(UIButton *)sender {
    [self dismissViewControllerAnimated:YES completion:nil];
}

-(void)timerAction{
  
    
    NSLog(@"timerAction");
}


-(void)startTimerOne{
    
    //__weak typeof(self)weakSelf=self;
    _timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
}

-(void)startTimerTwo{
    
    _timer= [NSTimer timerWithTimeInterval:1.f target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSDefaultRunLoopMode];
    [_timer fire];
}


@end

好了,循环引用的问题是解决了,但是大家发现一个问题了,_timer是weak的,应该创建后就释放了啊,按理来说在viewWillDisappear使用_timer是不能够销毁定时器吧,但是这为什么又能用了,这是因为scheduledTimerWithTimeInterval 在创建定时器的时候顺便把定时器加在了当前的runloop 里,runloop对_timer进行了强引用,那么此时的引用关系:
1.runloop->_timer->self
2.self->timer;
所以关键一步就是_timer 要invalidate,如果把_timer给invalidate,那么就相当于把_timer从runloop 中移除了,这就把1的线给断了是不是,如果timer是weak型那么这时timer就为nil,如果timer是strong型的,随着控制器的释放它也就被释放,所以在这种创建定时器的方式无论weak和strong并不影响定时器的运行

好了,解决第一种方式为

-(void)viewWillDisappear:(BOOL)animated{

    [super viewWillDisappear:animated];
    [_timer invalidate];
    _timer=nil;
}

那么我可以考虑下, 在timer上动手脚好像是不行的,无论怎么变都行不通了对吧,换个角度不老说self的强引用吗,如果我们创建的时候不引入当前控制器的对象会怎么样呢
建一个NSTimer 的分类

@interface NSTimer (EYTimer)

+(instancetype)ey_timerWithTimeInterval:(NSInteger)ti block:(void(^)(void))block repeated:(BOOL)repeat;
@end

+(instancetype)ey_timerWithTimeInterval:(NSInteger)ti block:(void(^)(void))block repeated:(BOOL)repeat{

    NSTimer * timer = [NSTimer timerWithTimeInterval:ti target:self selector:@selector(handleTimerAction:) userInfo:block repeats:repeat];
    return timer;
}

+(void)handleTimerAction:(NSTimer*)timer{

    void(^block)()=timer.userInfo;
    if (block) {
        block();
    }
    
}

@end

调用

@interface TestViewController ()

@property(nonatomic,strong)NSTimer * timer;

@end

@implementation TestViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    [self ey_startTimer];
}

-(void)ey_startTimer{

    __weak typeof(self)weakSelf=self;
    _timer = [NSTimer ey_timerWithTimeInterval:1.0 block:^{
        [weakSelf timerAction];
        
    } repeated:YES];
    [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSDefaultRunLoopMode];
}


-(void)dealloc{
    NSLog(@"dealloc:%s",__func__);
}
//
- (IBAction)goBack:(UIButton *)sender {
    [self dismissViewControllerAnimated:YES completion:nil];
}

-(void)timerAction{
  
    
    NSLog(@"timerAction");
}

先简单介绍下上面代码的block:
block在arc的模式下,一般情况下,它自动copy的,就是从栈block copy到堆block上,作为property,用copy/strong都是一样的
作为自己函数的参数,block一般是不copy的,系统的一些哪怕是函数参数也会copy
可能苹果开发知道这个循环引用不好,然后在iOS 10以后是有这种API的

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))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));
  • 子线程开启定时器

- (void)viewDidLoad {
    [super viewDidLoad];
    [NSThread detachNewThreadSelector:@selector(threadTimer) toTarget:self withObject:nil];
    
}

-(void)threadTimer{
    
    NSLog(@"%@",[NSThread currentThread]);
    _timer= [NSTimer timerWithTimeInterval:1.f target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSDefaultRunLoopMode];
}

开启一个字线程,在字线程里开启定时器,发现没有打印log,说明了定时器并没有运行,因为什么呢,因为在字线程的里runloop需要手动开启,主线程系统帮你开启了

-(void)threadTimer{
    
    NSLog(@"%@",[NSThread currentThread]);
    _timer= [NSTimer timerWithTimeInterval:1.f target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];//开启runloop
NSLog(@"字线程没了");
}

开启runloop就行,这里也介绍下runloop的创建,并不能用alloc,其实[NSRunLoop currentRunLoop] 就先相当于有就获取没有就创建,类似于懒加载的意思
那么怎么取消字线程的定时器呢

-(void)timerInvalidate{

    [_timer invalidate];

    _timer =nil;
}

给一个点击方法调用timerInvalidate,测试结果就是在定时器运行的时候点击下,结果定时器没有相应了,那么字线程的定时器在主线程上取消到底行不行?答案应该是不行的,应该在字线程的runloopshang添加一个timer,当timer资源失效的时候,runloop也应该失效了,死循环也应该没了

NSLog(@"字线程没了");

这句代码应该打印出来才对,但是经过测试这句并没有打印
这说明了字线程的定时器就应该在字线程上干掉它,不要在主线程上invalidate,否则会造成runloop资源的浪费
所以开启字线程定时器的代码如下

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self listTableView];
    
    //[self startTimerOne];
    //[self startTimerTwo];
    
    [NSThread detachNewThreadSelector:@selector(threadTimer) toTarget:self withObject:nil];
    
    
}

-(void)threadTimer{
    
    NSLog(@"%@",[NSThread currentThread]);
    _timer= [NSTimer timerWithTimeInterval:1.f target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSDefaultRunLoopMode];
    //[[NSRunLoop currentRunLoop] run];
   // [[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];
    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    /*
     运行runloop的几种方式
     1.- (void)run; 
     无条件运行
     因为这个接口会导致runloop永久性的运行在NSDefaultRunLoopMode模式,没有超时机制
     即使使用CFRunLoopStop(runloopRef);也无法停止Run Loop的运行,那么这个子线程就无法停止,只能永久运行下去。
     
     2.- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;
     参数为运行模式、时间期限
     即使limitDate的初始值小于当前的Date,RunLoop也会执行一次然后马上返回YES
     
     3.- (void)runUntilDate:(NSDate *)limitDate;
     参数为时间期限,运行模式为默认的NSDefaultRunLoopMode模式
     无法使用CFRunLoopStop(runLoopRef)来停止RunLoop的运行
     
     总结:建议是使用第二个接口来运行,因为它能够设置Run Loop的运行参数最多,而且最重要的是可以使用CFRunLoopStop(runLoopRef)来停止Run Loop的运行,而第一个和第三个接口无法使用CFRunLoopStop(runLoopRef)来停止Run Loop的运行
     */
    NSLog(@"字线程没了");
}

-(void)timerAction{
    
    static int i=0;
    i++;
    if (i==5) {
        CFRunLoopStop([NSRunLoop currentRunLoop].getCFRunLoop);
    }
    NSLog(@"timerAction");
}

经过测试,@"字线程没了"被打印出来了

  • GCD定时器

GCD定时器跟应该是内置了runloop,

- (void)viewDidLoad {
    [super viewDidLoad];

    //[self startTimerOne];
    //[self ey_startTimer];
    [self gcdTimer:1 repeat:YES];
}

-(void)gcdTimer:(NSTimeInterval)timeInter repeat:(BOOL)repeat{

    dispatch_queue_t queue = dispatch_queue_create("edison", 0);
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, timeInter * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
    dispatch_source_set_event_handler(timer, ^{
        
        [self timerAction];
    });
    dispatch_resume(timer);
}

可是为什么定时器没起来呢,为什么呢,
修改下代码:dispatch_source_t timer设置成员变量
结果就可以运行定时器了,原因在于如果是局部变量执行一次后对象就释放了,没有被持有
再修改下代码,还是设置成局部变量,只是在dispatch_source_set_event_handler里写一次timer


-(void)gcdTimer:(NSTimeInterval)timeInter repeat:(BOOL)repeat{

    dispatch_queue_t queue = dispatch_queue_create("edison", 0);
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, timeInter * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
    dispatch_source_set_event_handler(timer, ^{
        
        timer;
        [self timerAction];
    });
    dispatch_resume(timer);
}

这又是为什么n:内存问题,
dispatch_source_set_event_handler没有包含timer:当前timer为成员变量的时候没有问题,当为局部变量的时候只运行了一次
dispatch_source_set_event_handler包含timer:为局部变量的时候,因为block会对timer引用计数加1,所以不会被释放掉

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容