定时器 你真的会使用吗?
前言
定时器的使用是软件开发基础技能,用于延时执行或重复执行某些方法。
我相信大部分人接触 iOS 的定时器都是从这段代码开始的:
1 | [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(action:) userInfo:nil repeats:YES] |
但是你真的会用吗?
正文
iOS 定时器
首先来介绍 iOS 中的定时器
iOS 中的定时器大致分为这几类:
- NSTimer
- CADisplayLink
- GCD 定时器
NSTimer
使用方法
NSTime 定时器是我们比较常使用的定时器,比较常使用的方法有两种:
1 | + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo; |
这两种方法都是创建一个定时器,区别是用 timerWithTimeInterval:
方法创建的定时器需要手动加入 RunLoop 中。
1 | // 创建NSTimer对象 |
需要注意的是: UIScrollView
滑动时执行的是 UITrackingRunLoopMode
,NSDefaultRunLoopMode
被挂起,会导致定时器失效,等恢复为滑动结束时才恢复定时器。其原因可以查看我这篇《Objective-C RunLoop 详解》中的 “RunLoop 的 Mode“章节,有详细的介绍。
举个例子:
1 | - (void)startTimer{ |
将 timer
添加到 NSDefaultRunLoopMode 中,没 0.5 秒打印一次,然后滑动 UIScrollView
.
打印台输出:
可以看出在滑动 UIScrollView
时,定时器被暂停了。
所以如果需要定时器在 UIScrollView
拖动时也不影响的话,有两种解决方法
- timer 分别添加到
UITrackingRunLoopMode
和NSDefaultRunLoopMode
中
1 | [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; |
- 直接将 timer 添加到
NSRunLoopCommonModes
中:
1 | [[NSRunLoop mainRunLoop] addTimer:timer forMode: NSRunLoopCommonModes]; |
但并不是都 timer 所有的需要在滑动 UIScrollView
时继续执行,比如使用 NSTimer 完成的帧动画,滑动 UIScrollView
时就可以停止帧动画,保证滑动的流程性。
若没有特殊要求的话,一般使用第二种方法创建完 timer,会自动添加到 NSDefaultRunLoopMode
中去执行,也是平时最常用的方法。
1 | NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(action:) userInfo:nil repeats:YES]; |
参数:
TimeInterval
:延时时间
target
: 目标对象,一般就是 self
本身
selector
: 执行方法
userInfo
: 传入信息
repeats
: 是否重复执行
以上创建的定时器,若 repeats
参数设为 NO
,执行一次后就会被释放掉;
若 repeats
参数设为 YES
重复执行时,必须手动关闭,否则定时器不会释放 (停止)。
释放方法:
1 | // 停止定时器 |
实际开发中,我们会将 NSTimer
对象设置为属性,这样方便释放。
iOS10.0 推出了两个新的 API,与上面的方法相比,selector
换成 Block 回调以、减少传入的参数 (那几个参数真是鸡肋)。不过开发中一般需要适配低版本,还是尽量使用上面的方法吧。
1 | + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0)); |
特点
必须加入 Runloop
上面不管使用哪种方法,实际最后都会加入 RunLoop 中执行,区别就在于是否手动加入而已。
存在延迟
不管是一次性的还是周期性的 timer 的实际触发事件的时间,都会与所加入的 RunLoop 和 RunLoop Mode 有关,如果此 RunLoop 正在执行一个连续性的运算,timer 就会被延时出发。重复性的 timer 遇到这种情况,如果延迟超过了一个周期,则会在延时结束后立刻执行,并按照之前指定的周期继续执行,这个延迟时间大概为 50-100 毫秒.
所以 NSTimer 不是绝对准确的,而且中间耗时或阻塞错过下一个点,那么下一个点就 pass 过去了.
UIScrollView 滑动会暂停计时
添加到
NSDefaultRunLoopMode
的timer
在UIScrollView
滑动时会暂停,若不想被UIScrollView
滑动影响,需要将timer
添加再到UITrackingRunLoopMode
或 直接添加到NSRunLoopCommonModes
中
CADisplayLink
CADisplayLink 官方介绍:
A CADisplayLink object is a timer object that allows your application to synchronize its drawing to the refresh rate of the display
CADisplayLink 对象是一个和屏幕刷新率同步的定时器对象。每当屏幕显示内容刷新结束的时候,runloop 就会向 CADisplayLink 指定的 target
发送一次指定的 selector
消息, CADisplayLink 类对应的 selector
就会被调用一次。
从原理上可以看出,CADisplayLink 适合做界面的不停重绘,比如视频播放的时候需要不停地获取下一帧用于界面渲染,或者做动画。
使用方法
创建:
1 | @property (nonatomic, strong) CADisplayLink *displayLink; |
释放方法:
1 | [self.displayLink invalidate]; |
double delayInSeconds = 2.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[self someMethod];
});
1 |
|
// 创建 GCD 定时器
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_source_t _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_source_set_timer (_timer, dispatch_walltime (NULL, 0), 1.0 * NSEC_PER_SEC, 0); // 每秒执行
// 事件回调
dispatch_source_set_event_handler(_timer, ^{
dispatch_async(dispatch_get_main_queue(), ^{
// 在主线程中实现需要的功能
}
}
});
// 开启定时器
dispatch_resume(_timer);
// 挂起定时器(dispatch_suspend 之后的 Timer,是不能被释放的!会引起崩溃)
dispatch_suspend(_timer);
// 关闭定时器
dispatch_source_cancel(_timer);
1 |
|
// 计时时间
@property (nonatomic, assign) int timeout;
/* 开启倒计时 /
(void)startCountdown {
if (_timeout > 0) {
return;
}
_timeout = 60;
// GCD 定时器
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);dispatch_source_t _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_source_set_timer (_timer, dispatch_walltime (NULL, 0), 1.0 * NSEC_PER_SEC, 0); // 每秒执行
dispatch_source_set_event_handler(_timer, ^{
if(_timeout <= 0 ){// 倒计时结束 // 关闭定时器 dispatch_source_cancel(_timer); dispatch_async(dispatch_get_main_queue(), ^{ //设置界面的按钮显示 根据自己需求设置 [self.sendMsgBtn setTitle:@"发送" forState:UIControlStateNormal]; self.sendMsgBtn.enabled = YES; }); }else{// 倒计时中 // 显示倒计时结果 NSString *strTime = [NSString stringWithFormat:@"重发(%.2d)", _timeout]; dispatch_async(dispatch_get_main_queue(), ^{ //设置界面的按钮显示 根据自己需求设置 [self.sendMsgBtn setTitle:[NSString stringWithFormat:@"%@",strTime] forState:UIControlStateNormal]; self.sendMsgBtn.enabled = NO; }); _timeout--; }
});
// 开启定时器
dispatch_resume(_timer);
}
1 |
|
import
typedef void(^TimerBlock)();
@interface BYTimer : NSObject
(void)startTimerWithBlock:(TimerBlock)timerBlock;
(void)stopTimer;
@end
1 |
import “BYTimer.h”
@interface BYTimer ()
@property (nonatomic, strong) NSTimer *timer;
@property (nonatomic, strong) TimerBlock timerBlock;
@end
@implementation BYTimer
(void)startTimerWithBlock:(TimerBlock)timerBlock {
self.timer = [NSTimer timerWithTimeInterval:300 target:self selector:@selector(_timerAction) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
_timerBlock = timerBlock;
}
(void)_timerAction {
if (self.timerBlock) {self.timerBlock();
}
}(void)stopTimer {
[self.timer invalidate];
}
@end
```
该接口的实现很简单,就是 NSTimer 创建了一个 300s 执行一次的定时器,但是要注意定时器需要加入 NSRunLoopCommonModes
中。
要使定时器在后台能运行,app 就需要在 后台常驻。
结语
最后总结一下:
NSTimer 使用简单方便,但是应用条件有限。
CADisplayLink 刷新频率与屏幕帧数相同,用于绘制动画。具体使用可看我封装好的一个 水波纹动画。
GCD 定时器 精度高,可控性强,使用稍复杂。