# iOS 开发-定时器的使用

导语: 我们常常会使用定时器去做倒计时等功能,那么 iOS 的定时器到底有几种以及如何使用,本文将会简单讲解。

iOS 的定时器常用的有:

  • NSTimer: 普通的定时器
  • CADisplayLink: CADisplayLink 是一个能让我们以和屏幕刷新率相同的频率将内容画到屏幕上的定时器
  • GCDTimer: 不受runloop影响的定时器,定时器更加准确

# 一、 NSTimer 的使用

NSTimer 常用的创建定时器的方法如下:

iOS 10 及以上的版本最好使用如下方法: (优先使用)


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

参数说明

  • interval: 定时器时间间隔
  • repeats: 使用重复执行
  • ^block: 具体需要值的代码

iOS10 以下其余的情况请使用:


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

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

# 1. 两种类型方法的区别

两种类型方法的区别:

  • timerWithTimeInterval的定时器不会立即执行,需要手动添加到 runloop 中执行
  • scheduledTimerWithTimeInterval类型的定时器会立即执行

# 2. timerWithTimeInterval 的使用

在使用 timerWithTimeInterval 创建定时器的时候必须手动添加到 runloop 中,因为不会自动执行:

NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"hello mrgaogang");
}];
// 添加定时器到runloop中,用默认的Mode  NSDefaultRunLoopMode在页面滚动的时候定时器会暂停
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

注意点:

  • timerWithTimeInterval 使用 默认模式 NSDefaultRunLoopMode 的定时器在页面/容器滚动的时候,定时器会暂停;滚动完成会继续;
  • 如果希望定时器不被打扰/滚动的时候定时器继续执行,记得模式不能使用 NSDefaultRunLoopMode 而应该使用 NSRunLoopCommonModes;

# 3. scheduledTimerWithTimeInterval 的使用

一个样例:定时器每隔一秒就打印一句话(此处以 iOS10 的情况举例子)

// 立即执行
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
    NSLog(@"hello mrgaogang");
}];

注意点:

  • scheduledTimerWithTimeInterval其实是已经封装号了在 NSRunLoop 中,且使用的是默认的 mode NSDefaultRunLoopMode
  • 使用 scheduledTimerWithTimeInterval 的定时器在页面/容器滚动的时候,定时器会暂停;滚动完成会继续

# 4. 倒计时的销毁的暂停

  • 定时器的销毁和关闭
 [timer invalidate];
 timer = nil;
  • 定时器的暂停和开始

// 开始定时器
if (timer.isValid) {
    timer.fireDate = [NSDate date];
}

// 暂停定时器
if (timer.isValid) {
    timer.fireDate = [NSDate distantFuture];
}


一个例子:倒计时跳转页面


-(void)intervalTimer{

    self.timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        self.count = self.count - 1;

        [self.btnTime setTitle:[NSString stringWithFormat:@"%ld", (long)self.count] forState:UIControlStateNormal];

        if (self.count == 0) {
            // 销毁定时器并将定时器置空
            [self.timer invalidate];

            self.timer = nil;
            // 页面跳转
            [self performSegueWithIdentifier:@"abc" sender:nil];

        }

    }];

    //添加到RunLoop中执行
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

}

- (IBAction)btnTimeClick:(id)sender {
    // 销毁定时器并将定时器置空
    [self.timer invalidate];

    self.timer = nil;
    // 页面跳转
    [self performSegueWithIdentifier:@"abc" sender:nil];
}

优势:依托于设备屏幕刷新频率触发事件,所以其触发时间上是最准确的。也是最适合做 UI 不断刷新的事件,过渡相对流畅,无卡顿感;比较适合做游戏和动画。

缺点:

  • 不能做页面更新;想想 1s 更新 60 次的文本,我们
  • 由于依托于屏幕刷新频率,若果 CPU 不堪重负而影响了屏幕刷新,那么我们的触发事件也会受到相应影响。
  • selector 触发的时间间隔只能是 duration 的整倍数。
  • selector 事件如果大于其触发间隔就会造成掉帧现象。
  • CADisplayLink 不能被继承

初始化的方法

// 唯一的初始化方法
+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;

其他方法

- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode;
//将创建好点实例添加到RunLoop中

- (void)removeFromRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode;
//从RunLoop中移除

- (void)invalidate;
//销毁实例

