Trying to create an accurate timer with GCD

2019-06-01 07:30发布

问题:

I'm trying to make a music app that needs a very precise timer (it needs to be synced with background music). And I need to display the timer as a progress bar on the UI too.

I initially started with NSTimer, turned out to be not accurate at all, with above 20ms off. And I turned to GCD. But I seem not to be able to get it work neither.

Here is my code (removed parts that are not related)

This is the first line in my ViewController class

var dispatchQueue = dispatch_queue_t()

And in my override func viewDidLoad() function

dispatchQueue = dispatch_queue_create("myQueueId", nil)

Then the actual functions that use the dispatch queue

// Generate new measure
// In the main dispatch queue because it handles a lot of work
func newMeasure() {
    timerBar.progress = 1.0
    // Reset the timer
    logEnd = NSDate()
    let lapse = logEnd.timeIntervalSinceDate(logStart)
    println("Lapse: \(lapse)")
    logStart = NSDate()

    // measureTimer is set to 1.47, so the newMeasure is supposedly to be called every 1.47 seconds
    timer = measureTimer

    startTime = NSDate()
    dispatch_async(dispatchQueue, {
        self.gameTimer()
    })

    // ... do a lot of stuff that will drag the timer if not put in dispatch queue
}

// Accurate Game Timer
// In the dispacheQueue so the timer can has its own thread
func gameTimer() {
    // Get the time now
    let now = NSDate()
    let adjust = now.timeIntervalSinceDate(self.startTime)
    // Refresh the start time to be now
    self.startTime = now
    self.timer = self.timer - adjust

    // Go back to the main dispatch queue to update UI
    dispatch_async(dispatch_get_main_queue(), {
        self.timerBar.progress = Float(self.timer / self.measureTimer)
    })

    if (self.timer <= 0.2) {
        dispatch_async(dispatch_get_main_queue(), {
            // Going back to the main dispatch queue to start another new measure
            NSTimer.scheduledTimerWithTimeInterval(self.timer, target: self, selector: Selector("newMeasure"), userInfo: nil, repeats: false)
        })
    }
    else {
        dispatch_after(dispatch_time_t(
            100 * NSEC_PER_MSEC), dispatchQueue, {
                self.gameTimer()
        })
    }
}

But the actual log looks like this:

Lapse: 1.47152501344681
Lapse: 1.51420003175735
Lapse: 1.47065001726151
Lapse: 1.47149801254272
Lapse: 1.471755027771
Lapse: 1.47201299667358
Lapse: 1.47167503833771
Lapse: 1.47148901224136
Lapse: 1.47146201133728
Lapse: 1.47289103269577
Lapse: 1.47580003738403
Lapse: 1.47087097167969
Lapse: 1.47286003828049
Lapse: 1.47235900163651
Lapse: 1.47159999608994
Lapse: 1.47144496440887
Lapse: 1.50616401433945
Lapse: 1.51679295301437
Lapse: 1.47380495071411
Lapse: 1.47130501270294
Lapse: 1.50468301773071
Lapse: 1.4718160033226
Lapse: 1.49480104446411
Lapse: 1.50195497274399
Lapse: 1.50018000602722
Lapse: 1.47339296340942
Lapse: 1.47266495227814

They still are not so on time. And because time is so sensitive to music, this is not gonna work. Can someone help me with this accuracy issue? Thank you

回答1:

You are not actually using a GCD timer. A GCD timer would involve a dispatch source, and there is no dispatch source in your code.

Even then, however, the only really accurate way to get a timer is through CADisplayLink. It is a built-in timer that fires approximately every 1/60 second, because it is tied to the refresh rate of the hardware itself. Even CADisplayLink is not perfectly accurate, but it is close, and it as frequent as you can possibly get, and every firing includes a timestamp so you can compensate quite precisely.



回答2:

You should consider retiring NSTimer altogether and create a dispatch timer, which has a leeway parameter in which you can specify how much latitude you're giving the timer to run. Also, if you're trying to do timers on a background thread, it's a far simpler and more efficient way of doing that:

private var timer: dispatch_source_t!
private var start: CFAbsoluteTime!

func startTimer() {
    let queue = dispatch_queue_create("com.domain.app.timer", nil)
    timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue)
    dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, UInt64(1.47 * Double(NSEC_PER_SEC)), 0) // every 1.47 seconds, with no leeway
    dispatch_source_set_event_handler(timer) {
        let current = CFAbsoluteTimeGetCurrent()
        NSLog(String(format: "%.4f", current - self.start))
        self.start = current
    }

    start = CFAbsoluteTimeGetCurrent()
    dispatch_resume(timer)
}

func stopTimer() {
    dispatch_source_cancel(timer)
    timer = nil
}

This isn't going to happen precisely every 1.47 seconds, but it might be closer.

Note, I'm avoid dispatching stuff back and forth and/or scheduling new timers or dispatch_after, though. I create a single dispatch timer and let it go. Also, I'm avoiding NSDate and using CFAbsoluteTime, which should be more efficient.



回答3:

Remember: even though you dispatch_async to the main thread, it still has to be dispatched. So there is a finite time period between capture the state of self.timer and updating the UI. source: https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/WorkingwithBlocks/WorkingwithBlocks.html

NSTimer shouldnt be used for accurate measurements. source:

https://developer.apple.com/library/ios/documentation/Cocoa/Reference/Foundation/Classes/NSTimer_Class/index.html

Here is a sample class I'm working on that uses mach_absolute_time instead. Its just a stub, I still need to flesh it out, but it might give you something to go on.

Header:

#import <Foundation/Foundation.h>
#include <mach/mach_time.h>


@interface TimeKeeper : NSObject
{
    uint64_t timeZero;

}

+ (id) timer;

- (void) start;
- (uint64_t) elapsed;
- (float) elapsedSeconds;

@end

Implementation:

#import "TimeKeeper.h"

static mach_timebase_info_data_t timeBase;

@implementation TimeKeeper

+ (void) initialize
{
    (void) mach_timebase_info( &timeBase );
}

+ (id) timer
{
    return [[[self class] alloc] init];
}

- (id) init
{
    if( (self = [super init]) )
    {
        timeZero = mach_absolute_time();
    }
    return self;
}

- (void) start
{
    timeZero = mach_absolute_time();
}

- (uint64_t) elapsed
{
    return mach_absolute_time() - timeZero;
}

- (float) elapsedSeconds
{
    return ((float)(mach_absolute_time() - timeZero))
    * ((float)timeBase.numer) / ((float)timeBase.denom) / 1000000000.0f;
}

//TODO: Add a Pause method.

@end


标签: ios swift timer