它一个定时器 是用于同步屏幕刷新频率的计时器

CADisplayLink其实也是一个定时器,只不过这个定时器不用你来设置时间,它是要保证调用频率和屏幕的刷帧频率一致,通常来说大概是60FPS(一秒钟会调用60次),当然如果你主线程要是做了很多耗时操作的话也可能就不到60了

- (void)viewDidLoad {   
 [super viewDidLoad];
    // 保证调用频率和屏幕的刷帧频率一致,60FPS
    self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkTest)];
    [self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];

}

- (void)click {
  NSLog(@"1");
}

- (void)dealloc {
  [self.link invalidate];
}

它会造成循环引用 导致无法释放 从而导致dealloc不会执行

NSTimer

首先我们先来看一段代码

@interface SecondViewController ()

@property (nonatomic, strong) NSTimer *timer;

@end

@implementation SecondViewController

- (void)viewDidLoad {

    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];

	//使用方法1执行
    self.timer= [NSTimer timerWithTimeInterval:1 target:self selector:@selector(click) userInfo:nil repeats:YES];

    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

//  [[NSRunLoop currentRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate date]];

	//使用方法2执行
	NSTimer *timer1 = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector: @selector(click) userInfo:nil repeats:YES];

}

- (void)dealloc {
    [self.timer invalidate];
    self.timer = nil;
}

- (void)click {
    NSLog(@"123");
}

@end

不卖关子了 直接说结论 两者执行效果一致

我们再来看下官方的文档对**timerWithTimeInterval: target: selector: userInfo: repeats:**的解释

Initializes a timer object with the specified object and selector.

谷歌翻译:

使用指定的对象和选择器初始化计时器对象。

再看下

scheduledTimerWithTimeInterval: target: selector: userInfo: repeats:

Creates a timer and schedules it on the current run loop in the default mode.

谷歌翻译:

创建一个计时器并在默认模式下将其安排在当前运行循环上。

一个是创建一个定时器 一个是创建定时器并加入到runloop 所以 scheduledTimerWithTimeInterval 只是对**timerWithTimeInterval**的基本封装 因为**scheduledTimerWithTimeInterval**只是把time加入到runloop的默认模式 如果需要加入其他模式 还是需要自己处理下

再来看一下这段代码

- (void)dealloc {
   [self.timer invalidate];
   self.timer = nil;

}

很多时候我们发现 就算是我们放到dealloc里面 发现timer根本没有被销毁 会继续在后台执行

因为在初始化NSTimer的时候,传入的target(self)会被NSTimer强引用,并且控制器又强引用NSTimer,所以产生循环引用

- (void)viewDidDisappear:(BOOL)animated {
   [super viewWillDisappear:animated];
   [self.timer invalidate];
   self.timer = nil;
}

年少无知的我 为了搞定这个问题直接采用了一个暴力的方法 在**viewWillDisappear**里面操作了一波 效果是达到了 但是总感觉不那么优雅 而且而且 这个处理方式有一个弊端 就是如果有多级push跳转 就会导致timer被提前释放 无法再使用

于是有了下面的操作

1. 使用block方式 该方案是将计时器所应执行的任务封装成"Block",在调用计时器函数时,把block作为userInfo参数传进去

2. 给self添加中间件proxy

这两种方式都可以解决 也是目前网上比较多的解决方案 具体的实现代码我就不贴了 网上很多 搜索关键词

NSTimer 循环引用和解决方案

其实通过苹果的API也可以解决这个问题

- (void)viewDidLoad {
    [super viewDidLoad];
    __weak typeof(self) weakself = self;
    self.timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        [weakself click];
    }];

    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

}

- (void)dealloc {
   [self.timer invalidate];
   self.timer = nil;
}

API

+ (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));

官方api注释文档

Creates and returns a new NSTimer object initialized with the specified block object. This timer needs to be scheduled on a run loop (via -[NSRunLoop addTimer:]) before it will fire.

- parameter:  timeInterval  The number of seconds between firings of the timer. If seconds is less than or equal to 0.0, this method chooses the nonnegative value of 0.1 milliseconds instead

- parameter:  repeats  If YES, the timer will repeatedly reschedule itself until invalidated. If NO, the timer will be invalidated after it fires.

