iOS 定时器

Posted by LML on March 23, 2020

前言

本系列文章主要介绍 iOS 中几种常见的定时器的使用,主要包括:

  • GCD 定时器
  • NSTimer
  • CADisplayLink

GCD 定时器

循环调用 dispatch_source_t

创建、添加、开启方法

 (void)addDisPatch_source {
    //1.创建
    self.disTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
    //2.设置时间
    dispatch_source_set_timer(self.disTimer, dispatch_walltime(NULL,0 * NSEC_PER_SEC), 1 * NSEC_PER_SEC, 0);
    //3.执行
    dispatch_source_set_event_handler(self.disTimer, ^{
        NSLog(@"=========dispatch_source_t=========");
    });
    //开始
    dispatch_resume(self.disTimer);
    //暂停
    dispatch_suspend(self.disTimer);
    // 停止
    dispatch_source_cancel(self.disTimer);
}

注意的点

  • dispatch_source_create
    • 最后参数:The dispatch queue to which the event handler block is submitted. 只要不是main queue,都将在子线程执行
    • dispatch_source_t time 是一个对象,遵循ARC 规则,所以如果想要定时,不能设置为临时变量
  • 关于 dispatch_suspend()ispatch_resume的使用
    • dispatch_resumedispatch_suspend 应该成对使用,且刚开始定义的 默认是suspend状态
    • 因为重复调用会让dispatch_resume代码里if分支不成立,从而执行了DISPATCH_CLIENT_CRASH("Over-resume of an object")导致崩溃。
    • source在suspend状态下,如果直接设置source = nil或者重新创建source都会造成crash。正确的方式是在resume状态下调用dispatch_source_cancel(source)释放当前的source
    • dispatch_source_cancel 并没有使引用计数-1,source 在 resum 状态下可以直接 = nil或者重新创建
  • dispatch_source_cancel
    • Asynchronously cancels the dispatch source, preventing any further invocation of its event handler block.——不影响当前正在执行的Block
    • The optional cancellation handler is submitted to the target queue once the event handler block has been completed.
  • dispatch_source_set_event_handler使用
    • source(timer) 会强持有block
    • 这里有个循环引用需要注意
      • 因为dispatch_source_set_event_handler回调是个block,在添加到source的链表上时会执行copy并被source强引用,如果block里持有了self,self又持有了source的话,就会引起循环引用。
  • dispatch_source_set_timer 中第二个参数,当我们使用 dispatch_time 或者 DISPATCH_TIME_NOW 时,系统会使用默认时钟来进行计时。然而当系统休眠的时候,默认时钟是不走的,也就会导致计时器停止。使用 dispatch_walltime 可以让计时器按照真实时间间隔进行计时.
  • 第三个参数, 1.0 * NSEC_PER_SEC 为每秒执行一次,对应的还有毫秒,分秒,纳秒可以选择.
  • dispatch_source_set_event_handler 这个函数在执行完之后,block 会立马执行一遍,后面隔一定时间间隔再执行一次。而 NSTimer 第一次执行是到计时器触发之后。这也是和 NSTimer 之间的一个显著区别。

内部实现原理

https://xiaozhuanlan.com/topic/9481560732

单次调用

double delayInSeconds = 2.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
        [self someMethod];
    });

GCD 定时器特点

GCD定时器和NSTimer是不一样的,NSTimer受RunLoop影响,但是GCD的定时器不受影响,因为通过源码可知RunLoop也是基于GCD的实现的,所以GCD定时器有非常高的精度。 为什么说 GCD 定时器不受RunLoop的影响? 因为RunLoop也是基于 GCD实现的?——这个介绍RunLoop的时候在详细分析。

NSTimer

基本使用

创建

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable  
id)userInfo repeats:(BOOL)yesOrNo;

NSTimer *timer = [NSTimer timerWithTimeInterval:5 target:self selector:@selector(timerAction)  
 userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];  


+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector   
userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;

self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(checkPermission) userInfo:nil repeats:YES];

  • 上述两种使用的区别市:第一种需要我们手动加入到runloop,第二种已经自动加入到Runloop
    • reates a timer and schedules it on the current run loop in the default mode. ——加入的是当前runloop的default mode里
    • 要让timer生效,必须保证该线程的runloop已启动,而且其运行的runloopmode也要匹配
    • 每一个线程都有它自己的runloop,程序的主线程会自动的使runloop生效,但对于我们自己新建的线程,它的runloop是不会自己运行起来,当我们需要使用它的runloop时,就得自己启动
  • NSDefaultRunLoopMode UITrackingRunLoopMode NSRunLoopCommonModes 不同 mode
    • NSRunLoopCommonModes = NSDefaultRunLoopMode + UITrackingRunLoopMode
    • NSDefaultRunLoopMode 会收到UI滑动事件的影响
  • 存在延迟
    • 不管是一次性的还是周期性的timer的实际触发事件的时间,都会与所加入的RunLoop和RunLoop Mode有关,如果此RunLoop正在执行一个连续性的运算,timer就会被延时触发。重复性的timer遇到这种情况,如果延迟超过了一个周期,则会在延时结束后立刻执行,并按照之前指定的周期继续执行。
  • 强持有问题
    • timer强持有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.
    • RunLoop 强持有 Timer
      • You can add a timer to multiple input modes. While running in the designated mode, the receiver causes the timer to fire on or after its scheduled fire date. Upon firing, the timer invokes its associated handler routine, which is a selector on a designated object. The receiver retains aTimer. To remove a timer from all run loop modes on which it is installed, send an invalidate message to the timer. - (void)addTimer:(NSTimer *)timer forMode:(NSRunLoopMode)mode;
  • aSelector
    • The message to send to target when the timer fires.
  • userInfo ——timer 强持有userinfo
    • The user info for the timer. The timer maintains a strong reference to this object until it (the timer) is invalidated. This parameter may be nil.

