美文网首页
iOS: 倒计时三种方式

iOS: 倒计时三种方式

作者: 哈布福禄克 | 来源:发表于2019-11-09 20:57 被阅读0次

    1、NSTimer

    它在Foundation框架下的NSTimer.h文件下。
    一个NSTimer的对象只能注册在一个RunLoop当中,但是可以添加到多个RunLoop Mode当中。
    NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 Toll-Free Bridging 的。它的底层是由XNU 内核的 mk_timer来驱动的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。

    在文件中,系统提供了一共8个方法,其中三个方法是直接将timer添加到了当前runloop 的DefaultMode,而不需要我们自己操作,当然这样的代价是runloop只能是当前runloop,模式是DefaultMode:

    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;
    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
    

    其他五个方法,是不会自动添加到RunLoop的,还需要调用addTimer:forMode::

    + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
    + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
    + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;
    - (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(id)ui repeats:(BOOL)rep;
    - (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
    

    NSTimer其实算不上一个真正的时间机制。它只有在被加入到RunLoop的时候才能触发。
    假如在一个RunLoop下没能检测到定时器,那么它会在下一个RunLoop中检查,并不会延后执行。换个说法,我们可以理解为:“这趟火车没赶上,等下一班吧”。
    另外,有时候RunLoop正在处理一个很费事的操作,比如说遍历一个非常非常大的数组,那么也可能会“忘记”查看定时器了。这么我们可以理解为“火车晚点了”。
    当然,这两种情况表现起来其实都是NSTimer不准确。
    所以,真正的定时器触发时间不是自己设定的那个时间,而是可能加入了一个RunLoop的触发时间。并且,NSRunLoop算不上真正的线程安全,假如NSTimer没有在一个线程中操作,那么可能会触发不可意料的后果。

        // 定义一个定时器,约定两秒之后调用self的run方法
        NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    
        // 将定时器添加到当前RunLoop的NSDefaultRunLoopMode下
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    

    2、PerformSelecter方式实现

    这个方法在Foundation框架下的NSRunLoop.h文件下。当我们调用NSObject 这个方法的时候,在runloop的内部是会创建一个Timer并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。而且还有几个很大的缺陷:

    这个方法必须在NSDefaultRunLoopMode下才能运行

    因为它基于RunLoop实现,所以可能会造成精确度上的问题。
    这个问题在其他两个方法上也会出现,所以我们下面细说

    内存管理上非常容易出问题。
    当我们执行 [self performSelector: afterDelay:]的时候,系统会将self的引用计数加1,执行完这个方法时,还会将self的引用计数减1,当方法还没有执行的时候,要返回父视图释放当前视图的时候,self的计数没有减少到0,而导致无法调用dealloc方法,出现了内存泄露。

    因为它有如此之多的缺陷,所以我们不应该使用它,或者说,不应该在倒计时这方法使用它。

    #pragma mark 线程Thread方式实现
    - (IBAction)firstBtnAction:(id)sender {
        //创建一个后台线程执行计时操作
        [self performSelectorInBackground:@selector(timerThread) withObject:nil];
    }
    
    - (void)timerThread {
        for (int i = TIMECOUNT; i >= 0 ; i--) {
            self.count--;
            //切换到主线程中更新UI
            [self performSelectorOnMainThread:@selector(updateFirstBtn) withObject:nil waitUntilDone:YES];
            sleep(1);
        }
    }
    

    3、GCD

    定时器选择了GCD的dispatch_source_t,没有选择平时最常用的定时器NSTime,因为Timer有很多缺点,如

    ①循环引用导致内存泄漏
    ②因为受runloop影响定时可能不准确
    ③代码繁多

    使用dispatch_source_t有效的避免上面问题

    ①将self作为传入方法,避免了循环引用
    ②底层语言实现,不依赖runloop不会出现线程拥堵导致的定时不准确问题
    ③block块代码看起来更简洁,方便管理

    具体代码如下

    /**
     1、获取或者创建一个队列,一般情况下获取一个全局的队列
     2、创建一个定时器模式的事件源
     3、设置定时器的响应间隔
     4、设置定时器事件源的响应回调,当定时事件发生时,执行此回调
     5、启动定时器事件
     6、取消定时器dispatch源,【必须】
     */
    #pragma mark GCD实现
    - (IBAction)thirdBtnAction:(id)sender {
        __block NSInteger second = TIMECOUNT;
        //(1)
        dispatch_queue_t quene = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        //(2)
        dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, quene);
        //(3)
        dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC, 1 * NSEC_PER_SEC);//间隔10秒,误差1秒
        //(4)
        dispatch_source_set_event_handler(timer, ^{
            dispatch_async(dispatch_get_main_queue(), ^{
                if (second == 0) {
                    self.thirdBtn.userInteractionEnabled = YES;
                    [self.thirdBtn setTitle:[NSString stringWithFormat:@"点击获取验证码"] forState:UIControlStateNormal];
                    second = TIMECOUNT;
                    //(6)
                    dispatch_cancel(timer);
                } else {
                    self.thirdBtn.userInteractionEnabled = NO;
                    [self.thirdBtn setTitle:[NSString stringWithFormat:@"%ld秒后重新获取",second] forState:UIControlStateNormal];
                    second--;
                }
            });
        });
        //(5)
        dispatch_resume(timer);
    }
    

    参考文章:iOS倒计时的探究与选择

    相关文章

      网友评论

          本文标题:iOS: 倒计时三种方式

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