iOS: 倒计时三种方式

2019-11-09  本文已影响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倒计时的探究与选择

上一篇下一篇

猜你喜欢

热点阅读