iOS中计时器常用的有两种方式
使用NSTimer类(Swift 中更名为 Timer)
NSTimer 常用的初始化方法区别
方法一
objc
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;
swift
class func scheduledTimer(timeInterval ti: TimeInterval, target aTarget: Any, selector aSelector: Selector, userInfo: Any?, repeats yesOrNo: Bool) -> Timer
- 创建一个创建一个
NSTimer
对象,并添加到当前的RunLoop
中,使用默认模式RunLoop.current.add(timer, forMode: .defaultRunLoopMode)
,不需要手动fire
,系统会自动执行绑定的aSelector
- 如果设置了
repeats
为true
,每隔一定的时间会重复执行aSelector
,直到手动调用invalidate
方法。repeats
为false
,执行一次之后系统会自动调用invalidate
方法二
objc
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;
swift
init(timeInterval ti: TimeInterval, target aTarget: Any, selector aSelector: Selector, userInfo: Any?, repeats yesOrNo: Bool)
- 创建一个创建一个
NSTimer
对象,需要手动添加到RunLoop
中,手动添加到RunLoop
中之后开始计时,执行aSelector
- 如果设置了
repeats
为true
,每隔一定的时间会重复执行aSelector
,直到手动调用invalidate
方法。repeats
为false
,执行一次之后系统会自动调用invalidate
方法三 需要iOS 10以上
objc
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
swift
init(timeInterval interval: TimeInterval, repeats: Bool, block: @escaping (Timer) -> Void)
- 这个方法和方法二一样,使用
block
回调。block
中自带了NSTimer
参数,避免循环引用
方法四 需要iOS 10以上
objc
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
swift
class func scheduledTimer(withTimeInterval interval: TimeInterval, repeats: Bool, block: @escaping (Timer) -> Void) -> Timer
- 这个方法和方法一一样,使用
block
回调。block
中自带了NSTimer
参数,避免循环引用
注释
-
timer
对象中都是强引用。target
、userInfo
,添加到RunLoop中也是强引用timer
。 -
timer
对象调用invalidate
方法后,会从RunLoop
中移除引用,timer不会执行对应的方法或者block
,timer
会移除RunLoop
的引用和userInfo
、target
的引用,也是为一个方法可以移除timer
引用的方法。 - 官方特别注释
-
You must send this message from the thread on which the timer was installed. If you send this message from another thread, the input source associated with the timer may not be removed from its run loop, which could prevent the thread from exiting properly.
大致意思就是计时器在那个线程上创建开始的,就需要在那个线程上调用此方法,不过一般我都是在主线程上创建timer
,所以在这也没遇到什么坑 -
fire
方法只适合触发重复的计时器,并且不会重置计时器的计时时间。如果不是重复的计时器,触发之后会自动失效,不管计时器时间有没有到 - 官方注释如下:
You can use this method to fire a repeating timer without interrupting its regular firing schedule. If the timer is non-repeating, it is automatically invalidated after firing, even if its scheduled fire date has not arrived.
Timer 内存管理
Timer
对参数的引用都是强引用,而且添加到RunLoop
中也是强引用。在ViewController
中,controller
持有timer
,RunLoop
持有timer
,一般在dealloc
中调用timer.invalidate
,但是此时RunLoop
仍然持有timer
,并不会走dealloc
,导致ViewController
无法释放。要求不要的可以在viewWillDisappear:
中调用timer.invalidate
。这也只能在当前页面使用定时器,离开了这个页面就不行了。网上有折中的方法就是ViewController
不持有timer
,借助单例类来实现,这只能算折中的方法。在下方补充中有一种解决方案。
使用GCD
GCD在swift3
以后改动很大,所以分开来说
objc
uint64_t interval = 5 * NSEC_PER_SEC;
//创建一个专门执行timer回调的GCD队列
dispatch_queue_t queue = dispatch_queue_create("timerQueue", 0);
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
//使用dispatch_source_set_timer函数设置timer参数
dispatch_source_set_timer(_timer, dispatch_time(DISPATCH_TIME_NOW, 0), interval, 0);
//设置回调
__weak __typeof(self) ws = self
dispatch_source_set_event_handler(_timer, ^(){ [ws sendRequest]; });
dispatch_resume(_timer);
dealloc
方法
- (void)dealloc
{
if (_timer != NULL) {
dispatch_cancel(_timer);
_timer = NULL;
}
}
- 使用
dispatch_source_create
方法生成dispatch_source_t
实例,并且设置回调。 -
dispatch_resume(_timer);
执行这个方法timer
才会开始,默认是suspend
暂停、挂起状态 -
dispatch_suspend(_timer)
;暂停timer
,计时器出于暂停状态,重新启用调用dispatch_resume(_timer);
swift
var ti : DispatchSourceTimer!
ti = DispatchSource.makeTimerSource(flags:DispatchSource.TimerFlags.init(rawValue: 0) , queue: nil);
ti.schedule(deadline: DispatchTime.now(), repeating: 2.0)
ti.setEventHandler { [weak self] in self?.timerAction() }
ti.resume()
deinit { if ti != nil { ti.suspend() } }
-
ti.resume()
开始计数器,否则不是主动开始 -
ti.suspend()
暂停、挂起计时器,重新开始ti.resume()
总结
如果在一个页面里面需要使用定时器,离开页面就停止,个人倾向于使用NSTimer
类实现。如果离开页面不能停止还需要继续执行,使用GCD好一点吧,毕竟不会出现内存问题。折中的方法个人是不喜欢的。大家在项目中一定要具体问题具体对待,实现方法有很多,选择自己认为比较好的,没有问题的才是正确的。
补充 2017-12-19
- 对于NSTimer循环引用的问题可以使用分类解决,代码如下
- 新建一个NSTimer Schedule分类,实现如下方法。
+ (NSTimer *) timerWithSchedule:(NSTimeInterval)ti block:(void(^)())block userInfo:(id)userInfo repeats:(BOOL)yesOrNo{ NSMutableDictionary *info = [NSMutableDictionary dictionaryWithCapacity:2]; if (userInfo) { info[@"userInfo"] = userInfo; } info[@"timerBlock"] = block; NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:ti target:self selector:@selector(timerSelector:) userInfo:info repeats:yesOrNo]; return timer; }
+ (void)timerSelector:(NSTimer *)timer { void(^infoBlock)() = timer.userInfo[@"timerBlock"]; if (infoBlock) { infoBlock(); } }
使用 分类中的初始化方法进行NSTimer的初始化即可,只需避免block参数中的循环引用即可。