IOS之定时器

在开发中我们经常用到定时器,IOS为我们提供了很多种定时器,NSTimerCADisplayLinkGCDNSThread(performSelector:afterDelay),其本质都是通过RunLoop来实现的。这里记录一下学习定时的记录。

NSTimer

  • IOS中最基本的定时,在swift中成为Timer.其通过RunLoop来实现,一般情况下比较准确,但是当前循环耗时操作较多时,会出现延迟问题。同时也受加入的RunLoopModel影响。(如果直接创建使用,当界面处于滑动状态时,定时器会停止执行,等滑动结束在开始计时执行。)

1.NSTimer创建

  • 使用+ (NSTimer *)timerWithTimeInterval: target: selector: userInfo: repeats:;方法来创建此种方法创建的定时器,需要手动添加到RunLoop才能启动.
self.timer = [NSTimer timerWithTimeInterval:3 target:self selector:@selector(timerfunc:) userInfo:nil repeats:YES];
    
    //把定时器添加到当前的RunLoop中,如果不添加到RunLoop中 定时器不会启动
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
    
    //该方法是让定时器,立刻启动,然后在根据时间间隔来执行,如果调用此方法,那么定时器创建之后并不会立刻启动,会延迟间隔时间后,在执行
    [self.timer fire];
  • 使用NSInvocation的方式来创建定时器,创建方式+ (NSTimer *)timerWithTimeInterval:invocation: repeats:
- (void)creatTiemrToInvocation {
    //1.首先获取方法签名
    SEL selector = @selector(invocationTest:num:);
    NSMethodSignature *signature = [[self class] instanceMethodSignatureForSelector:selector];
    
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
    invocation.target = self;
    invocation.selector = selector;
    
    //设置invocation的参数 设置参数要冲第二个参数设置起  前面有两个参数是  self, _cmd
    NSString *str = @"guoweiyong";
    [invocation setArgument:&str atIndex:2];
    NSNumber *number = [NSNumber numberWithInt:20];
    [invocation setArgument:&number atIndex:3];
    
    //这里也可以设置返回值,但是我这里没有返回值就不设置了
    //    - (void)getReturnValue:(void *)retLoc;
    //    - (void)setReturnValue:(void *)retLoc;
    
    //执行方法 定时器不需要调用这个方法
    //[invocation invoke];
    
    self.timer = [NSTimer timerWithTimeInterval:1 invocation:invocation repeats:true];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
    //[self.timer fire];
}
- (void)invocationTest:(NSString *)str num:(NSNumber *)number {
    NSLog(@"invocation 执行Timer test str=====%@  number====%@",str,number);
}
  • 通过+ (NSTimer *)timerWithTimeInterval: target:selector: userInfo:repeats:;方法来创建的定时器,是不需要手动启动的,已经默认添加到RunLoop中,所以不需要手动启动。
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerfunc:) userInfo:nil repeats:YES];
  • Invocation的创建方式和上面一样
  • IOS10.0以后有一种新的创建方式: 代码块(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));

2.NSTime的问题

2.1坑一: 延时问题:

  • 当前RunLoop过于繁忙
  • RunLoop模式与定时器所在的模式不同(schedule方式启动的timer是add到 RunLoopNSDefaultRunLoopMode这就会出现其他mode时tiemr得不到调度的问题。最常见的问题就是在UITrackingRunLoopModeUIScorllView滑动过程中定时器失效。–> 解决方式:把Timer添加到NSRunLoopCommonModes模式下,UITrackingRunLoopModekCFRunLoopDefaultMode都被标记为了common模式,所以在这个模式下面Timer都可以运行)
  • 不管是一次性的还是周期性的Timer的实际触发的时间,都会与所加入的RunLoop的RunLoopMode模式有关,如果此RunLoop正在执行一个连续性的运算,timer就会被延时出发。重复性的timer遇到这种情况,如果延迟超过了一个周期,则会在延时结束后立刻执行,并按照之前指定的周期继续执行。

2.2 子线程启动定时器问题:

我们都知道IOS是通过RunLoop作为消息循环机制,主线程默认启动RunLoop,可是子线程没有默认的RunLoop,因此,我们在子线程启动定时器是不生效的。
解决方式也简单,在子线程启动一下RunLoop就可以了

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSTimer* timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timerfunc:) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
        [[NSRunLoop currentRunLoop] run];
    });

2.3坑三: 循环引用问题:

循环引用问题,是每个使用者都会遇到的问题,究其原因就是NSTimertarget被强引用了,而通常target就是所在的控制器,他有强引用的timer,造成了循环引用。

在这里首先声明下,不是所有的NSTimer都会造成循环引用。就想不是所有的block都会造成循环引用一样。以下两种timer不会造成循环引用。

  • repeats类型的,非repeats类型的定时器,不会强引用target,因此不会出现循环引用。
  • blcok类型的 ,新API,IOS10.0支持,因此对于还要支持老版本的app来说,这API暂时无法使用,当然block内部的循环引用也要避免。
  • 二次声明:不是解决了循环引用,target就可以释放了,别忘了在持有timer的类的dealloc的时候执行invalidate()方法

解决循环引用,无非就是打破Timer对self的强引用。
方法参考:参考IOS之循环引用NSTimer

GCD定时器

GCD中的Dispatch Source 中的一种类型是DISPATCH_SOURCE_TYPE_TIMER定时器类型,可以实现定时器的功能,需要注意点是这种定时器并不是把timer添加到RunLoop中,所以需要添加属性,否则不会执行定时器。