启动、暂停与销毁

  • 启动
    ```
  • (void)fire;
    ```
    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.

  • 停止

      - (void)invalidate;  
    

    Stops the timer from ever firing again and requests its removal from its run loop 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. If it was configured with target and user info objects, the receiver removes its strong references to those objects as well.
    invalidate 完成:移除Runloop对timer的强持有和timer对target和userinfo的强持有

NStimer 单次使用

NSObject 中的 performSelector:withObject:afterDelay: 以及 performSelector:withObject:afterDelay:inModes: 这两个方法在调用的时候会设置当前 runloop 中 timer ,前者设置的 timer 在 NSDefaultRunLoopMode 运行,后者则可以指定 NSRunLoop 的 mode 来执行。我们上面介绍过 runloop 中 timer 在 UITrackingRunLoopMode 被挂起,就导致了代码就会一直等待 timer 的调度,解决办法在上面也有说明。

循环引用与持有问题

常见的问题

  • NStimer不是属性 且self是target
    • NStimer 会强引用self,Runloop强引用NStimer 然后如果不调用invalidate,在runloop结束前NStimer都不会释放,导致self不会释放,内存泄露
    • 如果传入target的为weakself,那么self会被释放,在self的dealloc中调用invalidate函数,可以释放NStimer,即使不释放NStimr,也仅仅导致了NStimrt没有释放,没有导致self泄漏
  • NStimer是属性 且self是target
    • Nstimer计数2(self+Runloop);self计数2(self+Nstimer),self自己-1,self 被NStimer强引用不释放,这个即使后面runloop释放,self也释放不了:因为runloop释放后,self1,Nstimer1,除非主动释放self,但是取不到self了.
    • 这种情况下,即使找到合适时机调用invalidate,也需要注意如果调用invalidate函数,必须在self-1之前,这样才会在流程走通:nstimer invalidate(释放NSrunloop对NStimer的引用,和NStimer对target的引用——Self-1——self在释放——Timer释放
    • 另外解决措施:
      • weakself传入NStimer的target,self(1) NStimer(2) self可执行dealloc函数,然后dealloc里面写invalidate函数,然后Nstimer-1(Runloop释放对NAtimer的强引用),然后 self dealloc Nstimer -1
  • NStimer 不是属性,且 target 是 block
    • 一般block传入的话,都会注意block是weak,所以这种情况下,self是可以 执行到dealloc函数的,只是需要注意对Nstimer的invalidate函数调用,实现释放NStimer
  • NStimer 不是属性,且 target 是 block

正确的使用方法

总结:其实根本原因是Nstimer 的invalidate函数没有在合适时机调用.下面举个例子,合适的时机

如果实在找不到合适时机,需要在传入target的时候weakself,避免self的dealloc函数不执行,dealloc能执行可以在dealloc里面写invalidate,保证NStimer的释放

CADisplayLink

A CADisplayLink object is a timer object that allows your application to synchronize its drawing to the refresh rate of the display

CADisplayLink 对象是一个和屏幕刷新率同步的定时器对象。每当屏幕显示内容刷新结束的时候,runloop 就会向 CADisplayLink指定的 target 发送一次指定的 selector 消息, CADisplayLink 类对应的 selector 就会被调用一次。
从原理上可以看出,CADisplayLink 适合做界面的不停重绘,比如视频播放的时候需要不停地获取下一帧用于界面渲染,或者做动画。

使用方法

//创建
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDisplayLink:)];  
// 每隔1帧调用一次
self.displayLink.frameInterval = 1;  
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
释放方法:
[self.displayLink invalidate];  
self.displayLink = nil;
  • frameInterval
    • NSInteger类型的值,用来设置间隔多少帧调用一次selector方法,默认值是1,即每帧都调用一次。
    • displayLink.frameInterval = 60; 每60帧调用一次
  • duration
    • CFTimeInterval值为readOnly,表示两次屏幕刷新之间的时间间隔。需要注意的是,该属性在target的selector被首次调用以后才会被赋值。selector的调用间隔时间计算方式是:调用间隔时间 = duration × frameInterval。

持有问题

CADisplayLink的持有问题与NStimer类似:

  • CADisplayLink 对象强持有target
    • The newly constructed display link retains the target.
  • RunLoop 强持有 CADisplayLink 对象
    • The run loop retains the display link. To remove the display link from all run loops, send an invalidate message to the display link.

原理

与runloop关系,后续介绍一下

几种定时器的对比

性能对比

  • NSTimer 使用简单方便,但是应用条件有限。
  • CADisplayLink 刷新频率与屏幕帧数相同,用于绘制动画。具体使用可看我封装好的一个 水波纹动画。
  • GCD定时器 精度高,可控性强,使用稍复杂

持有问题对比

  • GCD  定时器
    • 持有关系上不涉及到RunLoop
    • 仅存在 定时器被作为属性持有,Block 持有、GCD 定时器持有Block的问题
  • NSTimer 与 CADisplayLink
    • 二者 被RunLoop 持有
    • 二者持有target
    • 作为属性,可能被 Target 持有
    • Block 持有问题

待解决问题

  • CADisplayLink也是runloop执行,在屏幕刷新之后去调用,不存在当时runloop卡住了吗?屏幕调用与runloop关系。屏幕显示图像,是视频控制器以一定周期从帧缓冲区取数据→显示器。感觉与runloop无关?runloop会感知屏幕刷新吗?那如何控制CADisplayLink
  • GCD 定时器为啥不受Runloop影响?runloop基于定时器
  • runloop 如何处理定时器的?