我在iPhone应用中使用CADisplayLink
。
以下是相关代码:
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 4上运行良好。
CADisplayLink
中iOS5.1
的功能是否有所不同?任何可能的解决方法/解决方案?
答案 0 :(得分:19)
我无法谈论iOS 5.xv 6.x的差异,但是当我使用CADisplayLink
时,我从来没有硬编码像每次迭代“移动x像素/点”这样的东西,而是我看看timestamp
(或更确切地说,我的初始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测量帧速率,它看起来会比它在设备上的速度慢。对于matt的评论,对于准确的帧速率,您应该在具有发布版本的实际设备上以编程方式进行测量。
答案 1 :(得分:3)
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)
答案 2 :(得分: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()