【问题标题】:Accurate timing in iOSiOS 中的精确计时
【发布时间】:2011-05-27 23:17:55
【问题描述】:

我正在查看来自 iOS SDK (http://developer.apple.com/library/ios/#samplecode/Metronome/Introduction/Intro.html) 的“Metronome”示例代码。我以 60 BPM 的速度运行节拍器,这意味着每秒一个滴答声。当我查看外接手表(PC 的手表)时,我发现节拍器运行速度太慢 - 它每分钟错过一个节拍,这就是 app。 15 毫秒的一致错误。相关的代码片段是:

- (void)startDriverTimer:(id)info {    
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];       

    // Give the sound thread high priority to keep the timing steady.    
    [NSThread setThreadPriority:1.0];    
    BOOL continuePlaying = YES;   

    while (continuePlaying) {  // Loop until cancelled.   
        // An autorelease pool to prevent the build-up of temporary objects.    
        NSAutoreleasePool *loopPool = [[NSAutoreleasePool alloc] init];     
        [self playSound];

        [self performSelectorOnMainThread:@selector(animateArmToOppositeExtreme) withObject:nil waitUntilDone:NO];    
        NSDate *curtainTime = [[NSDate alloc] initWithTimeIntervalSinceNow:self.duration];    
        NSDate *currentTime = [[NSDate alloc] init];  
        // Wake up periodically to see if we've been cancelled. 
        while (continuePlaying && ([currentTime compare:curtainTime] != NSOrderedDescending)) {     
            if ([soundPlayerThread isCancelled] == YES) {    
                continuePlaying = NO;    
            }    
            [NSThread sleepForTimeInterval:0.01];    
            [currentTime release];    
            currentTime = [[NSDate alloc] init];    
        }

        [curtainTime release];   
        [currentTime release];     
        [loopPool drain];    
    }    
    [pool drain];    
}

在哪里

self.duration

在 60 BPM 的情况下为 1.0 秒。我想知道这个错误来自哪里,以及如何制作更准确的计时器/间隔计数器。

编辑:当我将睡眠时间更改为较小的值时,问题也存在,例如 .001。

EDIT2(更新):当我使用CFAbsoluteTimeGetCurrent() 方法进行计时时,问题也存在。当我使用相同的方法测量按钮点击事件之间的时间时,时间似乎准确 - 我每秒点击一次(在观看手表时),测量的速率为 60 BPM(平均)。所以我想NSThread(?)一定有问题。另一件事是,在设备 (iPod) 上,问题似乎比在模拟器上更严重。

