美文网首页将来跳槽用
iOS 中精确定时的常用方法

iOS 中精确定时的常用方法

作者: 大成小栈 | 来源:发表于2019-07-09 14:21 被阅读10次

定时器用于延迟一段时间或在指定时间点执行特定的代码,之前我们介绍过iOS中处理定时任务常用方法,包括NSTimer、 NSObject 中的 performSelector、dispatch_after、dispatch_source_t 等。通过不同方法创建的定时器,其可靠性与精度都有不同。

  1. 定时器与runLoop:定时器NSTimer、CADisplayLink,底层基本都是由 runLoop 支持的。iOS中每个线程内部都会有一个NSRunLoop ,可以通过[NSRunLoop currentRunLoop]获取当前线程中的runLoop ,二者是一一对应关系。runLoop 启动之后,就能够让线程在没有消息时休眠,在有消息时被唤醒并处理消息,避免资源长期被占用。定时器可以作为资源被 add 到 runLoop 中,受runLoop循环的控制及影响。
  2. 可靠性:可靠性指是否严格按照设定的时间间隔按时执行selector;精度:指支持的最小时间间隔是多少。

1. NSTimer的精度

影响NSTimer的执行selector的因素:NSTimer被添加到特定mode的runLoop中;该mode型的runloop正在运行;到达激发时间。 runLoop 切换模式时,NSTimer 如果处于default模式下可能不会被触发。每个 runLoop 的循环间隔也无法保证,一般时间间隔限制为50-100毫秒比较合理,如果某个任务比较耗时,runLoop 的处理下一个就会被顺延,也就是说NSTimer但并不可靠。

测试代码:

#pragma mark - NSTimer Methods

- (void)resumeTimer {
    
    if (_timer) {
        [self pauseTimer];
    }
    _timer = [NSTimer scheduledTimerWithTimeInterval:_timeInterval target:self selector:@selector(onTimeout:) userInfo:nil repeats:YES];
    //_timer.tolerance = 1;// 误差范围1s内
    [_timer fire];
}

- (void)pauseTimer {

    [_timer invalidate];
    _timer = nil;
}


#pragma mark - Test Methods

- (void)startNSTimer {
    
    _maxCount = 10;
    _currentCount = 0;
    _timeInterval = 0.001;// 1ms
    
    [self resumeTimer];
}

- (void)onTimeout:(NSTimer *)sender {
    
    if (_currentCount < _maxCount) {
        
        // selector任务开始
        NSDate *startTime = [NSDate date];
        NSLog(@"---selector start--->> selectorNo.%ld, startTime:%@, start-start diff:%.3fms", (long)_currentCount, [self getTimeStampStr:startTime], [startTime timeIntervalSinceDate:_lastStartTime]*1000);
        _lastStartTime = startTime;
        
        // 耗时任务
        if (_currentCount == 8) {
            NSInteger count = 0;
            for (int i = 0; i < 1000000000; i++) {
                count++;
            }
        }

        // selector结束
        NSDate *endTime = [NSDate date];
        NSLog(@"---selector ended--->> selectorNo.%ld, endTime:%@, end-start diff:%.3fms", (long)_currentCount, [self getTimeStampStr:endTime], [endTime timeIntervalSinceDate:startTime]*1000);
        
        _currentCount++;
    } else {
        [self pauseTimer];
    }
}

- (NSString *)getTimeStampStr:(NSDate *)date {
    
    NSTimeInterval interval = [date timeIntervalSince1970];
    NSString *intervalStr = [NSString stringWithFormat:@"%.3fms", interval * 1000];
    
    return [NSString stringWithFormat:@"%@", intervalStr];
}