- parameter:  block  The execution body of the timer; the timer itself is passed as the parameter to this block when executed to aid in avoiding cyclical references

谷歌翻译

创建并返回一个用指定块对象初始化的新 NSTimer 对象。 这个计时器需要在运行循环(通过 -[NSRunLoop addTimer:])之前被调度,然后才会触发。

- 参数:timeInterval 定时器触发之间的秒数。 如果秒小于或等于 0.0,则此方法选择非负值 0.1 毫秒

- 参数:repeats 如果是,定时器将重复重新调度自己直到失效。 如果否,定时器将在触发后失效。

- 参数:block 定时器的执行体; 计时器本身在执行时作为参数传递给此块以帮助避免循环引用

参数block就是为了解决这个循环引用的问题 当然也要稍稍注意强引用问题 还有就是此方法是从ios10开始兼容

NSTimer的时间其实是不准的  因为有runloop的存在 很可能runloop循环几圈(runloop每循环一圈的时间都是不固定的) timer才走一秒 或者runloop循环一次需要的时间远大于timer执行一次的时间 也有可能导致错过某个需要回调的点 从而导致runloop不会很精准的去触发timer 持续时间长 就会有较大的影响

其实NSTimer自身是有一个属性可以允许有一定的时间误差存在 感兴趣的可以自己去搜索一下

GCD

GCD的定时器相比前面两个就要精确很多 因为他的执行是直接跟CPU内核挂钩 而且也不依赖runloop 可控性比较强

首先我们来创建一个GCD定时器

- (void)viewDidLoad {

    [super viewDidLoad];

    //创建队列
    dispatch_queue_t quene = dispatch_get_main_queue();

    //创建定时器
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, quene);

    dispatch_source_set_timer(timer,DISPATCH_TIME_NOW, 1  NSEC_PER_SEC, 0  NSEC_PER_SEC);

    dispatch_source_set_event_handler(timer, ^{
        NSLog(@"123");
    });
    dispatch_resume(timer);
}

当你满心欢喜的创建出定时器 发现他压根都不会执行打印 也就是handler回调没有执行

所以我们来改进下方法

- (void)viewDidLoad {

    [super viewDidLoad];

    //创建队列
    dispatch_queue_t quene = dispatch_get_main_queue();

    //创建定时器
    self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, quene);

    dispatch_source_set_timer(self.timer, dispatch_time(DISPATCH_TIME_NOW, 1  NSEC_PER_SEC), 1  NSEC_PER_SEC, 0);

    dispatch_source_set_event_handler(self.timer, ^{
        NSLog(@"123");
    });
    dispatch_resume(self.timer);
}

因为viewDidLoad在执行完成后timer就被释放掉了 导致不会走回调里面的方法 所以必须用self强引用保住timer不被释放 这个时候再次运行定时器就正常的执行打印了 同时当前界面被销毁时dealloc正常的执行

- (void)viewDidLoad {

    [super viewDidLoad];

    //创建队列
    dispatch_queue_t quene = dispatch_get_main_queue();

    //创建定时器
    self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, quene);

    dispatch_source_set_timer(self.timer, dispatch_time(DISPATCH_TIME_NOW, 1  NSEC_PER_SEC), 1  NSEC_PER_SEC, 0);

    dispatch_source_set_event_handler(self.timer, ^{
        NSLog(@"123");
        self.string = @"123";
    });

    dispatch_resume(self.timer);

}

如果在dispatch_source_set_event_handler 回调的block里面用self强引用 就会出现循环引用的问题 所以需要注意**__weak**的使用问题

dispatch_suspend 和 dispatch_resume

dispatch_suspend 是将定时器暂停

dispatch_resume 是恢复定时器

在方法

dispatch_suspend(dispatch_object_t object) 里面有一句说明

 Calls to dispatch_suspend() must be balanced with calls* to dispatch_resume().

谷歌翻译

对 dispatch_suspend() 的调用必须与调用平衡到 dispatch_resume()。

suspend 暂停 resume 恢复

你调用了suspend几次, 你想resume的话,就必须要remuse几次,才能继续运行。

但remuse的状态下,如果再进行一次resume就会crash,所以要注册一个BOOL值的状态进行记录,防止多次suspend和resume引起闪退。并且在suspend的状态下,如果你设置_timer = nil也会crash