关于iOS定时器你需要知道的一切

2,665 阅读3分钟

NSTimer

概述

NSTimerFoundation框架中的一个类,用于创建定时器,可以在指定的时间间隔内执行指定的任务。NSTimerRunLoop的方式工作,可以将其加入RunLoop的特定模式中,从而定时器可以在指定的时间间隔内持续工作

调用方式

方式1

// 以该方式创建的定时器以默认方式添加到当前线程runloop中,无需手动添加 
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) { 
    NSLog(@"%s, %d", __func__, __LINE__); 
}]; 
// 之所以指定该定时器所在的Mode为NSRunLoopCommonModes,是为了保证定时器可以在不同的RunLoop Mode下正常运行
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; 

// 停止定时器 
[timer invalidate];

方式2

// 以该方式创建的定时器需要手动添加到RunLoop才能正常执行 
NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(test:) userInfo:@"2" repeats:YES]; 
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; 

-(void)test: (NSTimer *)content { 
    NSLog(@"%s, %d, %@", __func__, __LINE__, [content userInfo]); 
} 

// 停止定时器 
[timer invalidate];

使用陷阱

  • NSTimer的工作方式是基于RunLoop的,因此需要将其加入RunLoop才能生效。如果不加入RunLoop,定时器将无法启动
  • 在使用NSTimer时要注意循环引用问题,应该使用弱引用或者block来避免循环引用,也可通过NSProxy创建一个临时Target
  • 在多线程中使用NSTimer,需要注意线程安全的问题,可以考虑使用GCD定时器或者NSRunLoopperformSelector:afterDelay: 方法等代替
  • 当定时器不再使用时,需要将其从RunLoop中移除,避免内存泄漏和无用功耗,且其创建与撤销必须在同一个线程操作,不能跨越线程操作
  • NSTimer依赖于Runloop,如果Runloop的任务过于繁重,可能会导致NStimer不准时

CADisplayLink

概述

CADisplayLinkCore Animation框架中的一个类,用于创建一个与显示器刷新频率相同的计时器,如每秒 60 次(屏幕刷新率为 60Hz)调用一次指定方法。CADisplayLink可以直接和Core Animation进行结合,用于实现动画渲染、图形绘制等操作,而且可以在多个RunLoop Modes中执行,非常灵活。

使用场景

  • 实时数据展示:比如在股票行情中,实时绘制股票价格图表
  • 游戏开发:比如用于精灵动画的连续播放
  • 视频录制:实时处理视频帧数据,实现视频的录制和处理
  • 音频波形分析:实时绘制音频波形图,显示音频数据变化

调用方式

// 需要将CADisplayLink对象添加到runLoop中,selector就能被周期性调用 
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(test:)]; 
[displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];

使用陷阱

  • CADisplayLink的任务方法需要在RunLoop中运行,因此需要将其加入指定的RunLoop Mode
  • CADisplayLink显示器刷新率为 60Hz,因此它的任务方法最好不要执行过于耗时的操作,以避免影响界面流畅性
  • 在使用CADisplayLink时要注意循环引用问题,应该使用弱引用或者block来避免循环引用
  • CADisplayLink不再使用时,需要手动停止它的工作,避免内存泄漏和无用功耗

GCD

+ (NSString *)executeTask:(task)task
                   start:(NSTimeInterval)start
                interval:(NSTimeInterval)interval
                 repeats:(BOOL)repeats
                   async:(BOOL)async {
    if (!task || start < 0 || (interval <= 0 && repeats) ) return nil;
    
    // 队列
    dispatch_queue_t queue = async ? dispatch_get_global_queue(0, 0) : dispatch_get_main_queue();

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

    // 设置时间
    // 精准度:一般为0,如果对定时操作要求不精确,只要在一段时间内执行就可以,那么可以把精准度设置的大一点,例如10,这样会提高程序性能
    dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, interval * NSEC_PER_SEC, 0);

    // 加锁
    dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);

    // 定时器唯一标识
    NSString *name = [NSString stringWithFormat:@"%ld", timers_.count];

    // 存放到字典中
    timers_[name] = timer;

    // 解锁
    dispatch_semaphore_signal(semaphore_);

    // 设置回调
    dispatch_source_set_event_handler(timer, ^{
        task();

        // 不重复的任务
        if (!repeats) {
            [self cancelTask:name];
        }
    });

    // 启动定时器
    dispatch_resume(timer);

    return name;
}

优点

GCD定时器实际上是使用了dispatch源(dispatch source),dispatch源监听系统内核对象并处理。dispatch类似生产者消费者模式,通过监听系统内核对象,在生产者生产数据后自动通知相应的dispatch队列执行,后者充当消费者。通过系统级调用,更加精准