//// 真机iPhone SE 测试,No.8时执行耗时任务的log
2019-07-09 13:53:46.239741+0800 QiTimer[1571:401783] ---selector start--->> selectorNo.0, startTime:1562651626239.612ms, start-start diff:nanms
2019-07-09 13:53:46.239964+0800 QiTimer[1571:401783] ---selector ended--->> selectorNo.0, endTime:1562651626239.911ms, end-start diff:0.299ms
2019-07-09 13:53:46.240410+0800 QiTimer[1571:401783] ---selector start--->> selectorNo.1, startTime:1562651626240.359ms, start-start diff:0.747ms
2019-07-09 13:53:46.240521+0800 QiTimer[1571:401783] ---selector ended--->> selectorNo.1, endTime:1562651626240.476ms, end-start diff:0.117ms
2019-07-09 13:53:46.244649+0800 QiTimer[1571:401783] ---selector start--->> selectorNo.2, startTime:1562651626244.570ms, start-start diff:4.211ms
2019-07-09 13:53:46.244817+0800 QiTimer[1571:401783] ---selector ended--->> selectorNo.2, endTime:1562651626244.768ms, end-start diff:0.198ms
2019-07-09 13:53:46.245274+0800 QiTimer[1571:401783] ---selector start--->> selectorNo.3, startTime:1562651626245.226ms, start-start diff:0.656ms
2019-07-09 13:53:46.245379+0800 QiTimer[1571:401783] ---selector ended--->> selectorNo.3, endTime:1562651626245.336ms, end-start diff:0.110ms
2019-07-09 13:53:46.245844+0800 QiTimer[1571:401783] ---selector start--->> selectorNo.4, startTime:1562651626245.710ms, start-start diff:0.484ms
2019-07-09 13:53:46.245978+0800 QiTimer[1571:401783] ---selector ended--->> selectorNo.4, endTime:1562651626245.907ms, end-start diff:0.197ms
2019-07-09 13:53:46.246242+0800 QiTimer[1571:401783] ---selector start--->> selectorNo.5, startTime:1562651626246.197ms, start-start diff:0.487ms
2019-07-09 13:53:46.246343+0800 QiTimer[1571:401783] ---selector ended--->> selectorNo.5, endTime:1562651626246.300ms, end-start diff:0.103ms
2019-07-09 13:53:46.246591+0800 QiTimer[1571:401783] ---selector start--->> selectorNo.6, startTime:1562651626246.544ms, start-start diff:0.347ms
2019-07-09 13:53:46.246696+0800 QiTimer[1571:401783] ---selector ended--->> selectorNo.6, endTime:1562651626246.651ms, end-start diff:0.107ms
2019-07-09 13:53:46.246942+0800 QiTimer[1571:401783] ---selector start--->> selectorNo.7, startTime:1562651626246.896ms, start-start diff:0.352ms
2019-07-09 13:53:46.247045+0800 QiTimer[1571:401783] ---selector ended--->> selectorNo.7, endTime:1562651626246.999ms, end-start diff:0.103ms
2019-07-09 13:53:46.247863+0800 QiTimer[1571:401783] ---selector start--->> selectorNo.8, startTime:1562651626247.769ms, start-start diff:0.873ms
2019-07-09 13:53:53.551494+0800 QiTimer[1571:401783] ---selector ended--->> selectorNo.8, endTime:1562651633551.411ms, end-start diff:7303.642ms
2019-07-09 13:53:53.552640+0800 QiTimer[1571:401783] ---selector start--->> selectorNo.9, startTime:1562651633552.600ms, start-start diff:7304.831ms
2019-07-09 13:53:53.552707+0800 QiTimer[1571:401783] ---selector ended--->> selectorNo.9, endTime:1562651633552.685ms, end-start diff:0.085ms

在设置不同timeInterval值实验时,start-start diff 的值最小在 1ms 左右。在第No.8次时执行一个较耗时的任务,No.8的任务执行时间 end-start diff 为 7303.642ms,导致第No.9次开始时间start-start diff比预期延迟了7303.831ms秒执行。本例中定时器被添加在主线程,由于定时器在runLoop每个循环中被检测一次,所以如果在这一次的runLoop中做了耗时的操作,当前runLoop持续的时间超过了定时器的间隔时间,那么下一次定时就被延后了。
解决方法: 在子线程中创建timer,在子线程中进行定时任务的操作,需要UI操作时切换回主线程进行操作。或者在子线程中创建timer,在主线程进行定时任务的操作。

2. GCDTimer 的精度

回顾一下 GCDTimer 的基本实现过程:

// 1. 创建 dispatch source,指定检测事件为定时
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue("Timer_Queue", 0));
// 2. 设置定时器启动时间、间隔
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 0.5 * NSEC_PER_SEC,  0 * NSEC_PER_SEC); 
// 3. 设置callback
dispatch_source_set_event_handler(timer, ^{
        NSLog(@"timer fired");
    });
dispatch_source_set_event_handler(timer, ^{
       //取消定时器时一些操作
    });
