在项目开发中,经常会在代码中处理一些需要延时或定时执行的任务,iOS 中处理定时任务的方法包括 performSelector 方法、NSTimer、GCD、CADisplayLink,其本质都是通过RunLoop来实现,下面我们就对这几个方法做一些总结。
1. performSelector方法
在NSRunLoop.h中有对NSObject类的扩展方法,简单易用:
@interface NSObject (NSDelayedPerforming)
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSRunLoopMode> *)modes;
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget selector:(SEL)aSelector object:(nullable id)anArgument;
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget;
@end
2. NSTimer
NSTimer 是最常使用的定时器,使用方式简单,NSTimer 是也通过添加到RunLoop中被触发并进行工作的,桥接 CFRunLoopTimerRef。NSTimer中定义的常用方法如下:
+ (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;
+ (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));
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(nullable id)ui repeats:(BOOL)rep NS_DESIGNATED_INITIALIZER;
以下是初始化NSTimer的不同方式:
// 自动加入currentRunLoop
self.timer = [NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(timerRuning) userInfo:nil repeats:YES];
//self.timer = [NSTimer scheduledTimerWithTimeInterval:5.0 repeats:YES block:^(NSTimer * _Nonnull timer) { }];
// 手动加入RunLoop
self.timer = [NSTimer timerWithTimeInterval:5 target:self selector:@selector(timerRuning) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
// 指定timer触发时刻
NSTimeInterval timeInterval = [self timeIntervalSinceReferenceDate] + 30;
NSDate *newDate = [NSDate dateWithTimeIntervalSinceReferenceDate:timeInterval];
self.timer = [[NSTimer alloc] initWithFireDate:newDate interval:5 target:self selector:@selector(timerRuning) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
如果当前界面中有UITableView,则在 UITableView 在滚动过程中,上述代码中的定时器到了时间并没有触发。根据RunLoop的相关知识,同一时刻 RunLoop 只运行在一种 Mode 上,并且只有这个 Mode 相关联的源或定时器会被传递消息,mainRunLoop 一般处于 NSDefaultRunLoopMode,但是在滚动或者点击事件等触发时,mainRunLoop 切换至 NSEventTrackingRunLoopMode ,而上面 timer 被加入的正是 NSDefaultRunLoopMode (未指明也默认加入默认模式),所以滑动时未触发定时操作。
解决方法:添加timer到mainRunLoop的NSRunLoopCommonMode中或者子线程中,需要注意的是加入子线程时要手动开启并运行子线程的RunLoop。
self.timer = [NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(timerRuning) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
NSRunLoopCommonMode这是一组可配置的常用模式。将输入源与此模式相关联也会将其与组中的每个模式相关联。对于Cocoa应用程序,此集合默认包括NSDefaultRunLoopMode,NSPanelRunLoopMode和NSEventTrackingRunLoopMode。
注意:
- iOS10以前初始化的timer在运行期间会对target进行持有,因此,在释放时需要手动调用invalidate方法,并置nil;
- timer不能在当前宿主的dealloc方法中调用,因为timer没有被释放前,当前宿主不会执行dealloc方法;
- 当前RunLoop会切换Mode,因此可能导致timer不是立刻被触发。
- 在同一线程中,timer重复执行期间,有其他耗时任务时,在改耗时任务完成前也不会触发定时,在耗时任务完成后,timer的定时任务会继续执行。
- dispatch_source_set_timer中设置启动时间,dispatch_time_t可通过两个方法生成:dispatch_time 和 dispatch_walltime
3. GCD定时器
我们也可以通过GCD中的方法实现定时器来处理定时任务,实现的代码逻辑如下:
// 1. 创建 dispatch source,指定检测事件为定时
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue("Timer_Queue", 0));
// 2. 设置定时器启动时间、间隔
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 0.5 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
// 3. 设置callback
dispatch_source_set_event_handler(timer, ^{
NSLog(@"timer fired");
});
dispatch_source_set_event_handler(timer, ^{
//取消定时器时一些操作
});
// 4. 启动定时器(刚创建的source处于被挂起状态)
dispatch_resume(timer);
// 5. 暂停定时器
dispatch_suspend(timer);
// 6. 取消定时器
dispatch_source_cancel(timer);
timer = nil;
当我们想要timer只是延时执行一次时,只调用以下方法即可:
// 在主线程中延时5s中执行
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
});
注意:
- 正在执行的 block,在调用dispatch_suspend(timer)时,当前block并不会立即停止而是继续执行至完成;
- dispatch source在挂起时,直接设置为 nil 或者重新赋值都会造成crash,需要在activate的状态下调用dispatch_source_cancel(timer)后置为 nil 或者重新赋值;
- dispatch_source_cancel方法可以在dispatch_source_set_event_handler中调用,即timer可内部持有也可外部持有;
- dispatch_resume和dispatch_suspend调用需成对出现,否则会crash;
- dispatch source会比 NSTimer 更精准一些。