美文网首页iOS开发
iOS开发 计时器的实现方式

iOS开发 计时器的实现方式

作者: superDg | 来源:发表于2017-10-14 14:02 被阅读81次

    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
    • 如果设置了repeatstrue,每隔一定的时间会重复执行aSelector,直到手动调用invalidate方法。repeatsfalse,执行一次之后系统会自动调用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
    • 如果设置了repeatstrue,每隔一定的时间会重复执行aSelector,直到手动调用invalidate方法。repeatsfalse,执行一次之后系统会自动调用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 对象中都是强引用。targetuserInfo,添加到RunLoop中也是强引用timer
    • timer 对象调用invalidate方法后,会从RunLoop中移除引用,timer不会执行对应的方法或者blocktimer会移除RunLoop的引用和userInfotarget的引用,也是为一个方法可以移除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持有timerRunLoop持有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参数中的循环引用即可。

    相关文章

      网友评论

        本文标题:iOS开发 计时器的实现方式

        本文链接:https://www.haomeiwen.com/subject/loyxuxtx.html