Xcode / Objective-C: Why is NSTimer sometimes slow / choppy?

I am developing an iPhone game and I have NSTimer which animates all 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 works very smoothly, but sometimes I see things moving slower or intermittently. I have pause and resume functions that stop and start timers accordingly. When I stop, I don’t stop; it seems that this corrects sloppiness.

Any ideas why this is happening? or how can i fix this?

+4
source share
3 answers

Any ideas why this is happening?

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

Additional notes:

  • NSTimer has a low resolution.
  • Your work is done in the main run loop. The timer may not fire when you expect it, because a lot of time can be spent while working on this thread, which blocks your timer from being triggered. Other system actions or even threads in your process can make this option even bigger.
  • Unsynchronized updates can lead to a lot of unnecessary work or variability, because the updates that you perform in your callback are published some time after they appear. This adds even more changes to the accuracy of synchronization of your updates. It’s easy to finish dropping frames or do a significant amount of unnecessary work when updates aren't syncing. This cost may not appear until your drawing is optimized.

From NSTimer docs (highlighting mine):

A timer is not a real-time mechanism ; it only fires when one of the run cycle modes to which the timer has been added is running, and it can check whether the timers have elapsed. Due to various input sources, a typical trigger cycle is controlled, the effective resolution of the time interval for the timer is limited to about 50-100 milliseconds . If, during a long callout, timers are triggered or when the cycle cycle is in a mode that the timer does not control, the timer does not work until the next timer test cycle. Thus, the actual time during which the timer can be triggered can be a significant period of time after the planned firing time .

The best way to fix the problem if you are ready to optimize your drawing as well is to use CADisplayLink , as others have mentioned. CADisplayLink is a special β€œtimer” on iOS that executes your feedback message when (able) sharing the screen refresh rate. This allows you to synchronize animation updates with screen updates. This callback is executed in the main thread. Note. This object is not very convenient for OS X, where there may be several displays ( CVDisplayLink ).

So, you can start by creating a link to the image and in your callback, do the work that will concern your animations and related tasks (for example, perform any necessary updates -setNeedsDisplayInRect: . Make sure your work is very fast, and that your rendering is also fast - then you should be able to achieve high frame rates. Avoid slow operations in this callback (like io file and network requests).

One final note: I try to copy my timer-synchronized callbacks into a single meta-callback, instead of setting a lot of timers on the startup loops (for example, running at different frequencies). If your implementations can quickly determine what updates to do at the moment, this can significantly reduce the number of timers you set (up to one).

+7
source

NSTimers is not guaranteed to execute at your exact rate. If you are developing a game, you should investigate CADisplayLink , which implements roughly the same interface as NSTimer, but runs every time the screen is redrawn.

You configure the link to the image in the same way as your timer, only you do not need to specify the update period.

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

First of all, you must measure the time elapsed since the last iteration. Do not rely on being exactly 1/30 or 1/60 of a second. If you have code like this

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

change it to

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

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

+2
source

As Hot Licks suggested, 30 Hz is fast enough, so depending on what you do inside moveEverything , you can just crush your phone's processor. You do not show moveEverything , so we cannot comment on what's inside ... but you may have to work to make this method more efficient or slow down the speed at which your timer fires.

In addition, it is possible that your NSTimer simply does not fire from time to time, because a timer has not been added for all the correct startup cycle modes.

For example, when scrolling is scrolled (heavy work), the user interface is in tracking mode. By default, as you created your timer, the timer does not work in tracking mode. You can change this 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]; 
+1
source

Source: https://habr.com/ru/post/1500222/


All Articles