【问题标题】:CADisplayLink running lower frame rate on iOS5.1CADisplayLink 在 iOS5.1 上运行较低的帧率
【发布时间】:2013-02-19 15:51:19
【问题描述】:

我在我的 iPhone 应用程序中使用 CADisplayLink

以下是相关代码:

SMPTELink = [CADisplayLink displayLinkWithTarget:self selector:@selector(onTick)];
SMPTELink.frameInterval = 2;//30fps 60/n = fps
[SMPTELink addToRunLoop:[NSRunLoop mainRunLoop]
                    forMode:NSDefaultRunLoopMode];

onTick 因此在30FPS(1/30 秒)的每一帧中被调用。这在iOS6+ 上效果很好 - 正是我需要的。但是,当我在运行 iOS5.1 的 iPhone 4s 上运行我的应用程序时,onTick 方法的运行速度比 iOS6 对应的方法稍慢。几乎就像它正在运行它29FPS。一点点之后,就和iOS6 iPhone 5不同步了。

onTick 方法中的代码并不耗时(这是我的想法之一……),它不是 iPhone,因为该应用程序在运行 iOS6 的 iPhone 4s 上运行良好。

CADisplayLinkiOS5.1 中的功能是否不同?任何可能的解决方法/解决方案?

【问题讨论】:

    标签: ios objective-c cadisplaylink


    【解决方案1】:

    我无法谈论 iOS 5.x 与 6.x 的差异,但是当我使用 CADisplayLink 时,我从不会在每次迭代时对诸如“移动 x 像素/点”之类的东西进行硬编码,而是查看 @ 987654326@(或更准确地说,我的初始timestamp 和当前timestamp 之间的增量)并根据经过的时间而不是经过的帧数来计算位置。这样,帧速率不会影响运动的速度,而只会影响运动的平滑度。 (而且 30 和 29 之间的差异可能无法区分。)

    引用CADisplayLink Class Reference

    一旦显示链接与运行循环相关联,当屏幕内容需要更新时,就会调用目标上的选择器。目标可以读取显示链接的timestamp 属性来检索上一帧的显示时间。例如,显示电影的应用程序可能会使用时间戳来计算接下来将显示哪个视频帧。执行自己的动画的应用程序可能会使用时间戳来确定显示的对象在即将到来的帧中出现的位置和方式。 duration 属性提供帧之间的时间量。您可以在应用程序中使用此值来计算显示的帧速率、显示下一帧的大致时间,并调整绘图行为以便及时准备好下一帧以显示。


    作为一个随机示例,here 我正在使用经过的秒数作为参数为UIBezierPath 设置动画。

    或者,或者,如果您正在处理UIImage 帧序列,您可以按如下方式计算帧号:

    @property (nonatomic) CFTimeInterval firstTimestamp;
    
    - (void)handleDisplayLink:(CADisplayLink *)displayLink
    {
        if (!self.firstTimestamp)
            self.firstTimestamp = displayLink.timestamp;
    
        CFTimeInterval elapsed = (displayLink.timestamp - self.firstTimestamp);
    
        NSInteger frameNumber = (NSInteger)(elapsed * kFramesPerSecond) % kMaxNumberOfFrames;
    
        // now do whatever you want with this frame number
    }
    

    或者,更好的是,为了避免丢失帧的风险,继续让它以 60 fps 运行,然后确定帧是否需要更新,这样可以降低丢失帧的风险。

    - (void)handleDisplayLink:(CADisplayLink *)displayLink
    {
        if (!self.firstTimestamp)
            self.firstTimestamp = displayLink.timestamp;
    
        CFTimeInterval elapsed = (displayLink.timestamp - self.firstTimestamp);
    
        NSInteger frameNumber = (NSInteger)(elapsed * kFramesPerSecond) % kMaxNumberOfFrames;
    
        if (frameNumber != self.lastFrame)
        {
            // do whatever you want with this frame number
    
            ... 
    
            // now update the "lastFrame" number property
    
            self.lastFrame = frameNumber;
        }
    }
    

    但通常,根本不需要帧号。例如,要将UIView 移动一个圆圈,您可以执行以下操作:

    - (void)handleDisplayLink:(CADisplayLink *)displayLink
    {
        if (!self.firstTimestamp)
            self.firstTimestamp = displayLink.timestamp;
    
        CFTimeInterval elapsed = (displayLink.timestamp - self.firstTimestamp);
    
        self.animatedView.center = [self centerAtElapsed:elapsed];
    }
    
    - (CGPoint)centerAtElapsed:(CFTimeInterval)elapsed
    {
        CGFloat radius = self.view.bounds.size.width / 2.0;
    
        return CGPointMake(radius + sin(elapsed) * radius,
                           radius + cos(elapsed) * radius);
    }
    

    顺便说一句,如果您使用 Instruments 来测量帧速率,它看起来可能比在设备上实际运行时要慢。对于马特的评论,为了获得准确的帧速率,您应该在具有发布版本的实际设备上以编程方式对其进行测量。

    【讨论】:

    • 好主意!所以,在 onTick 方法上,我打上时间戳,然后在下一个上,我再次打上时间戳 - 然后比较看看我是否得到 0.03333 秒的差异?如果我不这样做怎么办?你能详细说明你的概念吗?
    • @objectiveccoder001 如果出于某种原因您绝对需要 30 fps,那么,是的,您可以这样做。请参阅我的代码示例,该示例可以根据经过的时间确定帧号。但是,许多动画不需要这样做,而是可以根据经过的时间计算新位置,并且没有理由将其限制为 30 fps(对比 29 对比 59 对比等)。
    • 太棒了!非常感谢。 kMaxNumberOfFrames 是什么?
    • 动画中的最大数量?如果没有最大值怎么办?
    • 太棒了。我使用了您的第二个代码示例(60fps)。完美运行。修复了我在使用 iOS5 时遇到的所有问题。我很感激! :)
    【解决方案2】:

    Rob 的回答完全正确。您不必担心 CADisplayLink 的帧速率;事实上,你甚至不能期望计时器会以任何规律性的方式触发。您的工作是根据所需的时间尺度划分所需的动画,并通过将累积的时间戳相加来绘制每次计时器触发时实际获得的帧。

    这是我书中的示例代码:

    if (self->_timestamp < 0.01) { // pick up and store first timestamp
        self->_timestamp = sender.timestamp;
        self->_frame = 0.0;
    } else { // calculate frame
        self->_frame = sender.timestamp - self->_timestamp;
    }
    sender.paused = YES; // defend against frame loss
    
    [_tran setValue:@(self->_frame) forKey:@"inputTime"];
    CGImageRef moi3 = [self->_con createCGImage:_tran.outputImage
                                       fromRect:_moiextent];
    self->_iv.image = [UIImage imageWithCGImage:moi3];
    CGImageRelease(moi3);
    
    if (self->_frame > 1.0) {
        [sender invalidate];
        self->_frame = 0.0;
        self->_timestamp = 0.0;
    }
    sender.paused = NO;
    

    在该代码中,_frame 值介于 0(我们刚刚开始动画)和 1(我们已经完成动画)之间,而在中间,我只是做这个特殊情况所需的任何事情来绘制该帧。要使动画时间更长或更短,只需在设置 _frame ivar 时乘以比例因子即可。

    还请注意,您绝不能在模拟器中进行测试,因为结果完全没有意义。只有设备正常运行 CADisplayLink。

    (示例来自这里:http://www.apeth.com/iOSBook/ch17.html#_cifilter_transitions

    【讨论】:

      【解决方案3】:

      其他答案的基本 Swift 版本(减去动画代码)

      class BasicStopwatch {
      
          var timer: CADisplayLink!
          var firstTimestamp: CFTimeInterval!
          var elapsedTime: TimeInterval = 0
      
          let formatter: DateFormatter = {
              let df = DateFormatter()
              df.dateFormat = "mm:ss.SS"
              return df
          }()
      
          func begin() {
              timer = CADisplayLink(target: self, selector: #selector(tick))
              timer.preferredFramesPerSecond = 10 // adjust as needed
              timer.add(to: .main, forMode: .common)
          }
      
          @objc func tick() {
              if (self.firstTimestamp == nil) {
                  print("Set first timestamp")
                  self.firstTimestamp = timer!.timestamp
                  return
              }
      
              elapsedTime = timer.timestamp - firstTimestamp
              /// Print raw elapsed time
              // print(elapsedTime)
      
              /// print elapsed time
              print(elapsedTimeAsString())
      
              /// If need to track frames
              // let totalFrames: Double = 20
              // let frameNumber = (elapsedTime * Double(timer!.preferredFramesPerSecond)).truncatingRemainder(dividingBy: totalFrames)
              // print("Frame ", frameNumber)
          }
      
          func elapsedTimeAsString() -> String {
              return formatter.string(from: Date(timeIntervalSinceReferenceDate: elapsedTime))
          }
      }
      

      用法

      let watch = BasicStopwatch()
      watch.begin()
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2017-11-16
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多