如何在Objective-C/iOS中创建一个准确的计时器事件?

时间:2021-08-22 06:29:53

I'm looking to create a countdown timer for SMPTE Timecode (HH:MM:SS:FF) on iOS. Basically, it's just a countdown timer with a resolution of 33.33333ms. I'm not so sure NSTimer is accurate enough to be counted on to fire events to create this timer. I would like to fire an event or call a piece of code every time this timer increments/decrements.

我想在iOS上为SMPTE Timecode (HH:MM:SS:FF)创建一个倒计时计时器。基本上,它只是一个分辨率为33.3333毫秒的倒计时计时器。我不太确定NSTimer是否准确到可以触发事件来创建这个计时器。每当这个计时器递增或递减时,我想要触发一个事件或调用一段代码。

I'm new to Objective-C so I'm looking for wisdom from the community. Someone has suggested the CADisplayLink class, looking for some expert advice.

我是Objective-C的新手,所以我在社区里寻找智慧。有人建议CADisplayLink类,寻找一些专家建议。

3 个解决方案

#1


13  

Try CADisplayLink. It fires at the refresh rate (60 fps).

CADisplayLink试试。它以刷新率(60 fps)启动。

CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(timerFired:)];
displayLink.frameInterval = 2;
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

This will fire every 2 frames, which is 30 times per seconds, which seems to be what you are after.

每两帧发射一次,每秒钟30次,这就是你想要的。

Note, that this is tied to video frame processing, so you need to do your work in the callback very quickly.

注意,这与视频帧处理有关,所以您需要非常快地在回调中完成工作。

#2


5  

You basically have no guarantees with either NSTimer or dispatch_after; they schedule code to triggered on the main thread, but if something else takes a long time to execute and blocks the main thread, your timer won't fire.

对于NSTimer或dispatch_after基本上没有保证;他们计划在主线程上触发代码,但是如果其他事情需要很长时间来执行和阻塞主线程,你的计时器就不会开火。

That said, you can easily avoid blocking the main thread (use only asynchronous I/O) and things should be pretty good.

也就是说,您可以很容易地避免阻塞主线程(只使用异步I/O),而且事情应该很好。

You don't say exactly what you need to do in the timer code, but if all you need to do is display a countdown, you should be fine as long as you compute the SMPTE time based on the system time, and not the number of seconds you think should have elapsed based on your timer interval. If you do that, you will almost certainly drift and get out of sync with the actual time. Instead, note your start time and then do all the math based on that:

你不要说什么您需要做的定时器代码,但如果所有你需要做的就是显示倒计时,你应该很好只要你计算SMPTE时根据系统时间,而不是你认为的秒数应该根据你的定时器间隔运行。如果你这样做,你几乎肯定会偏离正轨,与实际时间不同步。相反,记下你的开始时间,然后在此基础上做所有的计算:

// Setup
timerStartDate = [[NSDate alloc] init];
[NSTimer scheduledTimer...

- (void)timerDidFire:(NSTimer *)timer
{
    NSTImeInterval elapsed = [timerStartDate timeIntervalSinceNow];
    NSString *smtpeCode = [self formatSMTPEFromMilliseconds:elapsed];
    self.label.text = smtpeCode;
}

Now you will display the correct time code no matter how often the timer is fired. (If the timer doesn't fire often enough, the timer won't update, but when it updates it will be accurate. It will never get out of sync.)

现在,您将显示正确的时间代码,无论计时器被触发的频率有多高。(如果计时器不够频繁,计时器将不会更新,但当它更新时,它将是准确的。它永远不会失去同步。

If you use CADisplayLink, your method will be called as fast as the display updates. In other words, as fast as it would be useful, but no faster. If you're displaying the time, that's probably the way to go.

如果您使用CADisplayLink,您的方法将被调用的速度与显示更新的速度一样快。换句话说,尽可能快地使用它,但不能更快。如果你在展示时间,那很可能就是这样。

#3


2  

If you are targeting iOS 4+, you can use Grand Central Dispatch:

如果你的目标是iOS 4+,你可以使用Grand Central Dispatch:

// Set the time, '33333333' nanoseconds in the future (33.333333ms)
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 33333333);
// Schedule our code to run
dispatch_after(time, dispatch_get_main_queue(), ^{
    // your code to run here...
});

This will call that code after 33.333333ms. If is this going to be a loop sorta deal, you may want to use the dispatch_after_f function instead that uses a function pointer instead of a block:

这将调用33333333之后的代码。如果这将是一个循环sorta协议,您可能希望使用dispatch_after_f函数,而不是使用函数指针而不是块:

void DoWork(void *context);

void ScheduleWork() {
    // Set the time, '33333333' nanoseconds in the future (33.333333ms)
    dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 33333333);
    // Schedule our 'DoWork' function to run
    // Here I pass in NULL for the 'context', whatever you set that to will
    // get passed to the DoWork function
    dispatch_after_f(time, dispatch_get_main_queue(), NULL, &DoWork);
}

void DoWork(void *context) {
    // ...
    // Do your work here, updating an on screen counter or something
    // ...

    // Schedule our DoWork function again, maybe add an if statement
    // so it eventually stops
    ScheduleWork();
}

