CADisplayLink running lower frame rate on iOS5.1

2019-03-20 17:07发布

I'm using CADisplayLink in my iPhone app.

Here is the relevant code:

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

onTick is thus called every frame of 30FPS (1/30th of a second). This works GREAT on iOS6+ - does exactly what I need. However, when I ran my app on an iPhone 4s running iOS5.1, the onTick method ran slightly slower than with the iOS6 counterpart. Almost like it was running it 29FPS. After a little bit, it was out of sync with the iOS6 iPhone 5.

The code in the onTick method is not time consuming (that was one of my thoughts...), and it's not the iPhone because the app runs fine on an iPhone 4s running iOS6.

Does CADisplayLink function differently in iOS5.1? Any possible workarounds/solutions?

3条回答
倾城 Initia
2楼-- · 2019-03-20 17:20

I can't speak to the iOS 5.x v 6.x differences, but when I use CADisplayLink, I never hard code stuff like "move x pixels/points" every iteration, but rather I look at the timestamp (or more accurately, the delta between my initial timestamp and the current timestamp) and calculate the location based upon how much time has elapsed, not by how many frames have passed. That way, frame rates don't affect the speed of the motion, but rather just the smoothness of it. (And the difference between 30 and 29 is likely to be indistinguishable.)

To quote from the CADisplayLink Class Reference:

Once the display link is associated with a run loop, the selector on the target is called when the screen’s contents need to be updated. The target can read the display link’s timestamp property to retrieve the time that the previous frame was displayed. For example, an application that displays movies might use the timestamp to calculate which video frame will be displayed next. An application that performs its own animations might use the timestamp to determine where and how displayed objects appear in the upcoming frame. The duration property provides the amount of time between frames. You can use this value in your application to calculate the frame rate of the display, the approximate time that the next frame will be displayed, and to adjust the drawing behavior so that the next frame is prepared in time to be displayed.


As a random example, here I'm animating a UIBezierPath using the number of seconds that have elapsed as a parameter.

Or, alternatively, if you're dealing with a sequence of UIImage frames, you could calculate the frame number as follows:

@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
}

Or, better yet, to avoid risking losing a frame, go ahead and let this run at 60 fps and just determine if the frame needs updating and that way you'll reduce the risk of dropping a frame.

- (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;
    }
}

But frequently, frame numbers aren't needed at all. For example, to move a UIView in a circle, you might do something like:

- (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);
}

By the way, if you use Instruments to measure frame rate, it can seem slower than it really will be on a device. To matt's comment, for accurate frame rates, you should measure it programmatically on an actual device with a release build.

查看更多
Emotional °昔
3楼-- · 2019-03-20 17:40

Basic Swift version of the other answers (minus the animation code)

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))
    }
}

Usage

let watch = BasicStopwatch()
watch.begin()
查看更多
唯我独甜
4楼-- · 2019-03-20 17:43

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)

查看更多
登录 后发表回答