// 4. 启动定时器(刚创建的source处于被挂起状态)
dispatch_resume(timer);
// 5. 暂停定时器
dispatch_suspend(timer);
// 6. 取消定时器
dispatch_source_cancel(timer);
timer = nil;

GCDTimer相较于NSTimer的优点很明显,NSTimer必须保证有一个活跃的runloop、创建与撤销必须在同一个线程操作、内存管理有潜在泄露的风险等,从上面的实现过程就可以看出使用GCDTimer基本没有这些顾虑。按照NSTimer的测试逻辑对GCDTimer也进行相应测试,代码如下:

+ (QiGCDTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats queue:(dispatch_queue_t)queue block:(void (^)(void))block {
    
    QiGCDTimer *timer = [[QiGCDTimer alloc] initWithInterval:interval repeats:repeats queue:queue block:block];
    return timer;
}

- (instancetype)initWithInterval:(NSTimeInterval)interval repeats:(BOOL)repeats queue:(dispatch_queue_t)queue block:(void (^)(void))block {
    
    self = [super init];
    if (self) {
        
        //// 测试
        _maxCount = 10;
        _currentCount = 0;
        

        self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
        dispatch_source_set_timer(self.timer, dispatch_time(DISPATCH_TIME_NOW, interval * NSEC_PER_SEC), interval * NSEC_PER_SEC, 0);
        dispatch_source_set_event_handler(self.timer, ^{
            if (!repeats) {
                dispatch_source_cancel(self.timer);
            }
            block();
            
            
            //// 测试
            [self onTimeout];
        });
        dispatch_resume(self.timer);
    }
    return self;
}

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

- (void)invalidate {
    
    if (self.timer) {
        dispatch_source_cancel(self.timer);
    }
}



//// 测试
- (void)onTimeout {
    
    if (_currentCount < _maxCount) {
        
        // selector任务开始
        NSDate *startTime = [NSDate date];
        NSLog(@"---selector start--->> selectorNo.%ld, startTime:%@, start-start diff:%.3fms", (long)_currentCount, [self getTimeStampStr:startTime], [startTime timeIntervalSinceDate:_lastStartTime]*1000);
        _lastStartTime = startTime;
        
        // 耗时任务
        if (_currentCount == 8) {
            NSInteger count = 0;
            for (int i = 0; i < 1000000000; i++) {
                count++;
            }
        }
        
        // selector结束
        NSDate *endTime = [NSDate date];
        NSLog(@"---selector ended--->> selectorNo.%ld, endTime:%@, end-start diff:%.3fms", (long)_currentCount, [self getTimeStampStr:endTime], [endTime timeIntervalSinceDate:startTime]*1000);
        
        _currentCount++;
    } else {
        [self invalidate];
    }
}

- (NSString *)getTimeStampStr:(NSDate *)date {
    
    NSTimeInterval interval = [date timeIntervalSince1970];
    NSString *intervalStr = [NSString stringWithFormat:@"%.3fms", interval * 1000];
    
    return [NSString stringWithFormat:@"%@", intervalStr];
}


//// 真机iPhone SE 测试,No.8时执行耗时任务的log
2019-07-09 13:46:25.143749+0800 QiTimer[1557:400177] ---selector start--->> selectorNo.0, startTime:1562651185143.624ms, start-start diff:nanms
2019-07-09 13:46:25.143968+0800 QiTimer[1557:400177] ---selector ended--->> selectorNo.0, endTime:1562651185143.916ms, end-start diff:0.292ms
2019-07-09 13:46:25.144218+0800 QiTimer[1557:400177] ---selector start--->> selectorNo.1, startTime:1562651185144.169ms, start-start diff:0.545ms
2019-07-09 13:46:25.144324+0800 QiTimer[1557:400177] ---selector ended--->> selectorNo.1, endTime:1562651185144.280ms, end-start diff:0.111ms
2019-07-09 13:46:25.144649+0800 QiTimer[1557:400177] ---selector start--->> selectorNo.2, startTime:1562651185144.522ms, start-start diff:0.353ms
2019-07-09 13:46:25.144820+0800 QiTimer[1557:400177] ---selector ended--->> selectorNo.2, endTime:1562651185144.752ms, end-start diff:0.230ms
2019-07-09 13:46:25.145095+0800 QiTimer[1557:400177] ---selector start--->> selectorNo.3, startTime:1562651185145.044ms, start-start diff:0.522ms
2019-07-09 13:46:25.145201+0800 QiTimer[1557:400177] ---selector ended--->> selectorNo.3, endTime:1562651185145.157ms, end-start diff:0.113ms
2019-07-09 13:46:25.145395+0800 QiTimer[1557:400177] ---selector start--->> selectorNo.4, startTime:1562651185145.349ms, start-start diff:0.305ms
2019-07-09 13:46:25.145563+0800 QiTimer[1557:400177] ---selector ended--->> selectorNo.4, endTime:1562651185145.518ms, end-start diff:0.169ms
2019-07-09 13:46:25.145776+0800 QiTimer[1557:400177] ---selector start--->> selectorNo.5, startTime:1562651185145.724ms, start-start diff:0.375ms
2019-07-09 13:46:25.145896+0800 QiTimer[1557:400177] ---selector ended--->> selectorNo.5, endTime:1562651185145.852ms, end-start diff:0.128ms
2019-07-09 13:46:25.146273+0800 QiTimer[1557:400177] ---selector start--->> selectorNo.6, startTime:1562651185146.239ms, start-start diff:0.515ms
2019-07-09 13:46:25.146344+0800 QiTimer[1557:400177] ---selector ended--->> selectorNo.6, endTime:1562651185146.315ms, end-start diff:0.076ms
2019-07-09 13:46:25.146497+0800 QiTimer[1557:400177] ---selector start--->> selectorNo.7, startTime:1562651185146.467ms, start-start diff:0.228ms
2019-07-09 13:46:25.146565+0800 QiTimer[1557:400177] ---selector ended--->> selectorNo.7, endTime:1562651185146.537ms, end-start diff:0.070ms
2019-07-09 13:46:25.147260+0800 QiTimer[1557:400177] ---selector start--->> selectorNo.8, startTime:1562651185147.224ms, start-start diff:0.757ms
2019-07-09 13:46:32.465747+0800 QiTimer[1557:400177] ---selector ended--->> selectorNo.8, endTime:1562651192465.652ms, end-start diff:7318.428ms
2019-07-09 13:46:32.466068+0800 QiTimer[1557:400177] ---selector start--->> selectorNo.9, startTime:1562651192466.045ms, start-start diff:7318.821ms
2019-07-09 13:46:32.466117+0800 QiTimer[1557:400177] ---selector ended--->> selectorNo.9, endTime:1562651192466.098ms, end-start diff:0.053ms

在设置不同timeInterval值实验时,start-start diff 的值最小在 0.3ms 左右。在第No.8次时执行一个较耗时的任务,No.8的任务执行时间 end-start diff 为 7318.428ms,导致第No.9次开始时间start-start diff比预期延迟了7317.821ms秒执行。本例中GCD定时器仍被运行在主线程,所以可以看出虽然GCDTimer不受runLoop影响,但是当前线程处理任务时间超过了定时器的间隔时间,那么下一次定时也会被延后,因此,需要处理耗时任务时,也需要借助异步线程。GCDTimer不受runloop影响,一般用在轮播图、读写文件等不需要受到runloop影响的情况下。

3. CADisplayLink

CADisplayLink 属于 QuartzCore框架,它调用间隔与屏幕刷新频率一致,每秒 60 帧,间隔 16.67ms。 当需与显示更新同步的定时时(如刷新界面动画等),建议CADisplayLink,可以省去一些多余的计算。我们之前没有介绍过CADisplayLink,下面我们看一下CADisplayLink的用法和精度:

3.1 调用形式
- (void)resumeCADisplayLink {

        _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(rotate)];
        _displayLink.frameInterval = 1;
        [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}

- (void) pauseCADisplayLink {

    [_displayLink invalidate];
    _displayLink = nil;
}

3.2 几个属性
  • frameInterval
    表示间隔多少帧调用一次selector,默认为1,即每帧都调用一次。官方文档中强调,当该值被设定小于1时,结果是不可预知的。
  • duration
    表示两次屏幕刷新之间的时间间隔,只读属性,该属性在target的selector被首次调用以后才会被赋值,我们可以计算出selector的调用间隔时间为duration * frameInterval。
    现存的iOS设备屏幕的刷新频率为60Hz,这一点可以从CADisplayLink的duration属性看出来。duration的值为1/60,即0.166666...
  • timestamp
    表示屏幕显示的上一帧的时间戳,只读属性,CFTimeInterval类型,该属性通常被target用来计算下一帧中应该显示的内容。
  • preferredFramesPerSecond
    可以通过该属性来设置CADisplayLink每秒刷新次数,默认值为屏幕最大帧率60Hz,如果在特定帧率内无法提供对象的操作,可以通过降低帧率解决,实际的屏幕帧率会和手动设置的preferredFramesPerSecond值有一定的出入。
3.3 CADisplayLink的精度

iOS设备的屏幕刷新频率(FPS)是60Hz,CADisplayLink调用间隔与屏幕刷新频率一致,即最小精度为 16.67 ms。在执行耗时任务时,CADisplayLink也会导致下次调用被推迟,因此也并不非常可靠。但是,假如调用者能够确保任务能够在最小时间间隔内执行完成,CADisplayLink 就比较可靠。

同样按照NSTimer的测试逻辑对CADisplayLink也进行相应测试,代码如下:

#pragma mark - NSTimer Methods

- (void)resumeDisplayLink {

    _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(onTimeout:)];
    //_displayLink.preferredFramesPerSecond = 50;
    [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}

- (void)pauseDisplayLink {

    [_displayLink invalidate];
    _displayLink = nil;
}



#pragma mark - Test Methods

- (void)startCADisplayLinkTimer {
    
    _maxCount = 10;
    _currentCount = 0;
    _timeInterval = 0.001;// 1ms
    
    [self resumeDisplayLink];
}

- (void)onTimeout:(NSTimer *)sender {
    
    if (_currentCount < _maxCount) {
        
        // selector任务开始
        NSDate *startTime = [NSDate date];
        NSLog(@"---selector start--->> selectorNo.%ld, startTime:%@, start-start diff:%.3fms", (long)_currentCount, [self getTimeStampStr:startTime], [startTime timeIntervalSinceDate:_lastStartTime]*1000);
        _lastStartTime = startTime;
        
        // 耗时任务
        if (_currentCount == 8) {
            NSInteger count = 0;
            for (int i = 0; i < 1000000000; i++) {
                count++;
            }
        }
        
        // selector结束
        NSDate *endTime = [NSDate date];
        NSLog(@"---selector ended--->> selectorNo.%ld, endTime:%@, end-start diff:%.3fms", (long)_currentCount, [self getTimeStampStr:endTime], [endTime timeIntervalSinceDate:startTime]*1000);
        
        _currentCount++;
    } else {
        [self pauseDisplayLink];
    }
}

- (NSString *)getTimeStampStr:(NSDate *)date {
    
    NSTimeInterval interval = [date timeIntervalSince1970];
    NSString *intervalStr = [NSString stringWithFormat:@"%.3fms", interval * 1000];
    
    return [NSString stringWithFormat:@"%@", intervalStr];
}


//// 真机iPhone SE 测试,No.8时执行耗时任务的log
2019-07-09 14:00:34.515269+0800 QiTimer[1576:402536] ---selector start--->> selectorNo.0, startTime:1562652034515.138ms, start-start diff:nanms
2019-07-09 14:00:34.515429+0800 QiTimer[1576:402536] ---selector ended--->> selectorNo.0, endTime:1562652034515.395ms, end-start diff:0.257ms
2019-07-09 14:00:34.532062+0800 QiTimer[1576:402536] ---selector start--->> selectorNo.1, startTime:1562652034531.969ms, start-start diff:16.831ms
2019-07-09 14:00:34.532228+0800 QiTimer[1576:402536] ---selector ended--->> selectorNo.1, endTime:1562652034532.195ms, end-start diff:0.226ms
2019-07-09 14:00:34.548910+0800 QiTimer[1576:402536] ---selector start--->> selectorNo.2, startTime:1562652034548.796ms, start-start diff:16.827ms
2019-07-09 14:00:34.549099+0800 QiTimer[1576:402536] ---selector ended--->> selectorNo.2, endTime:1562652034549.051ms, end-start diff:0.255ms
2019-07-09 14:00:34.565546+0800 QiTimer[1576:402536] ---selector start--->> selectorNo.3, startTime:1562652034565.440ms, start-start diff:16.644ms
2019-07-09 14:00:34.565732+0800 QiTimer[1576:402536] ---selector ended--->> selectorNo.3, endTime:1562652034565.682ms, end-start diff:0.242ms
2019-07-09 14:00:34.582133+0800 QiTimer[1576:402536] ---selector start--->> selectorNo.4, startTime:1562652034582.022ms, start-start diff:16.582ms
2019-07-09 14:00:34.582323+0800 QiTimer[1576:402536] ---selector ended--->> selectorNo.4, endTime:1562652034582.274ms, end-start diff:0.252ms
2019-07-09 14:00:34.598925+0800 QiTimer[1576:402536] ---selector start--->> selectorNo.5, startTime:1562652034598.806ms, start-start diff:16.784ms
2019-07-09 14:00:34.599122+0800 QiTimer[1576:402536] ---selector ended--->> selectorNo.5, endTime:1562652034599.072ms, end-start diff:0.266ms
2019-07-09 14:00:34.615504+0800 QiTimer[1576:402536] ---selector start--->> selectorNo.6, startTime:1562652034615.389ms, start-start diff:16.583ms
2019-07-09 14:00:34.615693+0800 QiTimer[1576:402536] ---selector ended--->> selectorNo.6, endTime:1562652034615.645ms, end-start diff:0.256ms
2019-07-09 14:00:34.632195+0800 QiTimer[1576:402536] ---selector start--->> selectorNo.7, startTime:1562652034632.084ms, start-start diff:16.695ms
2019-07-09 14:00:34.632382+0800 QiTimer[1576:402536] ---selector ended--->> selectorNo.7, endTime:1562652034632.335ms, end-start diff:0.251ms
2019-07-09 14:00:34.649002+0800 QiTimer[1576:402536] ---selector start--->> selectorNo.8, startTime:1562652034648.892ms, start-start diff:16.808ms
2019-07-09 14:00:41.948554+0800 QiTimer[1576:402536] ---selector ended--->> selectorNo.8, endTime:1562652041948.464ms, end-start diff:7299.572ms
2019-07-09 14:00:41.949088+0800 QiTimer[1576:402536] ---selector start--->> selectorNo.9, startTime:1562652041949.021ms, start-start diff:7300.129ms
2019-07-09 14:00:41.949141+0800 QiTimer[1576:402536] ---selector ended--->> selectorNo.9, endTime:1562652041949.121ms, end-start diff:0.100ms

最小精度为16.7ms左右,精度误差一般在 0.1 ~ 0.5 毫秒之间,精度比 NSTimer 要高。第No.8次受耗时任务影响,延时7299.572ms。因此CADisplayLink运行在主线程中在耗时任务之后,精度也不可控,需要借助多线程处理。

4. 更高精度的定时器

上述的几种定时器虽然形式与用法不一,但核心逻辑实际是一样的,都受限于苹果为提高性能采用的各种策略,可能导致下一次无法实时地执行selector。如果你确有需求要使用更高精度的定时器(一般视频/音频、精确帧速率的游戏等相关数据流操作中会需要),苹果也提供了相应方法 iOS/OS X 中的高精度定时器。这里说的高精度定时器与之前介绍的几个定时器处理逻辑不一样,它是基于高优先级的线程调度类创建的定时器,在没有多线程冲突的情况下,这类定时器的请求会被优先处理。

iOS/OS X 中的高精度定时器逻辑:把定时器所在的线程,移到高优先级的线程调度类;使用底层更精确的计时器API(以CPU时钟为参照的计时API)。

4.1 使用过程
  • 将计时线程,调度为实时线程
    把定时器所在的线程,移到高优先级的线程调度类即the real time scheduling class中:
#include <mach/mach.h>
#include <mach/mach_time.h>
#include <pthread.h>
 
void move_pthread_to_realtime_scheduling_class(pthread_t pthread)
{
    mach_timebase_info_data_t timebase_info;
    mach_timebase_info(&timebase_info);
 
    const uint64_t NANOS_PER_MSEC = 1000000ULL;
    double clock2abs = ((double)timebase_info.denom / (double)timebase_info.numer) * NANOS_PER_MSEC;
 
    thread_time_constraint_policy_data_t policy;
    policy.period      = 0;
    policy.computation = (uint32_t)(5 * clock2abs); // 5 ms of work
    policy.constraint  = (uint32_t)(10 * clock2abs);
    policy.preemptible = FALSE;
 
    int kr = thread_policy_set(pthread_mach_thread_np(pthread_self()),
                   THREAD_TIME_CONSTRAINT_POLICY,
                   (thread_policy_t)&policy,
                   THREAD_TIME_CONSTRAINT_POLICY_COUNT);
    if (kr != KERN_SUCCESS) {
        mach_error("thread_policy_set:", kr);
        exit(1);
    }
}
  • 会用到的计时API
    使用更精确的计时API mach_wait_until(),如下代码使用mach_wait_until()等待10秒:
#include <mach/mach.h>
#include <mach/mach_time.h>
 
static const uint64_t NANOS_PER_USEC = 1000ULL;
static const uint64_t NANOS_PER_MILLISEC = 1000ULL * NANOS_PER_USEC;
static const uint64_t NANOS_PER_SEC = 1000ULL * NANOS_PER_MILLISEC;
 
static mach_timebase_info_data_t timebase_info;
 
static uint64_t abs_to_nanos(uint64_t abs) {
    return abs * timebase_info.numer  / timebase_info.denom;
}
 
static uint64_t nanos_to_abs(uint64_t nanos) {
    return nanos * timebase_info.denom / timebase_info.numer;
}
 
void example_mach_wait_until(int argc, const char * argv[])
{
    mach_timebase_info(&timebase_info);
    uint64_t time_to_wait = nanos_to_abs(10ULL * NANOS_PER_SEC);
    uint64_t now = mach_absolute_time();
    mach_wait_until(now + time_to_wait);
}
4.2 该定时器的精度

mach_absolute_time() 用于获取机器时间(单位是纳秒),
测试代码来源于网络,其功能展示了高精度定时器与NSTimer的对比。

5. 总结

  1. NSTimer 最常用,需要注意的就是加入的 runLoop 的 Mode ,若是子线程,需要手动 run 这个 RunLoop ;同时注意使用 invalidate 手动停止定时,否则引起内存泄漏;NSTimer的创建与撤销必须在同一个线程操作,不能跨越线程操作;
  2. GCD Timer 较 NSTimer 精度高,一般用于对文件资源等定期读写操作很方便,使用时需要注意 dispatch_resume 与 dispatch_suspend 配套,并且要给 dispatch source 设置新值或者置nil,需先 dispatch_source_cancel(timer) ,否则会导致崩溃;
  3. 需与显示更新同步的定时,建议 CADisplayLink ,可以省去多余计算;
  4. 高精度定时,一般视频/音频、精确帧速率的游戏等相关数据流操作中会需要。

相关文章

  • iOS 中精确定时的常用方法

    级别: ★☆☆☆☆标签:「iOS」「定时 」作者: dac_1033审校: QiShare团队 定时器用于延迟一段...

  • iOS 中精确定时的常用方法

    定时器用于延迟一段时间或在指定时间点执行特定的代码,之前我们介绍过iOS中处理定时任务常用方法,包括NSTimer...

  • iOS 开发中 runtime 常用的几种方法

    iOS 开发中 runtime 常用的几种方法 iOS 开发中 runtime 常用的几种方法

  • GCD定时器使用

    iOS中的常用定时器分为这几类: NSTimer CADisplayLink GCD定时器 选择GCD定时器原因:...

  • iOS:NSTimer的几种创建方式

    在iOS开发中,经常会用到定时器,iOS中常用的定时器有三种:NSTimer,GCD,CADisplayLink。...

  • CADisplayLink 和 NSTimer 的精确度

    iOS中一般UI上面常用两种定时器 NSTimer和CADisplayLink,那么它们分别的精确度是如何呢? C...

  • iOS Timer

    iOS开发中定时器经常会用到,iOS中常用的定时器有三种,分别是NSTime,CADisplayLink和GCD。...

  • iOS三大定时器

    iOS开发中定时器经常会用到,iOS中常用的定时器有三种,分别是NSTime,CADisplayLink和GCD。...

  • iOS开发中常用的方法(一)

    系统弹窗:### 过期方法: 新方法: 定时器/延时:### 延迟调用方法一: 延迟调用方法二: 定时器一:(精确...

  • 【Swift】iOS中的定时器

    在iOS中我们常用的定时器有三种: Timer, CADisplayLink, DispatchSourceTim...

网友评论

    本文标题:iOS 中精确定时的常用方法

    本文链接:https://www.haomeiwen.com/subject/csaacctx.html