iOS开发之进阶篇(10)—— Timer

概述

本文将要讨论以下三种timer:

  1. NSTimer (Timer)
  2. CADisplayLink
  3. GCD

原理

计时好比数数, 在iOS中, 数数的人是系统内核.

内核会根据一些设定好的条件 (比如按时) 产生相应事件, 然后通过回调函数向外抛出 (可理解为"报时"). 我们通过注册观察者来监听取得这些回调, 从而达到计时的目的.

这些与时间相关的事件的载体叫做事件源 (Source), iOS中有两种 Source: Run Loop SourceDispatch Source.

  • Run Loop Source会唤醒当前Run Loop, 然后执行回调函数.
    关于Run Loop Source, 可参考之前的博文iOS开发之进阶篇(8)—— Run Loops.

  • Dispatch Source也会产生一些特定的事件, 事件通过 block 自动加入到对应的 dispatch queue 中.
    关于Dispatch Source, 可参考苹果文档.

也就是说, iOS中有两种计时方式, 分别对应Run LoopDispatch (GCD)中的两种计时器源.

  • NSTimer (Timer) 和 CADisplayLink 都属于 Run Loop Source.
  • GCD 属于Dispatch Source.

以上是口语解说, 如果你在等待书面文解释, 抱歉, 木有.

精确度

  1. NSTimer (Timer)

Run Loop中的计时器源会受到模式的影响, 如果模式不对则不会触发. 比如说添加到主线程中的NSTimer, 当滚动 Scroll View 的时候, 模式发生改变, NSTimer暂时失效.

而重复计时器会受到当前事务的影响, 可能在一定范围内产生偏差, 偏差大小则和计时器的tolerance属性有关.

如果触发时间延迟得太久, 以致错过了一个或多个计划的触发时间, 则 timer 将在错过的时间段内仅触发一次.

  1. CADisplayLink

CADisplaylink 也是由Run Loop中的计时器源触发, 它与NSTimer相似, 都可以以一定的时间间隔触发回调 selector. 不同的是, CADisplaylink 的时间间隔是与屏幕的刷新频率相关联的.

CADisplayLink 的时间间隔由preferredFramesPerSecond属性来决定的, 意为每秒刷新的帧数. 如果设备的最大刷新率是每秒60帧, 则实际帧速率包括每秒15、20、30和60帧.

如果你设置为一个特定的值, 他么这个值最终也会和15、20、30以及60之间做最合适匹配. 比如设置为26或35, 则实际帧速率为30.

iOS设备的屏幕刷新频率是固定的, 因此CADisplayLink在正常情况下会在每次刷新结束都被调用, 精确度相当高. 但不要在CADisplayLink计时器中做耗时操作, 因为可能会被系统忽略掉.

  1. GCD

由于GCD拥有强大的资源配置能力, 因此其计时器精确度是相当可观的. 而且GCD计时器不需添加到run loop中, 因此在子线程中也可直接使用.

使用GCD定时并不是说在指定时间后马上执行任务, 而是在指定时间后将任务添加到队列. 任务的执行则取决于当前的队列状态(串并行, 是否挂起等). 比如下面例子, 定时看似应该在2秒后执行, 结果却是5秒后:

    NSLog(@"start");
    
    dispatch_queue_t queue = dispatch_get_main_queue();
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0*NSEC_PER_SEC)), queue, ^{
        NSLog(@"time is up");
    });
    
    NSLog(@"end");
    
    sleep(5);

// ------------------------------------------------------
log:
2020-08-11 14:43:41.959636+0800 KKTimerDemo[2119:91835] start
2020-08-11 14:43:41.959804+0800 KKTimerDemo[2119:91835] end
2020-08-11 14:43:46.978124+0800 KKTimerDemo[2119:91835] time is up

1. NSTimer (Timer)

OC:

#import "ViewController.h"


@interface ViewController ()

@property (nonatomic, strong)   NSTimer *mainTimer;
@property (nonatomic, strong)   NSTimer *globalTimer;

@end


@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 在主线程中创建NSTimer
    [self createTimerInMain];
    
    // 在子线程中创建NSTimer
    [self createTimerInGlobal];
}

- (void)dealloc
{
    // 销毁定时器
    [self cancelMainTimer];
    [self cancelGlobalTimer];
}

#pragma mark - 创建

// 在主线程中创建NSTimer
- (void)createTimerInMain {
    
    self.mainTimer = [NSTimer scheduledTimerWithTimeInterval:5.0 target:self selector:@selector(mainTimeIsUp:) userInfo:nil repeats:YES];
//    [self.mainTimer fire];  // 立即执行
}

// 在子线程中创建NSTimer
- (void)createTimerInGlobal {
   
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        // 法一
        self.globalTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(globalTimeIsUp:) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] run];
        
        // 法二
//        self.globalTimer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(globalTimeIsUp:) userInfo:nil repeats:YES];
//        [[NSRunLoop currentRunLoop] addTimer:self.globalTimer forMode:NSRunLoopCommonModes];    // Common Mode 不受 Scroll View 滚动影响
//        [[NSRunLoop currentRunLoop] run];
    });
}

#pragma mark - 销毁

// 取消 mainTimer
- (void)cancelMainTimer {
    
    if (_mainTimer) {
        [self.mainTimer invalidate];
        self.mainTimer = nil;
    }
}

