CADisplayLink работает на более низкой частоте кадров на iOS5.1

Я использую CADisplayLink в моем приложении для iPhone.

Вот соответствующий код:

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+ - делает именно то, что мне нужно. Однако когда я запускал свое приложение на iPhone 4s под управлением iOS5.1, метод onTick работал немного медленнее, чем с аналогом iOS6. Почти как это было запущено 29FPS, Через некоторое время он был не синхронизирован с iOS6 iPhone 5.

Код в методе onTick не занимает много времени (это было одной из моих мыслей...), и это не iPhone, потому что приложение отлично работает на iPhone 4s под управлением iOS6.

Есть ли CADisplayLink функционировать по-разному в iOS5.1? Любые возможные обходные пути / решения?

2 ответа

Решение

Я не могу говорить о различиях iOS 5.x v 6.x, но когда я использую CADisplayLinkЯ никогда не пишу такие вещи, как "перемещать х пикселей / точки" на каждой итерации, а скорее смотрю на timestamp (или, точнее, дельта между моим начальным timestamp и текущий timestamp) и рассчитать местоположение на основе того, сколько времени прошло, а не сколько кадров прошло. Таким образом, частота кадров не влияет на скорость движения, а только на его плавность. (И разница между 30 и 29, вероятно, будет неразличимой.)

Чтобы процитировать цитату из класса CADisplayLink:

Как только ссылка на отображение связана с циклом выполнения, селектор на цели вызывается, когда необходимо обновить содержимое экрана. Цель может считывать свойство метки времени отображаемой ссылки, чтобы получить время, когда был отображен предыдущий кадр. Например, приложение, которое отображает фильмы, может использовать метку времени, чтобы вычислить, какой видеокадр будет отображаться следующим. Приложение, которое выполняет свои собственные анимации, может использовать метку времени, чтобы определить, где и как отображаются объекты в следующем кадре. Свойство duration указывает количество времени между кадрами. Вы можете использовать это значение в своем приложении, чтобы рассчитать частоту кадров дисплея, приблизительное время, в течение которого будет отображаться следующий кадр, и отрегулировать поведение при рисовании так, чтобы следующий кадр был подготовлен ко времени отображения.


В качестве случайного примера, здесь я оживляю 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 кадров в секунду и просто определите, нужно ли обновлять кадр, и таким образом вы уменьшите риск потери кадра.

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

Кстати, если вы используете инструменты для измерения частоты кадров, она может показаться медленнее, чем на устройстве. Чтобы комментировать Мэтта, для точной частоты кадров, вы должны измерить его программно на реальном устройстве с выпуском сборки.

Ответ Роба совершенно правильный. Вы не должны беспокоиться о частоте кадров 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 Ивар.

Также обратите внимание, что вы никогда не должны тестировать в симуляторе, так как результаты совершенно бессмысленны. Только устройство правильно запускает CADisplayLink.

(Пример можно найти здесь: http://www.apeth.com/iOSBook/ch17.html)

Базовая версия 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()
Другие вопросы по тегам