Xcode / Objective-C:为什么NSTimer有时会变慢/波动?

时间:2013-09-03 01:07:00

标签: iphone objective-c performance nstimer

我正在开发一款iPhone游戏,我有NSTimer动画屏幕上的所有对象:

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

大多数情况下它运行得非常顺利,但有时我会发现事情变得缓慢或起伏不定。我有暂停和恢复功能,分别停止和启动计时器。当我暂停然后取消暂停时,似乎可以解决这个问题。

为什么会发生这种情况的任何想法?或者我该如何解决?

3 个答案:

答案 0 :(得分:6)

  

为什么会发生这种情况的任何想法?

简短的回答是,在您的情况下,您正在基于非同步推送模型执行操作(动画),并且您使用的机制不适合您正在执行的任务。

附加说明:

  • NSTimer分辨率较低。
  • 您的工作是在主运行循环上执行的。计时器可能不会在您预期时触发,因为在该线程上的工作正在进行时可能会花费很多时间,从而阻止计时器触发。其他系统活动甚至流程中的线程都可以使这种变化更大。
  • 不同步的更新可能会导致大量不必要的工作或不连贯,因为您在回调中执行的更新会在发生后的某个时间发布。这会为更新的计时准确性带来更多变化。当更新未同步时,很容易丢弃帧或执行大量不必要的工作。在优化绘图之后,此成本可能不会很明显。

NSTimer文档(强调我的):

  

计时器不是实时机制;只有当添加了计时器的其中一个运行循环模式正在运行并且能够检查计时器的触发时间是否已经过去时,它才会触发。由于典型的运行循环管理各种输入源,定时器的时间间隔的有效分辨率被限制在大约50-100毫秒。如果在长时间标注期间或在运行循环处于不监视计时器的模式下发生计时器的触发时间,则计时器在下次运行循环检查计时器之前不会触发。因此,计时器可能发生的实际时间可能是计划点火时间后的一段很长时间

如果您准备优化绘图,解决问题的最佳方法是使用CADisplayLink,正如其他人所提到的那样。 CADisplayLink是iOS上的一个特殊“计时器”,它以屏幕刷新率的(有能力)划分执行回调消息。这允许您将动画更新与屏幕更新同步。此回调在主线程上执行。注意:在OS X上,此工具不太方便,可能存在多个显示(CVDisplayLink)。

因此,您可以从创建显示链接开始,在其回调中,执行处理动画和绘制相关任务的工作(例如,执行任何必要的-setNeedsDisplayInRect:更新)。确保您的工作非常快,并且您的渲染也很快 - 那么您应该能够实现高帧速率。避免此回调中的慢速操作(例如文件io和网络请求)。

最后一点:我倾向于将我的计时器同步回调集群到一个Meta-Callback中,而不是在运行循环上安装许多计时器(例如以不同的频率运行)。如果您的实现可以快速确定当前要执行的更新,那么这可能会显着减少您安装的计时器数量(至少一个)。

答案 1 :(得分:2)

NSTimers无法保证以您要求的速度运行。如果您正在开发游戏,则应调查CADisplayLink,它实现与NSTimer大致相同的界面但是每次重画时都会触发。

您可以像计时器一样设置显示链接,只需不必指定刷新周期。

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

最重要的是,你应该衡量自上次迭代以来经过的时间。不要依赖它恰好是1/30或1/60秒。如果您有这样的代码

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

将其更改为此

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

如果出于游戏之外的其他原因需要高精度计时器,请参阅Technical Note TN2169

答案 2 :(得分:1)

正如Hot Licks建议的那样,30赫兹相当快,所以根据你在moveEverything内所做的事情,你可能只会压倒手机的处理器。您没有显示moveEverything,因此我们无法对内部的内容进行评论......但是,您可能需要努力提高该方法的效率,或者降低计时器触发的速度。< / p>

此外,您的NSTimer有时可能根本没有触发,因为尚未为所有正确的运行循环模式添加计时器

例如,当滚动视图滚动(性能密集型操作)时,UI处于跟踪模式。默认情况下,您创建计时器的方式,在跟踪模式下计时器不会触发。您可以更改此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];