iOS定时器,你真的会使用吗?
kkiw1274
8年前
<p>定时器的使用是软件开发基础技能,用于延时执行或重复执行某些方法。我相信大部分人接触iOS的定时器都是从</p> <p>[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(action:) userInfo:nil repeats:YES]这段代码开始的吧。但是你真的会用吗?</p> <h2>iOS定时器</h2> <p>首先来介绍iOS中的定时器</p> <p>iOS中的定时器大致分为这几类:</p> <ul> <li><strong>NSTimer</strong></li> <li><strong>CADisplayLink</strong></li> <li><strong>GCD定时器</strong></li> </ul> <h3>NSTimer</h3> <p>使用方法</p> <p>NSTime定时器是我们比较常使用的定时器,比较常使用的方法有两种:</p> <pre> + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo; + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;</pre> <p>这两种方法都是创建一个定时器,区别是用 timerWithTimeInterval: 方法创建的定时器需要手动加入RunLoop中。</p> <pre> // 创建NSTimer对象 NSTimer *timer = [NSTimer timerWithTimeInterval:3 target:self selector:@selector(timerAction) userInfo:nil repeats:YES]; // 加入RunLoop中 [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];</pre> <p>需要 <strong>注意</strong> 的是: UIScrollView 滑动时执行的是 UITrackingRunLoopMode , NSDefaultRunLoopMode 被挂起,会导致定时器失效,等恢复为 <strong>滑动结束</strong> 时才恢复定时器。</p> <p>举个例子:</p> <pre> - (void)startTimer{ NSTimer *UIScrollView = [NSTimer timerWithTimeInterval:0.5 target:self selector:@selector(action:) userInfo:nil repeats:YES]; [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; } - (void)action:(NSTimer *)sender { static int i = 0; NSLog(@"NSTimer: %d",i); i++; }</pre> <p>将 timer 添加到 <strong>NSDefaultRunLoopMode</strong> 中,没0.5秒打印一次,然后滑动 UIScrollView .</p> <p>打印台输出:</p> <p style="text-align: center;"><img src="https://simg.open-open.com/show/e8e7a739f75f66c75077927c040dbf48.jpg"></p> <p>可以看出在滑动 UIScrollView 时,定时器被暂停了。</p> <p>所以如果需要定时器在 UIScrollView 拖动时也不影响的话,有两种解决方法</p> <ol> <li> <p>timer分别添加到 UITrackingRunLoopMode 和 NSDefaultRunLoopMode 中</p> <pre> [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; [[NSRunLoop mainRunLoop] addTimer:timer forMode: UITrackingRunLoopMode];</pre> </li> <li> <p>直接将 <strong>timer</strong> 添加到 NSRunLoopCommonModes 中:</p> <pre> [[NSRunLoop mainRunLoop] addTimer:timer forMode: NSRunLoopCommonModes];</pre> </li> </ol> <p>但并不是都 <strong>timer</strong> 所有的需要在滑动 UIScrollView 时继续执行,比如使用 <strong>NSTimer</strong> 完成的帧动画,滑动 UIScrollView 时就可以停止帧动画,保证滑动的流程性。</p> <p>若没有特殊要求的话,一般使用第二种方法创建完 <strong>timer</strong> ,会自动添加到 NSDefaultRunLoopMode 中去执行,也是平时最常用的方法。</p> <pre> NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(action:) userInfo:nil repeats:YES];</pre> <p>参数:</p> <p>TimeInterval :延时时间</p> <p>target :目标对象,一般就是 self 本身</p> <p>selector :执行方法</p> <p>userInfo :传入信息</p> <p>repeats :是否重复执行</p> <p>以上创建的定时器,若 repeats 参数设为 NO ,执行一次后就会被释放掉;</p> <p>若 repeats 参数设为 YES 重复执行时,必须手动关闭,否则定时器不会释放(停止)。</p> <p>释放方法:</p> <pre> // 停止定时器 [timer invalidate];</pre> <p>实际开发中,我们会将 NSTimer 对象设置为属性,这样方便释放。</p> <p>iOS10.0推出了两个新的API,与上面的方法相比, selector 换成Block回调以、减少传入的参数(那几个参数真是鸡肋)。不过开发中一般需要适配低版本,还是尽量使用上面的方法吧。</p> <pre> + (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)); + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));</pre> <h3>特点</h3> <ul> <li> <p>必须加入Runloop</p> <p>上面不管使用哪种方法,实际最后都会加入RunLoop中执行,区别就在于是否手动加入而已。</p> </li> <li> <p>存在延迟</p> <p>不管是一次性的还是周期性的timer的实际触发事件的时间,都会与所加入的RunLoop和RunLoop Mode有关,如果此RunLoop正在执行一个连续性的运算,timer就会被延时出发。重复性的timer遇到这种情况,如果延迟超过了一个周期,则会在延时结束后立刻执行,并按照之前指定的周期继续执行,这个延迟时间大概为50-100毫秒.</p> <p>所以NSTimer不是绝对准确的,而且中间耗时或阻塞错过下一个点,那么下一个点就pass过去了.</p> </li> <li> <p>UIScrollView滑动会暂停计时</p> <p>添加到 NSDefaultRunLoopMode 的 timer 在 UIScrollView 滑动时会暂停,若不想被 UIScrollView 滑动影响,需要将 timer 添加再到 UITrackingRunLoopMode 或 直接添加到 NSRunLoopCommonModes 中</p> </li> </ul> <h2>CADisplayLink</h2> <p>CADisplayLink官方介绍:</p> <p>A CADisplayLink object is a timer object that allows your application to synchronize its drawing to the refresh rate of the display</p> <p>CADisplayLink对象是一个和屏幕刷新率同步的定时器对象。每当屏幕显示内容刷新结束的时候,runloop就会向CADisplayLink指定的 target 发送一次指定的 selector 消息, CADisplayLink类对应的 selector 就会被调用一次。</p> <p>从原理上可以看出,CADisplayLink适合做界面的不停重绘,比如视频播放的时候需要不停地获取下一帧用于界面渲染,或者做动画。</p> <h3>使用方法</h3> <p>创建:</p> <pre> @property (nonatomic, strong) CADisplayLink *displayLink; self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDisplayLink:)]; // 每隔1帧调用一次 self.displayLink.frameInterval = 1; [self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];</pre> <p>释放方法:</p> <pre> [self.displayLink invalidate]; self.displayLink = nil;</pre> <p>当把 <strong>CADisplayLink</strong> 对象添加到runloop中后, selector 就能被周期性调用,类似于重复的NSTimer被启动了;执行 invalidate 操作时,CADisplayLink对象就会从runloop中移除, selector 调用也随即停止,类似于NSTimer的 invalidate 方法。</p> <p>CADisplayLink中有两个重要的属性:</p> <ul> <li> <p>frameInterval</p> <p>NSInteger类型的值,用来设置间隔多少帧调用一次 selector 方法,默认值是1,即每帧都调用一次。</p> </li> <li> <p>duration</p> <p>CFTimeInterval 值为 readOnly ,表示两次屏幕刷新之间的时间间隔。需要注意的是,该属性在 targe t的 selector 被首次调用以后才会被赋值。 selector 的调用间隔时间计算方式是: <strong>调用间隔时间 = duration × frameInterval</strong> 。</p> </li> </ul> <h3>特点</h3> <ul> <li> <p>刷新频率固定</p> <p>正常情况iOS设备的屏幕刷新频率是固定 <strong>60Hz</strong> ,如果CPU过于繁忙,无法保证屏幕60次/秒的刷新率,就会导致跳过若干次调用回调方法的机会,跳过次数取决CPU的忙碌程度。</p> </li> <li> <p>屏幕刷新时调用</p> <p>CADisplayLink在正常情况下会在每次刷新结束都被调用,精确度相当高。但如果调用的方法比较耗时,超过了屏幕刷新周期,就会导致跳过若干次回调调用机会</p> </li> <li> <p>适合做界面渲染</p> <p>CADisplayLink可以确保系统渲染每一帧的时候我们的方法都被调用,从而保证了动画的流畅性。</p> </li> </ul> <h2>GCD定时器</h2> <p>GCD定时器和NSTimer是不一样的,NSTimer受RunLoop影响,但是GCD的定时器不受影响,因为通过源码可知RunLoop也是基于GCD的实现的,所以GCD定时器有非常高的精度。</p> <h3>使用方法</h3> <p>创建GCD定时器定时器的方法稍微比较复杂,看下面的代码:</p> <p>单次的延时调用</p> <p>NSObject中的 performSelector:withObject:afterDelay: 以及 performSelector:withObject:afterDelay:inModes: 这两个方法在调用的时候会设置当前 runloop 中 timer ,前者设置的 timer 在 NSDefaultRunLoopMode 运行,后者则可以指定 <strong>NSRunLoop</strong> 的 mode 来执行。我们上面介绍过 runloop 中 timer 在 UITrackingRunLoopMode 被挂起,就导致了代码就会一直等待 timer 的调度,解决办法在上面也有说明。</p> <p>不过我们可以用另一套方案来解决这个问题,就是使用GCD中的 dispatch_after 来实现单次的延时调用:</p> <pre> double delayInSeconds = 2.0; dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ [self someMethod]; });</pre> <p>循环调用</p> <pre> // 创建GCD定时器 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_source_t _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); dispatch_source_set_timer(_timer, dispatch_walltime(NULL, 0), 1.0 * NSEC_PER_SEC, 0); //每秒执行 // 事件回调 dispatch_source_set_event_handler(_timer, ^{ dispatch_async(dispatch_get_main_queue(), ^{ // 在主线程中实现需要的功能 } } }); // 开启定时器 dispatch_resume(_timer); // 挂起定时器(dispatch_suspend 之后的 Timer,是不能被释放的!会引起崩溃) dispatch_suspend(_timer); // 关闭定时器 dispatch_source_cancel(_timer);</pre> <p>上面代码中要注意的是:</p> <ol> <li>dispatch_source_set_event_handler() 中的任务实在子线程中执行的,若需要回到主线程,要调用 dispatch_async(dispatch_get_main_queue(), ^{} .</li> <li>dispatch_source_set_timer 中第二个参数,当我们使用 dispatch_time 或者 DISPATCH_TIME_NOW 时,系统会使用默认时钟来进行计时。然而当系统休眠的时候,默认时钟是不走的,也就会导致计时器停止。使用 dispatch_walltime 可以让计时器按照真实时间间隔进行计时.</li> <li>第三个参数, 1.0 * NSEC_PER_SEC 为每秒执行一次,对应的还有毫秒,分秒,纳秒可以选择.</li> </ol> <ul> <li>dispatch_source_set_event_handler 这个函数在执行完之后,block 会立马执行一遍,后面隔一定时间间隔再执行一次。而 NSTimer 第一次执行是到计时器触发之后。这也是和 NSTimer 之间的一个显著区别。</li> <li>挂起(暂停)定时器, dispatch_suspend 之后的 Timer ,不能被释放的,会引起崩溃.</li> <li>创建的 timer 一定要有 dispatch_suspend(_timer) 或 dispatch_source_cancel(_timer) 这两句话来指定出口,否则定时器将不执行,若我们想无限循环可将 dispatch_source_cancel(_timer) 写在一句永不执行的 if 判断语句中。</li> </ul> <h2>使用场景</h2> <p>介绍完iOS中的各种定时器,接下来我们来说说这几种定时器在开发中的几种用法。</p> <h3>短信重发倒计时</h3> <p>短信倒计时使我们登录注册常用的功能,一般设置为60s,实现方法如下:</p> <pre> // 计时时间 @property (nonatomic, assign) int timeout; /** 开启倒计时 */ - (void)startCountdown { if (_timeout > 0) { return; } _timeout = 60; // GCD定时器 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_source_t _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); dispatch_source_set_timer(_timer, dispatch_walltime(NULL, 0), 1.0 * NSEC_PER_SEC, 0); //每秒执行 dispatch_source_set_event_handler(_timer, ^{ if(_timeout <= 0 ){// 倒计时结束 // 关闭定时器 dispatch_source_cancel(_timer); dispatch_async(dispatch_get_main_queue(), ^{ //设置界面的按钮显示 根据自己需求设置 [self.sendMsgBtn setTitle:@"发送" forState:UIControlStateNormal]; self.sendMsgBtn.enabled = YES; }); }else{// 倒计时中 // 显示倒计时结果 NSString *strTime = [NSString stringWithFormat:@"重发(%.2d)", _timeout]; dispatch_async(dispatch_get_main_queue(), ^{ //设置界面的按钮显示 根据自己需求设置 [self.sendMsgBtn setTitle:[NSString stringWithFormat:@"%@",strTime] forState:UIControlStateNormal]; self.sendMsgBtn.enabled = NO; }); _timeout--; } }); // 开启定时器 dispatch_resume(_timer); }</pre> <p>在上面代码中,我们设置了一个60s循环倒计时,当我们向服务器获取短信验证码成功时 调用该方法开始倒计时。每秒刷新按钮的倒计时数,倒计时结束时再将按钮 Title 恢复为“发送”.</p> <p>有一点需要注意的是,按钮的样式要设置为 <strong>UIButtonTypeCustom</strong> ,否则会出现刷新 Title 时闪烁.</p> <p>我们可以把这个方法封装一下,方便调用,否则在控制器中写这么一大段代码确实也不优雅。</p> <p>效果如下:</p> <p style="text-align: center;"><img src="https://simg.open-open.com/show/24ec946328dad9451df3644acf2d638e.jpg"></p> <p> </p> <h3>每个几分钟向服务器发送数据</h3> <p>在有定位服务的APP中,我们需要每个一段时间将定位数据发送到服务器,比如每5s定位一次每隔5分钟将再统一将数据发送服务器,这样会处理比较省电。</p> <p>一般程序进入后台时,定时器会停止,但是在定位APP中,需要持续进行定位,APP在后台时依旧可以运行,所以在后台定时器也是可以运行的。</p> <p>在使用GCD定时的时候发现GCD定时器也可以在后代运行,创建方法同上面的短信倒计时.</p> <p>这里我们使用 <strong>NSTimer</strong> 来创建一个每个5分钟执行一次的定时器.</p> <pre> #import <Foundation/Foundation.h> typedef void(^TimerBlock)(); @interface BYTimer : NSObject - (void)startTimerWithBlock:(TimerBlock)timerBlock; - (void)stopTimer; @end</pre> <pre> #import "BYTimer.h" @interface BYTimer () @property (nonatomic, strong) NSTimer *timer; @property (nonatomic, strong) TimerBlock timerBlock; @end @implementation BYTimer - (void)startTimerWithBlock:(TimerBlock)timerBlock { self.timer = [NSTimer timerWithTimeInterval:300 target:self selector:@selector(_timerAction) userInfo:nil repeats:YES]; [[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes]; _timerBlock = timerBlock; } - (void)_timerAction { if (self.timerBlock) { self.timerBlock(); } } - (void)stopTimer { [self.timer invalidate]; } @end</pre> <p>该接口的实现很简单,就是 <strong>NSTimer</strong> 创建了一个300s执行一次的定时器,但是要注意定时器需要加入 NSRunLoopCommonModes 中。</p> <p> </p> <p> </p> <p>来自:http://www.jianshu.com/p/c167ca4d1e7e</p> <p> </p>