在iOS开发中定时器是我们经常遇到的需求,常用到的定时器表示方式有
NSTimer、GCD
,那么它们之间有什么样的区别呢?本文将从两者的基本使用开始剖析它们之间的区别。
1、NSTimer
1.1、NSTimer简介
NSTimer
是iOS中最基本的定时器。NSTimer
是通过RunLoop
来实现的,在一般的情况下NSTimer作为定时器是比较准确的,但是如果当前的耗时操作较多时,可能出现延时问题。同时,因为受到RunLoop的支配,NSTimer会受到RunLoopMode
的影响。在创建NSTimer的时候默认是被加到defaultMode
的,但是如果在一个滑动的视图中如tableview,当RunLoop的mode发生变化时,当前的NSTimer就不会工作了,这就是我们在开发中遇到的NSTimer用在tableview中,当tableview滚动的时候NSTimer停止工作的原因,所以我们在创建NSTimer的时候将其加到RunLoop指定mode为NSRunLoopCommonModes
。
1.2、NSTimer基本使用
NSTimer的初始化方式有两种,分别是invocation
和selector
两种调用方式,这两种方式区别不大,但是selector
的方式更加简便。
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
下面我们来看下这两种方式的使用。
1.2.1、selector方式
使用selector方式初始化NSTimer比较简单,只需要指定执行的方法和是否循环就可以了。
- (void)selectorType {
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
// NSDefaultRunLoopMode模式,切换RunLoop模式,定时器停止工作.
// [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
// UITrackingRunLoopMode模式,切换RunLoop模式,定时器停止工作.
// [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
// common modes的模式,以下三种模式的组合模式 NSDefaultRunLoopMode & NSModalPanelRunLoopMode & NSEventTrackingRunLoopMode
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
- (void)timerTest {
NSLog(@"hello");
}
在上一个小节讲过,NSTimer依赖于RunLoop,需要把初始化好的timer添加到RunLoop中,对于RunLoop的几种模式在上面的代码注释中有说明。
这段代码的运行结果就是每隔两秒钟就会打印一次“hello”
打印结果:
2020-03-16 17:55:24.123435+0800 ThreadDemo[3845:9977585] hello
2020-03-16 17:55:26.122417+0800 ThreadDemo[3845:9977585] hello
2020-03-16 17:55:28.123599+0800 ThreadDemo[3845:9977585] hello
2020-03-16 17:55:30.122504+0800 ThreadDemo[3845:9977585] hello
1.2.1、invocation方式
通过invocation方式初始化timer相对于来说会稍微复杂一些,最主要的是invocation参数。同样的也需要手动将timer加入到RunLoop中。
- (void)invocationType {
// 获取到方法的签名
NSMethodSignature *signature = [[self class]instanceMethodSignatureForSelector:@selector(timerTest)];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
invocation.target = self;
invocation.selector = @selector(timerTest);
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 invocation:invocation repeats:YES];
[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
}
- (void)timerTest {
NSLog(@"hello");
}
这段代码的运行结果就是每隔两秒钟就会打印一次“hello”
打印结果:
2020-03-16 22:54:48.964318+0800 ThreadDemo[6400:10171057] hello
2020-03-16 22:54:50.964530+0800 ThreadDemo[6400:10171057] hello
2020-03-16 22:54:52.964403+0800 ThreadDemo[6400:10171057] hello
2020-03-16 22:54:54.964780+0800 ThreadDemo[6400:10171057] hello
1.2.3、scheduledTimerWithTimeInterval方法
在上面列举的API中其实有scheduledTimerWithTimeInterval
方法可以创建timer,这个方法和timerWithTimeInterval
的区别就在于前者会默认的将timer添加到了RunLoop,并且currentRunLoop是NSDefaultRunLoopMode
,而后者是需要开发者手动的将timer添加到RunLoop中。
- (void)scheduledTimer {
// NSTimer *timer1 = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
NSMethodSignature *signature = [[self class] instanceMethodSignatureForSelector:@selector(timerTest)];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
invocation.target = self;
invocation.selector = @selector(timerTest);
NSTimer *timer2 = [NSTimer scheduledTimerWithTimeInterval:2.0 invocation:invocation repeats:YES];
}
- (void)timerTest {
NSLog(@"hello");
}
这段代码的运行结果就是每隔两秒钟就会打印一次“hello”
打印结果:
2020-03-16 23:05:30.717027+0800 ThreadDemo[6581:10181270] hello
2020-03-16 23:05:32.715849+0800 ThreadDemo[6581:10181270] hello
2020-03-16 23:05:34.716522+0800 ThreadDemo[6581:10181270] hello
如上代码所示,并没有将timer添加到RunLoop,timer照样可以正常运行。
1.2.4 NSTimer在线程中使用
上面所列举的例子都是在主线程中运行的,那是因为主线程默认是启动RunLoop的,但是在线程是没有默认开启RunLoop的,所以当在子线程中使用NSTimer的时候就需要手动开启RunLoop了。
- (void)timerInThread {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
[[NSRunLoop currentRunLoop]run];
});
}
- (void)timerTest {
NSLog(@"hello");
}
1.3、NSTimer中存在的问题
1.3.1、RunLoop的mode问题
如果在一个滚动的视图(如tableview)使用NSTimer,在视图滚动的时候,timer会停止计时,那是因为当视图滚动的时候RunLoop的mode是UITrackingRunLoopMode
模式。解决方式就是把timer 添加到RunLoop的NSRunLoopCommonModes
,那么UITrackingRunLoopMode
和kCFRunLoopDefaultMode
都被标记为了common
模式,就可以在默认模式和追踪模式都能够运行。
1.3.2、NSTimer的循环引用
当NSTimer的target被强引用了,而target又强引用的timer,这样就造成了循环引用,导致timer无法释放产生内存泄露的问题。这也是在开发中经常遇到的问题。当然不是所有的NSTimer都会产生循环引用。
- repeats参数为NO的情况下,不会产生循环引用。
- ios10后的新的API方法
timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block
也不会产生循环引用,但是不要忘记了在合适的地方调用invalidate
方法停止定时器的运行。
- ios10后的新的API方法
要解决NSTimer的循环引用问题就需要打破NSTimer和target之间的循环条件,有如下几种方式。
1.3.2.1、NSProxy的方式
创建一个中间类DSProxy继承自NSProxy
,这个类中对timer的target进行弱引用,再把需要执行的方法都转发给timer的target。
@interface DSProxy : NSProxy
@property (weak, nonatomic) id target;
+ (instancetype)proxyWithTarget:(id)target;
@end
@implementation DSProxy
+ (instancetype)proxyWithTarget:(id)target {
DSProxy* proxy = [[self class] alloc];
proxy.target = target;
return proxy;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation{
SEL sel = [invocation selector];
if ([self.target respondsToSelector:sel]) {
[invocation invokeWithTarget:self.target];
}
}
@interface ProxyTimer : NSObject
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval target:(id)target selector:(SEL)selector userInfo:(nullable id)userInfo repeats:(BOOL)repeats;
@end
@implementation ProxyTimer
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval target:(id)target selector:(SEL)selector userInfo:(nullable id)userInfo repeats:(BOOL)repeats{
NSTimer* timer = [NSTimer scheduledTimerWithTimeInterval:timeInterval target:[DSProxy proxyWithTarget: target] selector:selector userInfo:userInfo repeats:repeats];
return timer;
}
@end
1.3.2.2、NSTimer封装
这种方式其实和NSProxy的方式很类似,创建一个类对NSTimer进行封装,将taget弱引用,
@interface DSTimer : NSObject
@property (nonatomic, weak) id target;
@property (nonatomic) SEL selector;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval target:(id)target selector:(SEL)selector userInfo:(nullable id)userInfo repeats:(BOOL)repeats;
@end
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval target:(id)target selector:(SEL)selector userInfo:(nullable id)userInfo repeats:(BOOL)repeats {
DSTimer *dsTimer = [[DSTimer alloc] init];
dsTimer.target = target;
dsTimer.selector = selector;
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:timeInterval target:dsTimer selector:@selector(timered:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
return timer;
}
- (void)timered:(NSTimer *)timer {
if ([self.target respondsToSelector:self.selector]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self.target performSelector:self.selector withObject:timer];
#pragma clang diagnostic pop
}
}
1.3.2.2、block实现
@interface NSTimer (DSTimer)
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval repeats:(BOOL)repeats blockTimer:(void (^)(NSTimer *))block;
@end
@implementation NSTimer (DSTimer)
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval repeats:(BOOL)repeats blockTimer:(void (^)(NSTimer *))block {
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:timeInterval target:self selector:@selector(timered:) userInfo:[block copy] repeats:repeats];
[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
return timer;
}
+ (void)timered:(NSTimer *)timer {
void (^ block)(NSTimer *timer) = timer.userInfo;
block(timer);
}
@end
2、GCD
2.1、GCD简介
GCD实现定时器功能,是利用GCD中的Dispatch Source
中的一种类型DISPATCH_SOURCE_TYPE_TIMER
来实现的。dispatch源(Dispatch Source)监听系统内核对象并处理,更加的精准。和NSTimer依赖于RunLoop不一样,GCD并不依赖于RunLoop,所以即使是在滚动视图中也不会出现视图滚动时定时器不起效果的情况。同时GCD定时器提供了定时器的启动、暂停、回复、取消等功能,相对而言更加的贴近开发需求。
2.2、GCD基本使用
GCD定时器调用 dispatch_source_create
方法创建一个source源,然后通过dispatch_source_set_timer
方法设置定时器,dispatch_source_set_event_handler
设置定时器任务,初创建的定时器是暂停的,需要调用dispatch_resume
方法启动定时器,当然也可以调用dispatch_suspend
或者dispatch_source_cancel
停止定时器。
下面是对于GCD的简单封装。
typedef enum : NSUInteger {
Status_Running,
Status_Pause,
Status_Cancle,
} TimerStatus;
@interface GCDTimer ()
@property (nonatomic, strong) dispatch_source_t gcdTimer;
@property (nonatomic, assign) TimerStatus currentStatus;
@end
@implementation GCDTimer
- (void)scheduledTimerWithTimeInterval:(NSTimeInterval)interval runNow:(BOOL)runNow afterTime:(NSTimeInterval)afterTime repeats:(BOOL)repeats queue:(dispatch_queue_t)queue block:(void (^)(void))block {
/** 创建定时器对象
* para1: DISPATCH_SOURCE_TYPE_TIMER 为定时器类型
* para2-3: 中间两个参数对定时器无用
* para4: 最后为在什么调度队列中使用
*/
self.gcdTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
/** 设置定时器
* para2: 任务开始时间
* para3: 任务的间隔
* para4: 可接受的误差时间,设置0即不允许出现误差
* Tips: 单位均为纳秒
*/
dispatch_time_t when;
if (runNow) {
when = DISPATCH_TIME_NOW;
} else {
when = dispatch_walltime(NULL, (int64_t)(afterTime * NSEC_PER_SEC));
}
dispatch_source_set_timer(self.gcdTimer, dispatch_time(when, interval * NSEC_PER_SEC), interval * NSEC_PER_SEC, 0);
dispatch_source_set_event_handler(self.gcdTimer, ^{
if (!repeats) {
dispatch_source_cancel(self.gcdTimer);
}
block();
});
dispatch_resume(self.gcdTimer);
self.currentStatus = Status_Running;
}
- (void)pauseTimer {
if (self.currentStatus == Status_Running && self.gcdTimer) {
dispatch_suspend(self.gcdTimer);
self.currentStatus = Status_Pause;
}
}
- (void)resumeTimer {
if (self.currentStatus == Status_Pause && self.gcdTimer) {
dispatch_resume(self.gcdTimer);
self.currentStatus = Status_Running;
}
}
- (void)stopTimer {
if (self.gcdTimer) {
dispatch_source_cancel(self.gcdTimer);
self.currentStatus = Status_Cancle;
self.gcdTimer = nil;
}
}
@end
2.3、GCD定时器的注意事项
1、dispatch_resume
和dispatch_suspend
调用要成对出现。dispatch_suspend
严格上只是把timer暂时挂起,dispatch_resume
和dispatch_suspend
分别会减少和增加 dispatch 对象的挂起计数。当这个计数大于 0 的时候,timer就会执行。但是Dispatch Source
并没有提供用于检测 source 本身的挂起计数的 API,也就是说外部不能得知一个 source 当前是不是挂起状态,那么在两者之间需要设计一个标记变量。
2、source在suspend状态下,如果直接设置source = nil或者重新创建source都会造成crash。正确的方式是在resume状态下调用dispatch_source_cancel(source)释放当前的source。
3、dispatch_source_set_event_handler
回调是一个block,在添加到source中后会被source强引用,所以在这里需要注意循环引用的问题。正确的方法是使用weak+strong或者提前调用dispatch_source_cancel
取消timer。
3、NSTimer和GCD定时器的比较
- NSTimer依赖于RunLoop运行,所以在子线程中使用NSTimer需要手动启动RunLoop。而GCD并不依赖于RunLoop,在子线程中可以正常使用。
- NSTimer依赖于RunLoop运行,在某种特定的环境下可能会需要RunLoop模式切换。
- NSTimer会存在延时的可能性,所以在定时层面准确性会有所偏差。GCD是监听系统内核对象并处理,定时更加精确。
- NSTimer的容易出现循环引用,GCD相对而言会好很多。当然规范编程合理设计这些都不是问题。