【问题讨论】:

    标签: cocoa-touch ios time ios4


    【解决方案1】:

    好的,我在做更多测试后得到了一些答案,所以我将它分享给任何有兴趣的人。

    我在play 方法(实际将play 消息发送到AVAudioPlayer 对象的方法)中放置了一个变量来测量滴答之间的时间间隔,并作为我的简单比较外部- 观察实验表明,60 BPM 太慢了 - 我得到了这些时间间隔(以秒为单位):

    1.004915
    1.009982
    1.010014
    1.010013
    1.010028
    1.010105
    1.010095
    1.010105
    

    我的结论是,在计算每 1 秒间隔后会经过一些开销时间,而额外时间(大约 10 毫秒)会在几十秒后累积到明显的数量——对于节拍器来说非常糟糕。因此,我决定测量第一次调用的 total 间隔,而不是测量 between 调用的间隔,这样就不会累积错误。换句话说,我已经替换了这个条件:

    while (continuePlaying && ((currentTime0 + [duration doubleValue]) >= currentTime1)
    

    在这种情况下:

    while (continuePlaying && ((_currentTime0 + _cnt * [duration doubleValue]) >= currentTime1 ))
    

    现在_currentTime0_cnt 是类成员(对不起,如果它是一个 c++ 行话,我对 Obj-C 很陌生),前者保存第一次调用该方法的时间戳,而后者是一个 int 计数滴答数(==函数调用)。这导致了以下测量的时间间隔:

    1.003942
    0.999754
    0.999959
    1.000213
    0.999974
    0.999451
    1.000581
    0.999470
    1.000370
    0.999723
    1.000244
    1.000222
    0.999869
    

    很明显,即使不计算平均值,这些值也会在 1.0 秒左右波动(平均值接近 1.0,精度至少为一毫秒)。

    我很高兴听到更多关于导致额外时间流逝的见解 - 10 毫秒对于现代 CPU 来说听起来是永恒的 - 尽管我不熟悉 iPod CPU 的规格(它是 iPod 4G,维基百科说CUP 是 PowerVR SGX GPU 535 @ 200 MHz)

    【讨论】:

    • +1 感谢分享!这些数字让我对使用 sleepForTimeInterval 时的预期抖动有所了解。
    • 1.0002901538 是我得到的平均值。
    【解决方案2】:

    谢谢!我花了一点时间才弄清楚将 CFAbsoluteTimeGetCurrent() 用于 currentTime0 和 currentTime1 ,并且不得不单独投射持续时间(CGFloat)(double myDuration = (double)duration),就像一匹母马试图使用 doubleValue 一样。

    您的建议非常适合我的需求 - 您是否设法提高了准确性?

    【讨论】:

    • 我不明白 - 准确度如何?如果您参考原始问题,那么是的,使用我的答案中描述的方法进行测量确实可以提高准确性。
    【解决方案3】:

    您是否考虑过使用 NSTimer 而不是这种“奇怪的”while-loop-solution?在我看来,您在这里 - 不仅有点 - 使非常非常简单的事情过于复杂......

    【讨论】:

    • 如果你需要准确的计时(当然“准确”是一个应用问题),NSTimer 还不够好。请参阅NSTimer 类参考:A timer is not a real-time mechanism; it fires only when one of the run loop modes to which the timer has been added is running and able to check if the timer’s firing time has passed. Because of the various input sources a typical run loop manages, the effective resolution of the time interval for a timer is limited to on the order of 50-100 milliseconds。 “奇怪”的 while 循环是 Apple 对节拍器的小鬼,即使这样也不够准确
    【解决方案4】:

    我对此也很感兴趣,也注意到了使用 NSTimer 和节拍器示例的谬误。

    我目前正在研究 CAMediaTiming 协议...但我不知道如何使用它,但它可以证明是一个更精确的解决方案,因为它用于动画。我也可能完全错了,因为当玩太多时游戏会变慢。我的理论基于我最喜欢的游戏,在“街头与对手战斗”时需要精确的时间。我认为游戏的 fps 已经降低到 30 与控制台对应的时钟为 60 相比。然而游戏的组合系统的时间非常关键,例如一个组合包括点击一个按钮,然后在 3 帧后点击另一个按钮,因此:

    • iOS 上的游戏在时间上更加宽容
    • 开发人员实现了自己的计时系统
    • 否则他们正在使用节拍器的计时器循环或 CAMediaTiming 协议。

    有关动画时间的信息可在此处找到:http://developer.apple.com/library/ios/#documentation/Cocoa/Conceptual/Animation_Types_Timing/Articles/Timing.html#//apple_ref/doc/uid/TP40006670-SW1

    【讨论】:

      【解决方案5】:

      问题更为根本。 IOS 不是实时操作系统,因此执行各种操作所需的时间不是“确定的”。它因人而异。为了准确,您需要像 RIM 的新 QNX 操作系统这样的实时操作系统。

      【讨论】:

      • 你是对的,但是从实际的角度来看,可用的准确度对于给定的应用程序可能“足够好”,可用的准确度取决于实现,你使用的 API,等
      【解决方案6】:

      使用 while 循环和 sleep 来为节拍器计时并不是解决此问题的可靠方法,因为正如您所见,它可能会产生抖动和漂移的计时。我相信解决这个问题的标准方法是使用 Core Audio(以一种或另一种方式)来提供一个连续的音频流,其中包含节拍器的节拍,它们之间由正确的静音量隔开,具体取决于节奏。因为您知道准确的采样率,所以您可以非常准确地计时。不幸的是,自己生成音频比您尝试做的要困难得多,但this Stackoverflow question 可能会让您入门。

      【讨论】:

        【解决方案7】:

        如果您需要 CADisplayLink。它以固定的时间间隔调用选择器,最快每秒 60 次,即每 0.0166667 秒。

        如果您想进行节拍器测试,请将 frameInterval 设置为 60,以便每秒回调一次。

        self.syncTimer = [CADisplayLink displayLinkWithTarget:self selector:@selector(syncFired:)];
        self.syncTimer.frameInterval = 60;
        [self.syncTimer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
        
        -(void)syncFired:(CADisplayLink *)displayLink
        {
            static NSTimeInterval lastSync = 0;
            NSTimeInterval now = CACurrentMediaTime();
            if (lastSync > 0) {
                NSLog(@"interval: %f", now - lastSync);
            }
            lastSync = now;
        }
        

        在我的测试中,该计时器在 0.0001 秒内每秒持续调用一次。

        如果您运行的设备的刷新率与 60Hz 不同,则必须将 displayLink.duration 除为 1.0 并四舍五入到最接近的整数(而不是下限)。

        【讨论】:

        • 房价 >60 怎么办?另外,我不喜欢将计时器与显示硬件相结合的想法,感觉就像是招来麻烦的黑客行为,不是吗..?
        • 如果它是一个黑客,它是一个带有long and honored history 的黑客,并且很可能会继续受到支持。一旦你被调用,你可以使用CACurrentMediaTime() 来获得更高的精度,但我不知道在 iOS 上获得更快回调的任何技术。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-01-23
        • 2017-07-22
        • 2015-11-15
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多