@property(readonly, nonatomic) CFTimeInterval timestamp;   //上一次Selector被调用到时间, 只读
@property(readonly, nonatomic) CFTimeInterval duration;   //屏幕刷新时间间隔, 目前iOS刷新频率是60HZ, 所以刷新时间间隔是16.7ms

//下一次被调用到时间
@property(readonly, nonatomic) CFTimeInterval targetTimestamp CA_AVAILABLE_IOS_STARTING(10.0, 10.0, 3.0);

@property(getter=isPaused, nonatomic) BOOL paused;  //设置为YES的时候会暂停事件的触发

//事件触发间隔。是指两次selector触发之间间隔几次屏幕刷新,默认值为1,也就是说屏幕每刷新一次,执行一次selector,这个也可以间接用来控制动画速度
@property(nonatomic) NSInteger preferredFramesPerSecond
    API_AVAILABLE(ios(10.0), watchos(3.0), tvos(10.0));

//每秒现实多少帧
@property(nonatomic) NSInteger preferredFramesPerSecond CA_AVAILABLE_IOS_STARTING(10.0, 10.0, 3.0);

一个样例:

- (void)viewDidLoad {

    [super viewDidLoad];

    self.displayLink = [CADisplayLink displayLinkWithTarget:self
                                                   selector:@selector(logCount)];

    self.displayLink.frameInterval  = 2;    //屏幕刷新2次调用一次Selector
    // 添加定时器到RunLoop中,注意一下Mode,在前面的NSTimer已经讲过
    [self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];

}


- (void)logCount {

    self.count ++;
    NSLog(@"Count = %ld", self.count);
    if (self.count > 99) {
        self.count = 0;
        [self.displayLink invalidate]; //直接销毁
        self.displayLink = nil;
    }
}

注意点:

  • RunLoop 的 mode 最好使用NSRunLoopCommonModes而不是NSDefaultRunLoopMode,因为NSDefaultRunLoopMode在列表/页面滑动的时候定时器会暂停

# 三、 GCT定时器

#import "ViewController.h"

@interface ViewController ()

@property (nonatomic, assign)   NSInteger count;
@property (nonatomic, strong)   dispatch_source_t tTimer;  //GCD计时器一定要设置为成员变量, 否则会立即释放

@end

@implementation ViewController

@synthesize tTimer;

- (void)viewDidLoad {
    
    [super viewDidLoad];
    
    //创建GCD timer资源, 第一个参数为源类型, 第二个参数是资源要加入的队列
    self.tTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
    
    //设置timer信息, 第一个参数是我们的timer对象, 第二个是timer首次触发延迟时间, 第三个参数是触发时间间隔 n* 秒, 最后一个是是timer触发允许的延迟值, 建议值是十分之一
    dispatch_source_set_timer(self.tTimer,
                              dispatch_walltime(NULL, 0 * NSEC_PER_SEC),
                              1 * NSEC_PER_SEC,
                              0);
    
    //设置timer的触发事件
    dispatch_source_set_event_handler(self.tTimer, ^{
        
        [self logCount];
    });
    
    //激活timer对象
    dispatch_resume(self.tTimer);
}


- (void)logCount {
    
    self.count ++;
    NSLog(@"Count = %ld", self.count);
    
    if (self.count > 99) {
        
        self.count = 0;
        //暂停timer对象
        dispatch_suspend(self.tTimer);
        
        //销毁timer, 注意暂停的timer资源不能直接销毁, 需要先resume再cancel, 否则会造成内存泄漏
        //dispatch_source_cancel(self.tTimer);
    }
}
@end


注意点

  • GCD timer资源必须设定为成员变量, 否则会在创建完毕后立即释放
  • suspend挂起或暂停后的timer要先resume才能cancel, 挂起的timer直接cancel会造成内存泄漏
  • GCDTimer的定时器准确率>NSTimer的准确率,因为GCDTimer 不受当前runloop Mode的影响。

GCDTimer的优势:不受当前runloop Mode的影响。

劣势:虽然说不受runloopMode的影响,但是其计时效应仍不是百分之百准确的。

另外,他的触发事件也有可能被阻塞,当GCD内部管理的所有线程都被占用时,其触发事件将被延迟。

参考

【未经作者允许禁止转载】 Last Updated: 10/14/2021, 11:20:21 AM