// 取消 globalTimer
- (void)cancelGlobalTimer {
    
    if (_globalTimer) {
        [self.globalTimer invalidate];
        self.globalTimer = nil;
    }
}

#pragma mark - 定时时间到

// mainTimer 定时时间到
- (void)mainTimeIsUp:(NSTimer *)timer {
    
    NSLog(@"mainTimeIsUp");
}

// globalTimer 定时时间到
- (void)globalTimeIsUp:(NSTimer *)timer {
    
    NSLog(@"globalTimeIsUp");
}


@end

Swift:

import Foundation

class SwiftTimer: NSObject {
    
    var mainTimer: Timer?
    var globalTimer: Timer?

    
    // MARK: - 创建
    
    // 在主线程中创建NSTimer
    @objc func createMainTimer() {
        
        mainTimer = Timer.scheduledTimer(timeInterval: 5.0, target: self, selector: #selector(mainTimeIsUp(timer:)), userInfo: nil, repeats: true)
//        mainTimer?.fire()   // 立即执行
    }
    
    // 在子线程中创建NSTimer
    @objc func createGlobalTimer() {
        
        DispatchQueue.global().async {
            // 法一
            self.globalTimer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(self.globalTimeIsUp(timer:)), userInfo: nil, repeats: true)
            RunLoop.current.run();
            // 法二
//            self.globalTimer = Timer.init(timeInterval: 1.0, target: self, selector: #selector(self.globalTimeIsUp(timer:)), userInfo: nil, repeats: true)
//            RunLoop.current.add(self.globalTimer!, forMode: .common)
//            RunLoop.current.run();
        }
    }
    
    
    // MARK: - 销毁
    
    @objc func cancelMainTimer() {
        self.mainTimer?.invalidate()
        self.mainTimer = nil
    }
    
    @objc func cancelGlobalTimer() {
        self.globalTimer?.invalidate()
        self.globalTimer = nil
    }
    
    
    // MARK: - 定时时间到
    
    @objc func mainTimeIsUp(timer: Timer) {
        print("mainTimeIsUp")
    }
    
    @objc func globalTimeIsUp(timer: Timer) {
        print("globalTimeIsUp")
    }
}

2. CADisplayLink

OC:

#pragma mark - CADisplayLink

- (void)createTimerUseCADisplayLink {
    
    self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLick)];
    self.link.preferredFramesPerSecond = 60;
    [self.link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}


- (void)cancelTimer {
    
    if (_link) {
        [self.link invalidate];
        self.link = nil;
    }
}


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

Swift:

import QuartzCore

    // MARK - CADisplayLink
    
    @objc func createTimerUseCADisplayLink() {
        
        self.link = CADisplayLink.init(target: self, selector: #selector(displayLick))
        self.link?.preferredFramesPerSecond = 60
        self.link?.add(to: RunLoop.current, forMode: .default)
    }
    
    @objc func calcelTimer() {
        self.link?.invalidate()
        self.link = nil
    }

    @objc func displayLick() {
        print("displayLick")
    }

3. GCD

OC:

@property (nonatomic, strong)   dispatch_source_t  gcdTimer;

- (void)createGCDTimer {
    
    // 定时一次
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"dispatch_after");
    });
    
    // 循环
    self.gcdTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
    // 1:计时器源  2:开始时间  3:间隔  4:误差
    dispatch_source_set_timer(self.gcdTimer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
    dispatch_source_set_event_handler(self.gcdTimer, ^{
        // do something
        NSLog(@"dispatch_event_handler");
    });
    dispatch_resume(self.gcdTimer);     // 开启定时器, 立即执行 dispatch_source_set_event_handler 里任务
}


- (void)cancelGCDTimer {
    
    if(_gcdTimer){
        dispatch_source_cancel(self.gcdTimer);
        self.gcdTimer = nil;
    }
}

注: 如需取消dispatch_after, 使用dispatch_block_cancel

Swift:

    // MARK - GCD
    
    static var gcdTimer: DispatchSourceTimer?

    @objc func createGCDTimer() {
        
        // 执行一次
        DispatchQueue.main.asyncAfter(deadline: .now()+1.0) {
            print("asyncAfter")
        }
        
        // 循环
        SwiftTimer.gcdTimer = DispatchSource.makeTimerSource(flags: [], queue: DispatchQueue.global())
//        SwiftTimer.gcdTimer = DispatchSource.makeTimerSource()     // 默认主线程
        SwiftTimer.gcdTimer?.schedule(deadline: .now(), repeating: .seconds(1), leeway: .nanoseconds(0))
        SwiftTimer.gcdTimer?.setEventHandler {
            print("dispatch_event_handler")
        }
        SwiftTimer.gcdTimer?.resume()     // 开始, 立即执行
    }
    
    @objc func cancelGCDTimer() {
        SwiftTimer.gcdTimer?.cancel()
        SwiftTimer.gcdTimer = nil
    }

注意:
因为本例是用OC调用Swift, 所以gcdTimer用static修饰, 使其执行过程中不会被释放. 如果在Swift工程中使用, 使其定义为全局变量即可, 不需static修饰.

你可能感兴趣的:(iOS开发之进阶篇(10)—— Timer)