IOS之定时器
在开发中我们经常用到定时器,IOS为我们提供了很多种定时器,NSTimer
、CADisplayLink
、GCD
、NSThread(performSelector:afterDelay)
,其本质都是通过RunLoop来实现的。这里记录一下学习定时的记录。
NSTimer
- IOS中最基本的定时,在swift中成为Timer.其通过RunLoop来实现,一般情况下比较准确,但是当前循环耗时操作较多时,会出现延迟问题。同时也受加入的RunLoopModel影响。(如果直接创建使用,当界面处于滑动状态时,定时器会停止执行,等滑动结束在开始计时执行。)
1.NSTimer创建
- 使用
+ (NSTimer *)timerWithTimeInterval: target: selector: userInfo: repeats:;
方法来创建此种方法创建的定时器,需要手动添加到RunLoop才能启动.
self.timer = [NSTimer timerWithTimeInterval:3 target:self selector:@selector(timerfunc:) userInfo:nil repeats:YES];
//把定时器添加到当前的RunLoop中,如果不添加到RunLoop中 定时器不会启动
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
//该方法是让定时器,立刻启动,然后在根据时间间隔来执行,如果调用此方法,那么定时器创建之后并不会立刻启动,会延迟间隔时间后,在执行
[self.timer fire];
- 使用
NSInvocation
的方式来创建定时器,创建方式+ (NSTimer *)timerWithTimeInterval:invocation: repeats:
- (void)creatTiemrToInvocation {
//1.首先获取方法签名
SEL selector = @selector(invocationTest:num:);
NSMethodSignature *signature = [[self class] instanceMethodSignatureForSelector:selector];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
invocation.target = self;
invocation.selector = selector;
//设置invocation的参数 设置参数要冲第二个参数设置起 前面有两个参数是 self, _cmd
NSString *str = @"guoweiyong";
[invocation setArgument:&str atIndex:2];
NSNumber *number = [NSNumber numberWithInt:20];
[invocation setArgument:&number atIndex:3];
//这里也可以设置返回值,但是我这里没有返回值就不设置了
// - (void)getReturnValue:(void *)retLoc;
// - (void)setReturnValue:(void *)retLoc;
//执行方法 定时器不需要调用这个方法
//[invocation invoke];
self.timer = [NSTimer timerWithTimeInterval:1 invocation:invocation repeats:true];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
//[self.timer fire];
}
- (void)invocationTest:(NSString *)str num:(NSNumber *)number {
NSLog(@"invocation 执行Timer test str=====%@ number====%@",str,number);
}
- 通过
+ (NSTimer *)timerWithTimeInterval: target:selector: userInfo:repeats:;
方法来创建的定时器,是不需要手动启动的,已经默认添加到RunLoop中,所以不需要手动启动。
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerfunc:) userInfo:nil repeats:YES];
Invocation
的创建方式和上面一样- IOS10.0以后有一种新的创建方式: 代码块(block)创建方式:
+ (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));
2.NSTime的问题
2.1坑一: 延时问题:
- 当前RunLoop过于繁忙
RunLoop
模式与定时器所在的模式不同(schedule方式启动的timer
是add到RunLoop
的NSDefaultRunLoopMode
这就会出现其他mode时tiemr得不到调度的问题。最常见的问题就是在UITrackingRunLoopMode
即UIScorllView
滑动过程中定时器失效。–> 解决方式:把Timer
添加到NSRunLoopCommonModes
模式下,UITrackingRunLoopMode
和kCFRunLoopDefaultMode
都被标记为了common
模式,所以在这个模式下面Timer都可以运行)- 不管是一次性的还是周期性的Timer的实际触发的时间,都会与所加入的RunLoop的RunLoopMode模式有关,如果此RunLoop正在执行一个连续性的运算,timer就会被延时出发。重复性的timer遇到这种情况,如果延迟超过了一个周期,则会在延时结束后立刻执行,并按照之前指定的周期继续执行。
2.2 子线程启动定时器问题:
我们都知道IOS是通过RunLoop作为消息循环机制,主线程默认启动RunLoop,可是子线程没有默认的RunLoop,因此,我们在子线程启动定时器是不生效的。
解决方式也简单,在子线程启动一下RunLoop就可以了
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSTimer* timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timerfunc:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
[[NSRunLoop currentRunLoop] run];
});
2.3坑三: 循环引用问题:
循环引用问题,是每个使用者都会遇到的问题,究其原因就是NSTimer
的target
被强引用了,而通常target就是所在的控制器,他有强引用的timer,造成了循环引用。
在这里首先声明下,不是所有的NSTimer都会造成循环引用。就想不是所有的block都会造成循环引用一样。以下两种timer不会造成循环引用。
- 非
repeats
类型的,非repeats
类型的定时器,不会强引用target,因此不会出现循环引用。 - blcok类型的 ,新API,IOS10.0支持,因此对于还要支持老版本的app来说,这API暂时无法使用,当然block内部的循环引用也要避免。
- 二次声明:不是解决了循环引用,target就可以释放了,别忘了在持有timer的类的dealloc的时候执行
invalidate()
方法
解决循环引用,无非就是打破Timer对self的强引用。
方法参考:参考IOS之循环引用NSTimer
GCD定时器
GCD中的Dispatch Source 中的一种类型是DISPATCH_SOURCE_TYPE_TIMER
定时器类型,可以实现定时器的功能,需要注意点是这种定时器并不是把timer添加到RunLoop中,所以需要添加属性,否则不会执行定时器。
- (void)GCDTiemr {
//最后一个参数是队列,在那个队列中执行
self.gcdTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_set_timer(self.gcdTimer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(self.gcdTimer, ^{
NSLog(@"GCD定时器执行");
});
//定时器的启动方法
dispatch_resume(self.gcdTimer);
}
- GCD定时器在子线程中执行:
- (void)gcdTiemrAsync {
self.gcdTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0));
dispatch_source_set_timer(self.gcdTimer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(self.gcdTimer, ^{
NSLog(@"GCD定时器在子线程中执行====%@",[NSThread currentThread]);
});
dispatch_resume(self.gcdTimer);
}
GCD定时器的好处就是,它并不是假日RunLoop中执行的,因此子线程也可以使用,并且不会硬气循环引用的问题。测试发现,当控制器(VC)销毁时,GCD定时器自动停止执行,销毁。
延时操作
dispatch_after
dispatch_after 延时操作,不会对控制器强引用, dispatch_after的操作和控制器的消亡没有关系,及时控制器销毁,但是dispatch_after在延迟时间到达后还是会执行操作。如果dispatch_after的block代码块中,对当前控制器产生了强引用,当销毁控制器时,控制器不会立刻销毁,等到dispatch_after中的block代码执行完毕之后,在销毁。
- (void)afterExecture {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//对当前控制器强引用
NSLog(@"延迟10秒执行操作===%@",self.mark);
});
}
没有产生引用的执行代码和结果:
- (void)afterExecture {
__weak typeof(self) weakSelf = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//对当前控制器强引用
NSLog(@"延迟10秒执行操作===%@",weakSelf.mark);
});
}
performSelector: afterDelay:
此方法也可以执行延时操作:
- (void)afterExecture2 {
__weak typeof(self) weakSelf = self;
//使用perform 的方式来执行 延时操作
[self performSelector:@selector(afterExectureTest) withObject:nil afterDelay:5];
}
这种调用方式的好处是可以取消:
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget selector:(SEL)aSelector object:(nullable id)anArgument;
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget;
当前VC中有使用此方法执行操作的haul,如果要销毁,必须等待此方法执行完毕才能销毁。这个方法其内部也是基于定时器添加到RunLoop中实现的,因此也存在此方法在子线程中无法执行的问题。
- (void)ansycAfterExecture {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self performSelector:@selector(afterExectureTest) withObject:nil afterDelay:5];
});
}
- (void)afterExectureTest {
NSLog(@"延迟5秒执行操作=========%@",[NSThread currentThread]);
}
虽然无法执行这个操作,但是控制器的依然还是 等到延时时间到了之后,才销毁
-
延时一次的操作选择:
几种方式都是定时器,都可以实现延时操作。综合相比:如果只是单独一次的延时操作,NSTimer和GCD的定时器都显得有些笨重。performSelector方式比较合适,但是又收到了子线程runloop的限制。因此,dispatch_after是最优的选择。 -
延时的取消操作:
以下几种方式都可以实现取消 -
NSTimer
可以通过invalidate
来停止定时器。 -
GCD
的定时器可以调用dispatch_suspend
来挂起。 -
performSelector:after
可以通过cancelPreviousPerformRequestsWithTarget
取消。 -
dispatch_after
可以通过dispatch_block_cancel
来取消。
- (void)cancelAfterExecture {
self.delayBlcok = dispatch_block_create(0, ^{
NSLog(@"测试dispatch_after的取消操作");
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), self.delayBlcok);
//去下dispatch_after的操作
dispatch_block_cancel(self.delayBlcok);
}
CADisplayLink
CADisplayLink
是基于屏幕刷新的周期,所以一般很准时,Meizu秒刷新60次,其本质也是通过RunLoop,所以不难看出,当RunLoop选择其他模式或被耗时操作过多时,仍旧会造成延迟。
- 其使用步骤是 创建-> 添加到RunLoop ->终止 ->销毁。
//创建CADisplayLink定时器
self.linkTiemr = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkTest)];
//添加到RunLoop中
[self.linkTiemr addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
//终止定时器
[self.linkTiemr invalidate];
//销毁对象
self.linkTiemr = nil;
由于并非NSTimer的子类,所以使用RunLoop的添加Timer无法加入,应使用其自己的添加到RunLoop的方法,来把定时器添加到RunLoop中。
同时由于基于屏幕刷新,所以度量单位是每帧,该定时器提供了根据屏幕刷新设置时间间隔属性frameInterval
(该属性支持IOS 10.0以下),IOS 10.0以上请使用preferredFramesPerSecond
属性。其决定屏幕刷新多少帧调用一次方法,默认为1,即1/60秒调用一次。
如果我们想要计算每次调用的时间间隔,可以通过@property(readonly, nonatomic) CFTimeInterval duration;
属性求出,后者为每帧间隔的只读属性。
在日常开发中,适当使用CADisplayLink甚至有优化作用。比如对于需要动态计算进度的进度条,由于起进度反馈主要是为了UI更新,那么当计算进度的频率超过帧数时,就造成了很多无谓的计算。如果将计算进度的方法绑定到CADisplayLink上来调用,则只在每次屏幕刷新时计算进度,优化了性能。
代码地址:demo