And then just call ScheduleWork(); when you want to start the timer. For a repeating loop, I personally think this is a little cleaner than the block method above, but for a one time task I definitely prefer the block method.

然后调用ScheduleWork();当你想要启动计时器时。对于重复循环,我个人认为这比上面的块方法更简洁一些,但是对于一次任务,我肯定更喜欢块方法。

See the Grand Central Dispatch docs for more info.

有关更多信息,请参阅*分发文档。

#1


13  

Try CADisplayLink. It fires at the refresh rate (60 fps).

CADisplayLink试试。它以刷新率(60 fps)启动。

CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(timerFired:)];
displayLink.frameInterval = 2;
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

This will fire every 2 frames, which is 30 times per seconds, which seems to be what you are after.

每两帧发射一次,每秒钟30次,这就是你想要的。

Note, that this is tied to video frame processing, so you need to do your work in the callback very quickly.

注意,这与视频帧处理有关,所以您需要非常快地在回调中完成工作。

#2


5  

You basically have no guarantees with either NSTimer or dispatch_after; they schedule code to triggered on the main thread, but if something else takes a long time to execute and blocks the main thread, your timer won't fire.

对于NSTimer或dispatch_after基本上没有保证;他们计划在主线程上触发代码,但是如果其他事情需要很长时间来执行和阻塞主线程,你的计时器就不会开火。

That said, you can easily avoid blocking the main thread (use only asynchronous I/O) and things should be pretty good.

也就是说,您可以很容易地避免阻塞主线程(只使用异步I/O),而且事情应该很好。

You don't say exactly what you need to do in the timer code, but if all you need to do is display a countdown, you should be fine as long as you compute the SMPTE time based on the system time, and not the number of seconds you think should have elapsed based on your timer interval. If you do that, you will almost certainly drift and get out of sync with the actual time. Instead, note your start time and then do all the math based on that:

你不要说什么您需要做的定时器代码,但如果所有你需要做的就是显示倒计时,你应该很好只要你计算SMPTE时根据系统时间,而不是你认为的秒数应该根据你的定时器间隔运行。如果你这样做,你几乎肯定会偏离正轨,与实际时间不同步。相反,记下你的开始时间,然后在此基础上做所有的计算:

// Setup
timerStartDate = [[NSDate alloc] init];
[NSTimer scheduledTimer...

- (void)timerDidFire:(NSTimer *)timer
{
    NSTImeInterval elapsed = [timerStartDate timeIntervalSinceNow];
    NSString *smtpeCode = [self formatSMTPEFromMilliseconds:elapsed];
    self.label.text = smtpeCode;
}

Now you will display the correct time code no matter how often the timer is fired. (If the timer doesn't fire often enough, the timer won't update, but when it updates it will be accurate. It will never get out of sync.)

现在,您将显示正确的时间代码,无论计时器被触发的频率有多高。(如果计时器不够频繁,计时器将不会更新,但当它更新时,它将是准确的。它永远不会失去同步。

If you use CADisplayLink, your method will be called as fast as the display updates. In other words, as fast as it would be useful, but no faster. If you're displaying the time, that's probably the way to go.

如果您使用CADisplayLink,您的方法将被调用的速度与显示更新的速度一样快。换句话说,尽可能快地使用它,但不能更快。如果你在展示时间,那很可能就是这样。

#3


2  

If you are targeting iOS 4+, you can use Grand Central Dispatch:

如果你的目标是iOS 4+,你可以使用Grand Central Dispatch:

// Set the time, '33333333' nanoseconds in the future (33.333333ms)
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 33333333);
// Schedule our code to run
dispatch_after(time, dispatch_get_main_queue(), ^{
    // your code to run here...
});

This will call that code after 33.333333ms. If is this going to be a loop sorta deal, you may want to use the dispatch_after_f function instead that uses a function pointer instead of a block:

这将调用33333333之后的代码。如果这将是一个循环sorta协议,您可能希望使用dispatch_after_f函数,而不是使用函数指针而不是块:

void DoWork(void *context);

void ScheduleWork() {
    // Set the time, '33333333' nanoseconds in the future (33.333333ms)
    dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 33333333);
    // Schedule our 'DoWork' function to run
    // Here I pass in NULL for the 'context', whatever you set that to will
    // get passed to the DoWork function
    dispatch_after_f(time, dispatch_get_main_queue(), NULL, &DoWork);
}

void DoWork(void *context) {
    // ...
    // Do your work here, updating an on screen counter or something
    // ...

    // Schedule our DoWork function again, maybe add an if statement
    // so it eventually stops
    ScheduleWork();
}

And then just call ScheduleWork(); when you want to start the timer. For a repeating loop, I personally think this is a little cleaner than the block method above, but for a one time task I definitely prefer the block method.

然后调用ScheduleWork();当你想要启动计时器时。对于重复循环,我个人认为这比上面的块方法更简洁一些,但是对于一次任务,我肯定更喜欢块方法。

See the Grand Central Dispatch docs for more info.

有关更多信息,请参阅*分发文档。