6

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

以下是相关代码:

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

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

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

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

4

3 回答 3

19

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

引用CADisplayLink 类参考

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


作为一个随机示例,这里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;
    }
}

但通常,根本不需要帧号。例如,要将 a 移动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 来测量帧速率,它看起来可能比在设备上实际运行时要慢。对于马特的评论,为了获得准确的帧速率,您应该在具有发布版本的实际设备上以编程方式对其进行测量。

于 2013-02-19T16:28:45.497 回答
3

Rob's answer is exactly right. You've no business worrying about the frame rate of CADisplayLink; in fact, you mustn't even expect that the timer will fire with anything like regularity. Your job is to divide up the desired animation in accordance with the desired time scale, and draw the frame you actually get each time the timer fires by adding up the accumulated timestamps.

Here is sample code from my book:

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;

In that code, the _frame value runs between 0 (we are just starting the animation) and 1 (we have finished the animation), and in the middle I just do whatever this particular situation requires to draw that frame. To make the animation take longer or shorter, just multiply a scale factor when setting the _frame ivar.

Also note that you must never test in the Simulator, as the results are utterly meaningless. Only the device runs CADisplayLink properly.

(Example comes from here: http://www.apeth.com/iOSBook/ch17.html#_cifilter_transitions)

于 2013-02-19T19:14:06.357 回答
0

其他答案的基本 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()
于 2018-12-14T07:47:34.480 回答