定时器 你真的会使用吗?
前言
定时器的使用是软件开发基础技能,用于延时执行或重复执行某些方法。
我相信大部分人接触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定时器 精度高,可控性强,使用稍复杂。