在iOS开发中定时器是咱们常常遇到的需求,经常使用到的定时器表示方式有
NSTimer、GCD
,那么它们之间有什么样的区别呢?本文将从二者的基本使用开始剖析它们之间的区别。ios
NSTimer
是iOS中最基本的定时器。NSTimer
是经过RunLoop
来实现的,在通常的状况下NSTimer做为定时器是比较准确的,可是若是当前的耗时操做较多时,可能出现延时问题。同时,由于受到RunLoop的支配,NSTimer会受到RunLoopMode
的影响。在建立NSTimer的时候默认是被加到defaultMode
的,可是若是在一个滑动的视图中如tableview,当RunLoop的mode发生变化时,当前的NSTimer就不会工做了,这就是咱们在开发中遇到的NSTimer用在tableview中,当tableview滚动的时候NSTimer中止工做的缘由,因此咱们在建立NSTimer的时候将其加到RunLoop指定mode为NSRunLoopCommonModes
。编程
NSTimer的初始化方式有两种,分别是invocation
和selector
两种调用方式,这两种方式区别不大,可是selector
的方式更加简便。bash
+ (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;
复制代码
下面咱们来看下这两种方式的使用。markdown
使用selector方式初始化NSTimer比较简单,只须要指定执行的方法和是否循环就能够了。async
- (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”oop
打印结果: 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] helloatom
经过invocation方式初始化timer相对于来讲会稍微复杂一些,最主要的是invocation参数。一样的也须要手动将timer加入到RunLoop中。spa
- (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设计
在上面列举的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照样能够正常运行。
上面所列举的例子都是在主线程中运行的,那是由于主线程默认是启动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"); } 复制代码
若是在一个滚动的视图(如tableview)使用NSTimer,在视图滚动的时候,timer会中止计时,那是由于当视图滚动的时候RunLoop的mode是UITrackingRunLoopMode
模式。解决方式就是把timer 添加到RunLoop的NSRunLoopCommonModes
,那么UITrackingRunLoopMode
和kCFRunLoopDefaultMode
都被标记为了common
模式,就能够在默认模式和追踪模式都可以运行。
当NSTimer的target被强引用了,而target又强引用的timer,这样就形成了循环引用,致使timer没法释放产生内存泄露的问题。这也是在开发中常常遇到的问题。固然不是全部的NSTimer都会产生循环引用。
timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block
也不会产生循环引用,可是不要忘记了在合适的地方调用invalidate
方法中止定时器的运行。要解决NSTimer的循环引用问题就须要打破NSTimer和target之间的循环条件,有以下几种方式。
建立一个中间类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 复制代码
这种方式其实和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 } } 复制代码
@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 复制代码
GCD实现定时器功能,是利用GCD中的Dispatch Source
中的一种类型DISPATCH_SOURCE_TYPE_TIMER
来实现的。dispatch源(Dispatch Source)监听系统内核对象并处理,更加的精准。和NSTimer依赖于RunLoop不同,GCD并不依赖于RunLoop,因此即便是在滚动视图中也不会出现视图滚动时定时器不起效果的状况。同时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 复制代码
一、dispatch_resume
和dispatch_suspend
调用要成对出现。dispatch_suspend
严格上只是把timer暂时挂起,dispatch_resume
和dispatch_suspend
分别会减小和增长 dispatch 对象的挂起计数。当这个计数大于 0 的时候,timer就会执行。可是Dispatch Source
并无提供用于检测 source 自己的挂起计数的 API,也就是说外部不能得知一个 source 当前是否是挂起状态,那么在二者之间须要设计一个标记变量。 二、source在suspend状态下,若是直接设置source = nil或者从新建立source都会形成crash。正确的方式是在resume状态下调用dispatch_source_cancel(source)释放当前的source。 三、dispatch_source_set_event_handler
回调是一个block,在添加到source中后会被source强引用,因此在这里须要注意循环引用的问题。正确的方法是使用weak+strong或者提早调用dispatch_source_cancel
取消timer。