本文约100行代码,读完大概用时5-10分钟,理解的话看个人知识掌握程度。
App在开发的过程中,经常会遇到倒计时等等与时间计算有关的需求,这时就需要我们去使用定时器了,本篇我们就来盘点盘点iOS中的三大定时器:NSTimer、dispatch_source_t和CADisplayLink。
一、NSTimer
1.NSTimer的介绍
NSTimer应该是新手最耳熟能详的定时器了,通过Apple开发文档的描述 A timer that fires after a certain time interval has elapsed, sending a specified message to a target object.
我们可以看到它是通过间隔一定的时间,向目标对象发送指定的消息(OC中调用方法在底层就是发送消息)来实现定时器的功能的。NSTimer在使用的过程中其实是有很多小细节需要注意的,下面都会讲到。
2.NSTimer的方法
NSTimer有3个timerWith类方法(初始化):
/// @param ti 定时器的时间间隔
/// @param invocation 方法调用
/// @param yesOrNo 是否重复执行
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
/// @param ti 定时器的时间间隔
/// @param aTarget 目标对象(一般是self)
/// @param aSelector 方法调用
/// @param yesOrNo 是否重复执行
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
/// @param interval 定时器的时间间隔
/// @param repeats 是否重复执行
/// @param block 方法调用(代码块的形式)
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
这3种类方法需要你手动将timer对象添加到runloop中;
// 在主runloop上添加 [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSDefaultRunLoopMode];
和3个scheduledTimer类方法(初始化):
/// @param ti 定时器的时间间隔
/// @param invocation 方法调用
/// @param yesOrNo 是否重复执行
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
/// @param ti 定时器的时间间隔
/// @param aTarget 目标对象(一般是self)
/// @param aSelector 方法调用
/// @param yesOrNo 是否重复执行
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
/// @param interval 定时器的时间间隔
/// @param repeats 是否重复执行
/// @param block 方法调用(代码块的形式)
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
这3种类方法需要会自动将timer对象添加到当前runloop(默认是主runloop)中,并且mode为NSDefaultRunLoopMode;
以及2个initWith实例方法(初始化):
/// 需要手动将timer对象添加到runloop中
/// @param date 开始执行的日期
/// @param interval 定时器的时间间隔
/// @param repeats 是否重复执行
/// @param block 方法调用(代码块的形式)
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
/// 需要手动将timer对象添加到runloop中
/// @param date 开始执行的日期
/// @param ti 定时器的时间间隔
/// @param t 目标对象(一般是self)
/// @param s 方法调用
/// @param ui 可自定义的参数
/// @param rep 是否重复执行
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(nullable id)ui repeats:(BOOL)rep;
以上8个初始化方法,scheduledTimer
方法会自动添加timer到当前runloop,其他则不会
使用timerWith
和initWith
时需要手动添加timer到runloop
2个常用的实例方法:
// 开始执行
- (void)fire;
// 销毁
- (void)invalidate;
fire
会立即调用方法,在执行完后,如果不是重复的timer,会立即 invalidate ;
invalidate
会停止重复的timer(不重复的执行一次后会自动invalidate),并将其从runloop中remove。
3.NSTimer的使用示例
3.1 在一个普通的VC中使用:
@property (nonatomic, strong) NSTimer *timer; //作为属性一般使用strong修饰,因为timer是一个对象,需要被持有者强引用以防提前释放
// 这里使用weakSelf来避免循环引用从而导致内存泄漏
__weak typeof(self) weakSelf = self;
_timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
[weakSelf changeLabelText];
}];
值得注意的是,当我们使用 target: selector:
的方式时,target后面使用weakSelf 并不能避免循环引用,此时timer依然会对self进行强引用,会导致内存泄漏,下面的代码是错误的:
_timer = [NSTimer scheduledTimerWithTimeInterval:1 target:weakSelf selector:@selector(changeLabelText) userInfo:nil repeats:YES];
如果此时页面上有scrollView或者tableView等在滑动时,需要手动更改timer的mode:
/* 当scrollView滚动的时候,当前的 MainRunLoop 会处于 UITrackingRunLoopMode 的模式下,
在这个模式下,是不会处理 NSDefaultRunLoopMode 的任务的 */
[[NSRunLoop mainRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
3.2 立即执行timer的方法调用:
// 执行fire方法后,会立即执行本来需要时间间隔后才执行的指定方法调用
// 对于重复的timer,它是一次额外的操作,并且不会打破正常的schedule
// 对于不重复的timer,它一触发完后,timer就被invalidate,就不管原来设定的时间间隔了
[_timer fire];
3.3 timer的释放与销毁:
if ([_timer isValid]) {
[_timer invalidate]; //对于不重复的timer可以不写,因为不重复的timer在执行完后就自动invalidate了
_timer = nil;
}
PS:看到很多文章说timer的销毁不能放在dealloc
中,要放在- (void)viewDidDisappear:(BOOL)animated
中,因为在dealloc中并不会去执行。这种说法一般是不对的,首先,dealloc中不会去执行大概率是出现了循环引用,此时VC仍然被timer强引用,导致VC没法dealloc
,那么timer当然不会去执行销毁;其次,viewDidDisappear
时去销毁那么你在跳往下一级页面而不是返回上一级页面的时候,此时当前页面一般是要继续存在的,这么做就将当前页面的timer销毁了,肯定是不对的,正确的做法是使用上面timer的block API + weakSelf来避免循环使用。
4.NSTimer的更多使用技巧
4.1 暂停和启动:
// 让timer的fire时间为“遥远的未来”,那么它就“暂停”了
[_timer setFireDate:[NSDate distantFuture]];
// 让timer的fire时间为“马上”或“远古”,那么它就启动了
[self.timer setFireDate:[NSDate date]];
[self.timer setFireDate:[NSDate distantPast]];
4.2 非固定时间间隔执行timer
- (void)randomTimeTimer {
// 此处先将timer的timeInterval设置为无穷大,这样它便不会执行
_timer = [NSTimer timerWithTimeInterval:MAXFLOAT target:self selector:@selector(randomTimeFireMethod) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer: _timer forMode:NSDefaultRunLoopMode];
}
- (void)randomTimeFireMethod {
static int timeExecute = 0;
// 这里的4是你想要timer执行的次数
if (timeExecute < 4) {
// 不定长执行
NSTimeInterval timeInterval = [self.randomTime[timeExecute] doubleValue];
timeExecute++;
// 使用fireDate来控制timer以达到不定长执行
_timer.fireDate = [NSDate dateWithTimeIntervalSinceNow:timeInterval];
}
}
OC中并没有NSTimer的暂停、启动和非固定时间间隔的方法,我们可以使用这种奇思妙想来达到这个目的。
4.3 在子线程中使用NSTimer
5.NSTimer的注意事项
5.1 为什么NSTimer的销毁需要invalidate
+ =nil
?
在OC中,一般我们销毁强引用,会直接使用 =nil
,但是NSTimer不可以。我们来看看ARC中的NSTimer创建到销毁的过程中具体的引用关系变化:
-
VC创建NSTimer后,此时VC对timer强引用,再之后timer加入到runloop后,系统也会强引用timer
NSTimer的创建过程 -
=nil
后,VC解除了对timer的强引用,但此时系统依然对timer有强引用
=nil - 调用
invalidate
方法后,系统解除对timer的强引用
invalidate后
综合以上,我们需要对timerinvalidate
+=nil
,才能真正的销毁NSTimer。
5.2 NSTimer不是实时的 / NSTimer可以设置Tolerance(容忍度)。
- NSTimer加入的runloop正好处在一个耗时的周期内;
- NSTimer添加的runloopMode不是当前runloop所处的mode时,如
NSDefaultRunLoopMode
的NSTimer在页面滑动时暂停; - Tolerance大概是避免NSTimer在runloop中的不实时带来的时间偏移的(实际开发中使用较少,暂时没怎么研究)。
二、dispatch_source_t
1. dispatch_source_t的介绍
dispatch_source_t
是众多DISPATCH_SOURCE
种类的一种
针对NSTimer受runloop的影响而不精准的问题,dispatch_source_t是一种相对精准的计时器,并且它天生就可以使用GCD在子线程中执行,解决NSTimer在主线程中导致卡顿的问题,但是它的缺点也比较明显,就是代码量相对比较多一点。
2. dispatch_source_t的使用
定义属性:
// 此处也用strong修饰,虽然没有 * ,但是dispatch_source_t也是对象,和普通的对象一样,strong防止提前释放
@property (nonatomic, strong) dispatch_source_t timer;
初始化和设定:
- (void)initTimer {
if (!_gcdTimer) {
// 创建队列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
// 初始化timer(设定source_type,以及队列)
_gcdTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 设定timer的开始时间
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC));
// 如果timer的间隔时间比较大,那么可以使用dispatch_walltime来创建start,可以避免误差
dispatch_time_t start_0 = dispatch_walltime(0, 0);
// 设定timer的固定时间间隔
uint64_t interval = (uint64_t)(1 * NSEC_PER_SEC);
// 设置timer,最后一个参数为leeway,是用来设置定时器的“期望精度值”,系统会根据这个值延迟或提前触发定时器
dispatch_source_set_timer(_gcdTimer, start, interval, 0);
// 设定timer的方法调用
dispatch_source_set_event_handler(_gcdTimer, ^{
// 如果timer的方法调用是UI方面相关的操作,需要在主线程中执行(线程间通信)
dispatch_async(dispatch_get_main_queue(), ^{
[self changeLabelText];
});
});
// 开启定时器
dispatch_resume(_gcdTimer);
}
}
dispatch_source_create
方法参数详细说明
- 第一个参数:dispatch_source_type_t type为设置GCD源方法的类型,前面已经列举过了。
- 第二个参数:uintptr_t handle Apple的API介绍说,暂时没有使用,传0即可。
- 第三个参数:unsigned long mask Apple的API介绍说,使用DISPATCH_TIMER_STRICT,会引起电量消耗加剧,毕竟要求精确时间,所以一般传0即可,视业务情况而定。
- 第四个参数:dispatch_queue_t _Nullable queue 队列,将定时器事件处理的Block提交到哪个队列,可以传Null,默认为全局队列。
开启定时器:
dispatch_resume(_gcdTimer);
暂停定时器:
// 暂停
- (void)pauseTimer {
if (_gcdTimer) {
dispatch_suspend(_gcdTimer);
}
}
// 暂停后的重新开启
dispatch_resume(_gcdTimer);
销毁定时器:
dispatch_source_cancel(_gcdTimer);
_gcdTimer = nil;
3. dispatch_source_t的注意事项
timer被dispatch_suspend
后是不能释放的,否则会引起崩溃。因为OC中并没有dispatch_source_t的暂停和开启状态的记录,所以如果我们用到了它的暂停和开启,则我们必须手动记录,有dispatch_suspend
则必有dispatch_resume
。
4. dispatch_source_t的优缺点
优点:
- 性能更好,相对更精确;
- 自带暂停、继续;
- 天生适合在子线程中使用;
- 不需要加入到runloop中,也不需要管runloop的mode。
缺点:
- 每次
dispatch_resume
都会先执行一次; - 本质上也不是完全精确;
- 代码量较多。
三、CADisplayLink
1. CADisplayLink的介绍
CADisplayLink是OC中精度最高的定时器,它是根据设备的屏幕刷新频率来执行操作,因此它的使用场景也相对当一,比较适合用来做UI的绘制、自定义的动画引擎以及视频播放的渲染。
2. CADisplayLink的方法和相关属性
1个初始化类方法:
/// @param target 目标对象(一般是self)
/// @param sel 方法调用
+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;
3个实例方法
/// 将CADisplayLink对象添加到runloop中并指定mode
/// @param runloop 加入的runloop
/// @param mode 指定runloopMode
- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode;
/// 将CADisplayLink对象从runloop指定的mode中移除
/// @param runloop 被移除CADisplayLink对象的runloop
/// @param mode 指定的runloopMode
- (void)removeFromRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode;
// 将CADisplayLink对象从runloop所有mode中移除
- (void)invalidate;
两个移除方法的区别
removeFromRunLoop
会将其从指定的runloop的指定mode中移除,此方法在runloop或者mode任一不匹配的情况下都无效,而且remove时需要进行判断,如果指定的mode中不存在,那么将会引起crash,原因是重复over-release
;
invalidate
是从runloop的所有模式中移除,并取消和target的关联关系,此方法可以多次调用,不会引起crash。
3个只读属性:
// 当前屏幕上显示帧率的时间戳
@property(readonly, nonatomic) CFTimeInterval timestamp;
// 定时器的时间间隔
@property(readonly, nonatomic) CFTimeInterval duration;
// 客户端针对其渲染的下一个时间戳
@property(readonly, nonatomic) CFTimeInterval targetTimestamp;
3个读写属性:
// 是否暂停,设置了暂停后定时器将暂停,直到设置为false的时候再执行
@property(getter=isPaused, nonatomic) BOOL paused;
// 从iOS10开始已废弃,不要去使用
@property(nonatomic) NSInteger frameInterval;
// 每秒刷新次数(帧率)
@property(nonatomic) NSInteger preferredFramesPerSecond;
3. CADisplayLink使用示例
// 创建
- (void)initDisplaylink {
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(changeLabelText)];
_displayLink.preferredFramesPerSecond = 0; //每秒刷新次数,设置为0时就是默认屏幕的最大刷新帧率
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
}
// 销毁
[_displayLink invalidate];
_displayLink = nil;
4. CADisplayLink的特性
CADisplayLink
不能够继承-
修改帧率
CADisplayLink的实际帧率是由屏幕最大帧率(maximumFramesPerSecond
)和参数preferredFramesPerSecond
一起决定的,规则为:如果屏幕最大帧率是60,实际帧率只能是15、20、30、60中的一种;如果设置大于60的值,屏幕实际帧率为60。如果设置的是26~35之间的值,实际帧率是30;如果设置为0,会使用最高帧率。 - 在添加进runloop时应当选择高优先级的,以保证动画的流畅
5. CADisplayLink防止循环引用
上面NSTimer在防止循环引用时使用了NSTimer本身提供的block方法而非传入target的方式,但是CADisplayLink本身没有提供block方法,只有传入target的方式,那么我们怎么避免循环引用呢?
首先我们来看一种错误的做法:
__weak typeof(self) weakSelf = self;
_displayLink = [CADisplayLink displayLinkWithTarget:weakSelf selector:@selector(changeLabelText:)];
_displayLink.preferredFramesPerSecond = 10;
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
然后再看另一种错误的做法:
// 将displayLink属性声明为weak
@property (nonatomic, weak) CADisplayLink *displayLink;
// 初始化
CADisplayLink *temp = [CADisplayLink displayLinkWithTarget:self selector:@selector(changeLabelText:)];
temp.preferredFramesPerSecond = 10;
[temp addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
_displayLink = temp;
以上两种方法都是错误的,在页面销毁时,它们无一例外的定时器都没有被销毁,依然在工作,原因在于此时runloop对定时器依然有强引用。
此时正确的做法有两种:
1.使用NSProxy
创建一个继承自NSProxy
的新类GQProxy
// .h
@interface GQProxy : NSProxy
@property (weak, nonatomic) id target;
+ (instancetype)proxyWithTarget:(id)target;
@end
// .m
#import "GQProxy.h"
@implementation GQProxy
+ (instancetype)proxyWithTarget:(id)target {
GQProxy *proxy = [GQProxy alloc];
proxy.target = target;
return proxy;
}
//返回方法签名
-(NSMethodSignature*)methodSignatureForSelector:(SEL)sel{
return [self.target methodSignatureForSelector:sel];
}
-(void)forwardInvocation:(NSInvocation *)invocation{
[invocation invokeWithTarget:self.target];
}
@end
在初始化定时器时
_displayLink = [CADisplayLink displayLinkWithTarget:[GQProxy proxyWithTarget:self] selector:@selector(changeLabelText:)];
_displayLink.preferredFramesPerSecond = 10;
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
这样就可以在避免循环引用了,推荐NSTimer也使用这种方法。
2.使用category扩展block方法
新建一个分类 CADisplayLink+GQTool
// .h
#import <QuartzCore/QuartzCore.h>
typedef void(^GQExecuteDisplayLinkBlock) (CADisplayLink *displayLink);
@interface CADisplayLink (GQTool)
@property (nonatomic,copy) GQExecuteDisplayLinkBlock executeBlock;
+ (CADisplayLink *)displayLinkWithExecuteBlock:(GQExecuteDisplayLinkBlock)block;
@end
// .m
#import "CADisplayLink+GQTool.h"
#import <objc/runtime.h>
@implementation CADisplayLink (GQTool)
- (void)setExecuteBlock:(GQExecuteDisplayLinkBlock)executeBlock{
objc_setAssociatedObject(self, @selector(executeBlock), [executeBlock copy], OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (GQExecuteDisplayLinkBlock)executeBlock{
return objc_getAssociatedObject(self, @selector(executeBlock));
}
+ (CADisplayLink *)displayLinkWithExecuteBlock:(GQExecuteDisplayLinkBlock)block{
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(gq_executeDisplayLink:)];
displayLink.executeBlock = [block copy];
return displayLink;
}
+ (void)gq_executeDisplayLink:(CADisplayLink *)displayLink{
if (displayLink.executeBlock) {
displayLink.executeBlock(displayLink);
}
}
@end
在初始化定时器时
__weak typeof(self) weakSelf = self;
_displayLink = [CADisplayLink displayLinkWithExecuteBlock:^(CADisplayLink * _Nonnull displayLink) {
[weakSelf changeLabelText];
}];
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
这样也可以避免定时器对VC的强引用,但本质上只是将定时器的target从控制器换成了定时器本身的类,还是存在循环引用,只不过对我们的系统没有影响了。所以推荐使用NSProxy这种方法。
以上就是关于iOS中三种定时器的详细介绍,原创不易,如果您觉得这篇文章对您有用的话,就顺手点个赞+关注吧。
网友评论