前言
@interface NSTimer : NSObject
@interface CADisplayLink : NSObject
- 作用
- 在指定的时间执行指定的任务。
- 每隔一段时间执行指定的任务。
1、定时器的创建
1.1 NSTimer 定时器
当定时器创建完(不用 scheduled 的,添加到 runloop 中)后,该定时器将在初始化时指定的 ti 秒后自动触发。如果 NSTimer 的触发时间到的时候,runloop 在阻塞状态,触发时间就会推迟到下一个 runloop 周期。
-
1、scheduled 方式
- 创建并启动定时器。
- 默认将时钟以 NSDefaultRunLoopMode 模式添加到运行循环。
-
发生用户交互的时候,时钟会被暂停。
/* + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo; 参数: TimeInterval:触发时间,单位秒 target:定时起触发对象 selector:定时器响应方法 userInfo:用户信息 repeats:是否重复执行,YES 每个指定的时间重复执行,NO 只执行一次 */ // 创建并启动定时器 NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(updateTimer:) userInfo:nil repeats:YES];
-
2、timer 方式
- 创建定时器,添加到运行循环后启动定时器。
-
将时钟以指定的模式添加到运行循环。
/* + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo; - (void)addTimer:(NSTimer *)timer forMode:(NSString *)mode; NSDefaultRunLoopMode:发生用户交互的时候,时钟会被暂停。时钟,网络。 NSRunLoopCommonModes:发生用户交互的时候,时钟仍然会触发,如果时钟触发方法非常耗时, 使用此方式时用户操作会造成非常严重的卡顿。用户交互,响应级别高。 */ // 创建定时器 NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(updateTimer:) userInfo:nil repeats:YES]; // 将定时器添加到运行循环 [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
1.2 CADisplayLink 定时器
-
1、CADisplayLink
CADisplayLink 是一个能让我们以和屏幕刷新率相同的频率将内容画到屏幕上的定时器。我们在应用中创建一个新的 CADisplayLink 对象,把它添加到一个 runloop 中,并给它提供一个 target 和 selector 在屏幕刷新的时候调用。
一但 CADisplayLink 以特定的模式注册到 runloop 之后,每当屏幕需要刷新的时候,runloop 就会调用 CADisplayLink 绑定的 target 上的 selector,这时 target 可以读到 CADisplayLink 的每次调用的时间戳,用来准备下一帧显示需要的数据。例如一个视频应用使用时间戳来计算下一帧要显示的视频数据。在 UI 做动画的过程中,需要通过时间戳来计算 UI 对象在动画的下一帧要更新的大小等等。
在添加进 runloop 的时候我们应该选用高一些的优先级,来保证动画的平滑。可以设想一下,我们在动画的过程中,runloop 被添加进来了一个高优先级的任务,那么,下一次的调用就会被暂停转而先去执行高优先级的任务,然后在接着执行 CADisplayLink 的调用,从而造成动画过程的卡顿,使动画不流畅。
另外 CADisplayLink 不能被继承。
-
属性:
duration :提供了每帧之间的时间,也就是屏幕每次刷新之间的的时间。我们可以使用这个时间来计算出下一帧要显示 的 UI的数值。但是 duration 只是个大概的时间,如果 CPU 忙于其它计算,就没法保证以相同的频率 执行屏幕的绘制操作,这样会跳过几次调用回调方法的机会。 frameInterval :可读可写的 NSInteger 型值,标识间隔多少帧调用一次 selector 方法,默认值是 1,即每帧都调 用一次。如果每帧都调用一次的话,对于 iOS 设备来说那刷新频率就是 60HZ 也就是每秒 60 次,如果 将 frameInterval 设为 2 那么就会两帧调用一次,也就是变成了每秒刷新 30 次。 pause :控制 CADisplayLink 的运行。当我们想结束一个 CADisplayLink 的时候,应该调用 `- (void)invalidate`从 runloop 中删除并删除之前绑定的 target 跟 selector。
-
2、CADisplayLink 与 NSTimer 有什么不同
- iOS 设备的屏幕刷新频率是固定的,CADisplayLink 在正常情况下会在每次刷新结束都被调用,精确度相当高。
- NSTimer 的精确度就显得低了点,比如 NSTimer 的触发时间到的时候,runloop 如果在阻塞状态,触发时间就会推迟到下一个 runloop 周期。并且 NSTimer 新增了 tolerance 属性,让用户可以设置可以容忍的触发的时间的延迟范围。
- CADisplayLink 使用场合相对专一,适合做 UI 的不停重绘,比如自定义动画引擎或者视频播放的渲染。NSTimer 的使用范围要广泛的多,各种需要单次或者循环定时处理的任务都可以使用。在 UI 相关的动画或者显示内容使用 CADisplayLink 比起用 NSTimer 的好处就是我们不需要再格外关心屏幕的刷新频率了,因为它本身就是跟屏幕刷新同步的。
-
3、CADisplayLink 使用示例
@property (nonatomic, strong) CADisplayLink *link; // 创建定时器 self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(angleChange)]; [self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; // 开启定时器,默认 self.link.paused = NO; // 暂停定时器 self.link.paused = YES; // 定时器触发事件处理 - (void)angleChange { }
@property (nonatomic, strong) CADisplayLink *displayLink; self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateTextColor)]; self.displayLink.paused = YES; [self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; - (void)updateTextColor { } - (void)startAnimation { self.beginTime = CACurrentMediaTime(); self.displayLink.paused = NO; } - (void)stopAnimation { self.displayLink.paused = YES; [self.displayLink invalidate]; self.displayLink = nil; }
-
4、给非 UI 对象添加动画效果
- 我们知道动画效果就是一个属性的线性变化,比如 UIView 动画的 EasyIn EasyOut 。通过数值按照不同速率的变化我们能生成更接近真实世界的动画效果。我们也可以利用这个特性来使一些其他属性按照我们期望的曲线变化。比如当播放视频时关掉视频的声音我可以通过 CADisplayLink 来实现一个 EasyOut 的渐出效果:先快速的降低音量,在慢慢的渐变到静音。
-
5、注意
- 通常来讲:iOS 设备的刷新频率事 60HZ 也就是每秒 60 次。那么每一次刷新的时间就是 1/60 秒 大概 16.7 毫秒。当我们的 frameInterval 值为 1 的时候我们需要保证的是 CADisplayLink 调用的 target 的函数计算时间不应该大于 16.7 否则就会出现严重的丢帧现象。
在 Mac 应用中我们使用的不是 CADisplayLink 而是 CVDisplayLink 它是基于 C 接口的用起来配置有些麻烦但是用起来还是很简单的。
2、定时器的启动与关闭
// 启动定时器
[timer setFireDate:[NSDate distantPast]];
// 暂停定时器
[timer setFireDate:[NSDate distantFuture]];
// 关闭定时器,永久关闭定时器
[timer invalidate];
3、子线程定时器的创建
-
在子线程创建定时器时,需要手动开启子线程的运行循环。
dispatch_async(dispatch_get_global_queue(0, 0), ^{ // 在子线程创建定时器 /* scheduled 或 timer 方式创建 */ self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(updateTimer:) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode]; // 启动子线程的运行循环 /* 这句代码就是一个死循环!如果不停止运行循环,不会执行添加到此句之后的任何代码 */ CFRunLoopRun(); // 停止子线程运行循环之前,不会执行添加到此处的任何代码 });
// 定时器执行操作方法 - (void)updateTimer { static int num = 0; num++; // 满足条件后,停止当前的运行循环 if (num == 8) { // 停止当前的运行循环 /* 一旦停止了运行循环,后续代码能够执行,执行完毕后,线程被自动销毁 */ CFRunLoopStop(CFRunLoopGetCurrent()); } }
4、定时任务
-
1)performSelector
// 延时调用 /* 1.5 秒后自动调用 self 的 hideHUD 方法 */ [self performSelector:@selector(hideHUD) withObject:nil afterDelay:1.5]; // 取消延时调用 [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(hideHUD) object:nil];
-
2)GCD
// 多线程 /* 1.5 秒后自动执行 block 里面的代码 */ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ self.hud.alpha = 0.0; });
-
3)NSTimer
// 定时器 /* 1.5 秒后自动调用 self 的 hideHUD 方法 */ [NSTimer scheduledTimerWithTimeInterval:1.5 target:self selector:@selector(hideHUD) userInfo:nil repeats:NO];