美文网首页将来跳槽用
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 中精确定时的常用方法

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