Xcode / Objective-C: Why is NSTimer sometimes slow

2019-01-19 06:27发布

I am developing an iPhone game and I have an NSTimer that animates all of the objects on the screen:

EverythingTimer = [NSTimer scheduledTimerWithTimeInterval:1.0/30.0 target:self selector:@selector(moveEverything) userInfo:nil repeats:YES];

Most of the time it runs very smoothly, but sometimes I see things move slower or choppy. I have pause and resume functions that stop and start the timers respectively. When I pause then unpause, it seems to fix the choppiness.

Any ideas to why this is happening? or How can i fix it?

3条回答
冷血范
2楼-- · 2019-01-19 07:02

Any ideas to why this is happening?

The short answer is that in your case, you are performing actions (animations) based on an unsynchronized push model, and the mechanism you are using is not suited for the task you are performing.

Additional notes:

  • NSTimer has a low resolution.
  • Your work is performed on the main run loop. The timer may not fire when you expect it to because much time may be spent while work on that thread is underway, which blocks your timer from firing. Other system activities or even threads in your process can make this variation even greater.
  • Unsynchronized updates can result in a lot of unnecessary work, or choppiness because the updates you perform in your callback(s) are posted at some time after they occur. This adds even more variation to the timing accuracy of your updates. It's easy to end up dropping frames or perform significant amounts of unnecessary work when the updates are not synchronized. This cost may not be evident until after your drawing is optimized.

Out of the NSTimer docs (emphasis mine):

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. If a timer’s firing time occurs during a long callout or while the run loop is in a mode that is not monitoring the timer, the timer does not fire until the next time the run loop checks the timer. Therefore, the actual time at which the timer fires potentially can be a significant period of time after the scheduled firing time.

The best way to remedy the issue, if you are prepared to also optimize your drawing -- is to use CADisplayLink, as others have mentioned. The CADisplayLink is a special 'timer' on iOS which performs your callback message at a (capable) division of the screen refresh rate. This allows you to synchronize your animation updates with the screen updates. This callback is performed on the main thread. Note: This facility is not so convenient on OS X, where multiple displays may exist (CVDisplayLink).

So, you can start by creating a display link and in its callback, perform work which would deal with your animations and drawing related tasks (e.g. perform any necessary -setNeedsDisplayInRect: updates). Make sure your work is very quick, and that your rendering is also quick -- then you should be able achieve a high frame rate. Avoid slow operations in this callback (e.g. file io and network requests).

One final note: I tend to cluster my timer sync callbacks into one Meta-Callback, rather than installing many timers on run loops (e.g. running at various frequencies). If your implementations can quickly determine which updates to do at the moment, then this could significantly reduce the number of timers you install (to exactly one).

查看更多
▲ chillily
3楼-- · 2019-01-19 07:02

NSTimers are not guaranteed to run at exactly the rate you asked for. If you're developing a game, you should investigate CADisplayLink, which implements roughly the same interface as NSTimer but is fired at every screen redraw.

You set up a display link just like your timer, only you don't have to specify a refresh period.

CADisplayLink* displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(moveEverything)];
[displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];

Above all you should measure the amount of time that has passed since the last iteration. Don't rely on it being exactly 1/30th or 1/60th of a second. If you have code like this

thing.origin.x += thing.velocity * 1/30.0;

change it to this

NSTimeInterval timeSince = [displayLink duration];
thing.origin.x += thing.velocity * timeSince;

If you need a high-precision timer for a reason other than making a game, see Technical Note TN2169.

查看更多
Juvenile、少年°
4楼-- · 2019-01-19 07:29

As Hot Licks suggested, 30 Hz is reasonably fast, so depending on what you're doing inside moveEverything, you might simply be overwhelming your phone's processor. You don't show moveEverything, so we can't really comment on what's inside ... but, you may need to work at making that method more efficient, or slowing down the rate at which your timer fires.

Also, it's possible that your NSTimer is simply not firing at times, because the timer has not been added for all the right run loop modes.

For example, when a scroll view scrolls (a performance intensive operation), the UI is in tracking mode. By default, the way you've created your timer, the timer will not fire when in tracking mode. You can change this by using something like this:

EverythingTimer = [NSTimer scheduledTimerWithTimeInterval:1.0/30.0 
                                                   target:self 
                                                 selector:@selector(moveEverything) 
                                                 userInfo:nil 
                                                  repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:EverythingTimer forMode:NSRunLoopCommonModes];
查看更多
登录 后发表回答