- (void)GCDTiemr {
    //最后一个参数是队列,在那个队列中执行
    self.gcdTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
    dispatch_source_set_timer(self.gcdTimer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
    dispatch_source_set_event_handler(self.gcdTimer, ^{
        NSLog(@"GCD定时器执行");
    });
    //定时器的启动方法
    dispatch_resume(self.gcdTimer);
}

在这里插入图片描述

  • GCD定时器在子线程中执行:
- (void)gcdTiemrAsync {
    self.gcdTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0));
    dispatch_source_set_timer(self.gcdTimer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
    dispatch_source_set_event_handler(self.gcdTimer, ^{
        NSLog(@"GCD定时器在子线程中执行====%@",[NSThread currentThread]);
    });
    dispatch_resume(self.gcdTimer);
}

在这里插入图片描述

GCD定时器的好处就是,它并不是假日RunLoop中执行的,因此子线程也可以使用,并且不会硬气循环引用的问题。测试发现,当控制器(VC)销毁时,GCD定时器自动停止执行,销毁。

延时操作

dispatch_after

dispatch_after 延时操作,不会对控制器强引用, dispatch_after的操作和控制器的消亡没有关系,及时控制器销毁,但是dispatch_after在延迟时间到达后还是会执行操作。如果dispatch_after的block代码块中,对当前控制器产生了强引用,当销毁控制器时,控制器不会立刻销毁,等到dispatch_after中的block代码执行完毕之后,在销毁。

- (void)afterExecture {
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        //对当前控制器强引用
        NSLog(@"延迟10秒执行操作===%@",self.mark);
    });
}

在这里插入图片描述

没有产生引用的执行代码和结果:

- (void)afterExecture {
    __weak typeof(self) weakSelf = self;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        //对当前控制器强引用
        NSLog(@"延迟10秒执行操作===%@",weakSelf.mark);
    });
}

在这里插入图片描述

performSelector: afterDelay:

此方法也可以执行延时操作:

- (void)afterExecture2 {
    __weak typeof(self) weakSelf = self;
    //使用perform 的方式来执行 延时操作
    [self performSelector:@selector(afterExectureTest) withObject:nil afterDelay:5];
}

在这里插入图片描述
这种调用方式的好处是可以取消:

+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget selector:(SEL)aSelector object:(nullable id)anArgument;
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget;

当前VC中有使用此方法执行操作的haul,如果要销毁,必须等待此方法执行完毕才能销毁。这个方法其内部也是基于定时器添加到RunLoop中实现的,因此也存在此方法在子线程中无法执行的问题。

- (void)ansycAfterExecture {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [self performSelector:@selector(afterExectureTest) withObject:nil afterDelay:5];
    });
}
- (void)afterExectureTest {
    NSLog(@"延迟5秒执行操作=========%@",[NSThread currentThread]);
}

在这里插入图片描述
虽然无法执行这个操作,但是控制器的依然还是 等到延时时间到了之后,才销毁
在这里插入图片描述

  • 延时一次的操作选择:
    几种方式都是定时器,都可以实现延时操作。综合相比:如果只是单独一次的延时操作,NSTimer和GCD的定时器都显得有些笨重。performSelector方式比较合适,但是又收到了子线程runloop的限制。因此,dispatch_after是最优的选择。

  • 延时的取消操作:
    以下几种方式都可以实现取消

  • NSTimer可以通过invalidate来停止定时器。

  • GCD的定时器可以调用dispatch_suspend来挂起。

  • performSelector:after可以通过cancelPreviousPerformRequestsWithTarget取消。

  • dispatch_after可以通过dispatch_block_cancel来取消。

- (void)cancelAfterExecture {

    self.delayBlcok = dispatch_block_create(0, ^{
        NSLog(@"测试dispatch_after的取消操作");
    });
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), self.delayBlcok);
    
    //去下dispatch_after的操作
    dispatch_block_cancel(self.delayBlcok);
}

CADisplayLink

CADisplayLink是基于屏幕刷新的周期,所以一般很准时,Meizu秒刷新60次,其本质也是通过RunLoop,所以不难看出,当RunLoop选择其他模式或被耗时操作过多时,仍旧会造成延迟。

  • 其使用步骤是 创建-> 添加到RunLoop ->终止 ->销毁
//创建CADisplayLink定时器
    self.linkTiemr = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkTest)];
    //添加到RunLoop中
    [self.linkTiemr addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
    //终止定时器
    [self.linkTiemr invalidate];
    //销毁对象
    self.linkTiemr = nil;

由于并非NSTimer的子类,所以使用RunLoop的添加Timer无法加入,应使用其自己的添加到RunLoop的方法,来把定时器添加到RunLoop中。
同时由于基于屏幕刷新,所以度量单位是每帧,该定时器提供了根据屏幕刷新设置时间间隔属性frameInterval(该属性支持IOS 10.0以下),IOS 10.0以上请使用preferredFramesPerSecond属性。其决定屏幕刷新多少帧调用一次方法,默认为1,即1/60秒调用一次。

如果我们想要计算每次调用的时间间隔,可以通过@property(readonly, nonatomic) CFTimeInterval duration;属性求出,后者为每帧间隔的只读属性。

在日常开发中,适当使用CADisplayLink甚至有优化作用。比如对于需要动态计算进度的进度条,由于起进度反馈主要是为了UI更新,那么当计算进度的频率超过帧数时,就造成了很多无谓的计算。如果将计算进度的方法绑定到CADisplayLink上来调用,则只在每次屏幕刷新时计算进度,优化了性能。

代码地址:demo

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值