我在我的iPhone应用程序中使用CADisplayLink。
相关代码如下:
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+上运行得很好--这正是我所需要的。然而,当我在运行iOS6 5.1的iPhone 4s上运行我的应用程序时,onTick方法的运行速度比iOS6方法略慢。就像它在29FPS上运行一样。一段时间后,它与iOS6 iPhone 5不同步。
onTick方法中的代码并不耗时(这是我的想法之一……),而且它也不是iPhone,因为这个应用程序可以在运行iOS6的iPhone 4s上运行良好。
在iOS5.1中,CADisplayLink的功能是否有所不同?有什么可行的变通方法/解决方案吗?
发布于 2013-02-20 00:28:45
我不能说出iOS 5.x和6.x的区别,但是当我使用CADisplayLink时,我从来不会硬编码“移动x像素/点”这样的东西,而是查看timestamp (或者更准确地说,我的初始timestamp和当前timestamp之间的增量),并根据经过了多少时间来计算位置,而不是通过经过多少帧来计算位置。这样,帧速率不会影响运动的速度,而只会影响运动的平滑度。( 30和29之间的区别很可能是难以区分的。)
引用CADisplayLink Class Reference的话
一旦显示链接与运行循环相关联,当需要更新屏幕内容时,将调用目标上的选择器。目标可以读取显示链接的timestamp属性,以检索显示前一帧的时间。例如,显示电影的应用程序可能使用时间戳来计算下一个视频帧将被显示。执行其自己的动画的应用程序可以使用时间戳来确定所显示的对象在即将到来的帧中出现的位置和方式。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 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的评论,为了获得准确的帧率,您应该在具有发布版本的实际设备上以编程方式测量它。
发布于 2013-02-20 03:14:06
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)
发布于 2018-12-14 15:47:34
其他答案的Basic 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()https://stackoverflow.com/questions/14961581
复制相似问题