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