引言

定时器:A timer waits until a certain time interval has elapsed and then fires, sending a specified message to a target object.

翻译如下:在固定的时间间隔被触发,然后给指定目标发送消息。总结为三要素吧:时间间隔、被触发、发送消息(执行方法)

按照官方的描述,我们也确实是这么用的;但是里面有很多细节,你是否了解呢?

  • 它会被添加到runloop,否则不会运行,当然添加的runloop不存在也不会运行;
  • 还要指定添加到的runloop的哪个模式,而且还可以指定添加到runloop的多个模式,模式不对也是不会运行的
  • runloop会对timer有强引用,timer会对目标对象进行强引用(是否隐约的感觉到坑了。。。)
  • timer的执行时间并不准确,系统繁忙的话,还会被跳过去
  • invalidate调用后,timer停止运行后,就一定能从runloop中消除吗,资源????

呵呵。。。下面会解决这些问题

定时器的一般用法

控制器中添加定时器,例如:

- (void)viewDidLoad {
NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1 target:self selector:@selector(timerFire) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
self.timer = timer;
} - (void)timerFire {
NSLog(@"timer fire");
}

上面的代码就是我们使用定时器最常用的方式,可以总结为2个步骤:创建,添加到runloop

系统提供了8个创建方法,6个类创建方法,2个实例初始化方法。

  • 有三个方法直接将timer添加到了当前runloop default mode,而不需要我们自己操作,当然这样的代价是runloop只能是当前runloop,模式是default mode:
+ (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;

对上面所有方法参数做个说明:

  1. ti(interval):定时器触发间隔时间,单位为秒,可以是小数。如果值小于等于0.0的话,系统会默认赋值0.1毫秒
  2. invocation:这种形式用的比较少,大部分都是block和aSelector的形式
  3. yesOrNo(rep):是否重复,如果是YES则重复触发,直到调用invalidate方法;如果是NO,则只触发一次就自动调用invalidate方法
  4. aTarget(t):发送消息的目标,timer会强引用aTarget,直到调用invalidate方法
  5. aSelector(s):将要发送给aTarget的消息,如果带有参数则应:- (void)timerFireMethod:(NSTimer *)timer声明
  6. userInfo(ui):传递的用户信息。使用的话,首先aSelector须带有参数的声明,然后可以通过[timer userInfo]获取,也可以为nil,那么[timer userInfo]就为空
  7. date:触发的时间,一般情况下我们都写[NSDate date],这样的话定时器会立马触发一次,并且以此时间为基准。如果没有此参数的方法,则都是以当前时间为基准,第一次触发时间是当前时间加上时间间隔ti
  8. block:timer触发的时候会执行这个操作,带有一个参数,无返回值

添加到runloop,参数timer是不能为空的,否则抛出异常

- (void)addTimer:(NSTimer *)timer forMode:(NSRunLoopMode)mode;

另外,系统提供了一个- (void)fire;方法,调用它可以触发一次:

  • 对于重复定时器,它不会影响正常的定时触发
  • 对于非重复定时器,触发后就调用了invalidate方法,既使正常的还没有触发

NSTimer添加到NSRunLoop

如同引言中说的那样,timer必须添加到runloop才有效,很明显要保证两件事情,一是runloop存在(运行),另一个才是添加。确保这两个前提后,还有runloop模式的问题。

一个timer可以被添加到runloop的多个模式,比如在主线程中runloop一般处于NSDefaultRunLoopMode,而当滑动屏幕的时候,比如UIScrollView或者它的子类UITableView、UICollectionView等滑动时runloop处于UITrackingRunLoopMode模式下,因此如果你想让timer在滑动的时候也能够触发,就可以分别添加到这两个模式下。或者直接用NSRunLoopCommonModes一个模式集,包含了上面的两种模式。

但是一个timer只能添加到一个runloop(runloop与线程一一对应关系,也就是说一个timer只能添加到一个线程)。如果你非要添加到多个runloop,则只有一个有效

关于强引用的问题

还是经常使用到的代码

- (void)viewDidLoad {
// 代码标记1
NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1 target:self selector:@selector(timerFire) userInfo:nil repeats:YES];
// 代码标记2
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
// 代码标记3
self.timer = timer;
} - (void)timerFire {
NSLog(@"timer fire");
}

假设代码中的视图控制器由UINavigationController管理,且self.timer是strong类型,则强引用可以表示如下:

上面有四根强引用线,它们是如何产生的呢,这个也必须搞清楚?

  • L1:这个简单,nav push 控制器的时候会强引用,即在push的时候产生;
  • L2:是在代码标记3的位置产生;
  • L3:是在代码标记1的位置产生,至此L2与L3已经产生了循环引用,虽然timer还没有添加到runloop
  • L4:是在代码标记2的位置产生

根据上图就很清晰了,我们经常说到timer与self会造成循环引用,并不是因为runloop引起,而是timer本身会对self有强引用。

invalidate方法

invalidate方法有2个功能:一是将timer从runloop中移除,那么图中的L4就消失,二是timer本身也会释放它持有资源,比如target、userinfo、block(关于block强引用self具体参考这里:http://www.cnblogs.com/mddblog/p/4754190.html),那么强引用L3就消失。如果self.timer是weak引用,也就是L2是弱引用,那么timer的引用计数就为0了,timer本身也就被释放了。如果你此时又调用addTimer:forMode:则会抛异常,因为timer为nil,因此当控制器使用weak方式引用timer时,应注意这点

之后的timer也就永远无效了,调用它的getter方法isValid返回是NO,即使你再次将它正确的添加到runloop,也不会触发,因为timer已对target、block释放了。

timer只有这一个方法可以完成此操作,所以我们取消一个timer必须要调用此方法。而在添加到runloop前,可以使用它的getter方法isValid来判断,一个是防止为nil,另一个是防止为无效。

然而就像引言中说的那个耸人听闻的问题一样,invalidate方法调用必须在timer添加到的runloop所在的线程,如果不在的话:虽然timer本身会释放掉它自己持有的资源比如target、userinfo、block,图中的L3会消失。但是runloop不会释放timer,即图中的L4不会消失,假设,self被pop了-->L1无效-->self引用计数为0,self释放-->L2也消失。此时就剩runloop、timer、L4,timer也就永远不会释放了,造成内存泄露。

下面不得不面对另一个问题,runloop退出或者本身被释放不就可以了吗???

这才真心是一个头疼的问题:是的,没错,runloop退出甚至自身释放后,L4消失,timer也就释放了。。。可以参考之前那篇关于runloop退出释放的问题NSRunLoop原理详解——不再有盲点:http://www.jianshu.com/p/4263188ed940

这里补充一点,timer没有被释放,那么它会作为runloop的输入源,从而阻止runloop的退出(runloop的退出是会释放掉timer的)。

只关心runloop的退出就好,至于释放就别深究了,或者就当它不释放(我的理解是随着线程释放而释放)

关于强引用再举个常见例子

重复的添加timer,例如下面的代码:

// 无论self.timer是strong还是weak
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:2 target:self selector:@selector(timerHandle) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
}

每点击一次屏幕就会添加一次,就会造成重复添加,你的timerHandle方法会被调用多次,添加几次就调用几次。。。

假设点击了2次屏幕,即创建2了个timer,我们标记为t1,t2。我们分析一下:第二次的时候,self.timer引用t2,虽然不在引用t1但是,runloop还在引用它,所以不会释放,不用说t2也是不会释放的。

那么如何解决呢?setter方法里面调用invalidate即可:

- (void)setTimer:(NSTimer *)timer {
[_timer invalidate];
_timer = timer;
}

其实记住两条即可

  • timer不用了,一定要调用invalidate
  • 一般是target释放的同时,才会知道timer不用了,那么怎么捕获target被释放了呢?dealloc方法肯定是不行的。如果是控制器的话可以尝试监听pop方法的调用(nav的代理),viewDidDisappear方法里面(但要记着,再次展示的时候从新添加。。。)

不调用invalidate方法,target是不会被释放的,因为图中的L4,L3一直存在

timer执行是否准时

不准时!

第一种不准时:有可能跳过去

  1. 线程处理比耗时的事情时会发生
  2. 还有就是timer添加到的runloop模式不是runloop当前运行的模式,这种情况经常发生。

对于第一种情况我们不应该在timer上下功夫,而是应该避免这个耗时的工作。那么第二种情况,作为开发者这也是最应该去关注的地方,要留意,然后视情况而定是否将timer添加到runloop多个模式

虽然跳过去,但是,接下来的执行不会依据被延迟的时间加上间隔时间,而是根据之前的时间来执行。比如:

定时时间间隔为2秒,t1秒添加成功,那么会在t2、t4、t6、t8、t10秒注册好事件,并在这些时间触发。假设第3秒时,执行了一个超时操作耗费了5.5秒,则触发时间是:t2、t8.5、t10,第4和第6秒就被跳过去了,虽然在t8.5秒触发了一次,但是下一次触发时间是t10,而不是t10.5。

第二种不准时:不准点

比如上面说的t2、t4、t6、t8、t10,并不会在准确的时间触发,而是会延迟个很小的时间,原因也可以归结为2点:

  1. RunLoop为了节省资源,并不会在非常准确的时间点触发
  2. 线程有耗时操作,或者其它线程有耗时操作也会影响

以我来讲,从来没有特别准的时间,

iOS7以后,Timer 有个属性叫做 Tolerance (时间宽容度,默认是0),标示了当时间点到后,容许有多少最大误差。

它只会在准确的触发时间到加上Tolerance时间内触发,而不会提前触发(是不是有点像我们的火车,只会晚点。。。)。另外可重复定时器的触发时间点不受Tolerance影响,即类似上面说的t8.5触发后,下一个点不会是t10.5,而是t10 + Tolerance,不让timer因为Tolerance而产生漂移(突然想起嵌入式令人头疼的温漂)。

其实对于这种不准点,对我们开发影响并不大(基本是毫秒妙级别以下的延迟),很少会用到非常准点的情况。

GCD定时器简单介绍

其实这种我们平时也经常用(一次性定时):

void dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t block);

when接受两种类型参数:dispatch_time相对时间,相对系统的时间,比如上面相对于DISPATCH_TIME_NOW;dispatch_walltime是绝对时间,比如某年月日某时分秒。。。之后由GCD帮我们计算一个相对时间。下面说下dispatch_time,支持纳秒级别

dispatch_time_t when = dispatch_time (DISPATCH_TIME_NOW, 1);// 还没这么用过1纳秒的延迟

应该很准确了,但是定时时间到后只是将block添加到指定的queue,去执行。这样的话,执行时间也是不保证的,首先执行线程要等待内核的调度,其次执行线程正好没有其它事情做。如果还需要创建线程的话,就更浪费时间了。所以这个也是不符合我们期望的

when也支持DISPATCH_TIME_NOW,但是这样就没意义了,不如直接调用dispatch_async。而至于DISPATCH_TIME_FOREVER就更。。。

重复性定时,代码示例如下:

// 需要强引用
@property (nonatomic, strong)dispatch_source_t gcdTime; - (void)gcdTimerTest {
// 这里需要强引用
self.gcdTime = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
// 开始时间支持纳秒级别
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)2 * NSEC_PER_SEC);
// 2秒执行一次
uint64_t dur = (uint64_t)(2.0 * NSEC_PER_SEC);
// 最后一个参数是允许的误差,即使设为零,系统也会有默认的误差
dispatch_source_set_timer(self.gcdTime, start, dur, 0);
// 设置回调
dispatch_source_set_event_handler(self.gcdTime, ^{
NSLog(@"---%@---%@",[NSThread currentThread],self);
});
dispatch_resume(self.gcdTime);
}

取消定时器:dispatch_cancel(self.gcdTimer);,取消后再次调用dispatch_source_set_timer是没有用的。self.gcdTimer已不可用

虽然支持纳秒级别,但是定时也是不准的,上面的例子使用的是dispatch_get_global_queue队列,执行线程也是不确定的。所以在实际开发中这种很少用,好处是它不受runloop mode限制

最新文章

  1. strace命令(收集整理,常看常新)
  2. SPFA(负环) LightOJ 1074 Extended Traffic
  3. 轮式移动机器人QBot的使用
  4. 一道 JavaScript 面试题
  5. Winform ListView根据条件定位到指定行
  6. HDU 1518
  7. [转贴]C++调用openssl 的AES加密例子
  8. kafka安装与使用
  9. php模式设计之 中介者模式
  10. 201521123093 java 第六周学习总结
  11. Flex中宽度计算
  12. 仿QQ发语音、图片选择、表情选择demo
  13. beta冲刺 用户使用调查报告
  14. Spring Boot通过命令行启动发生FileNotFoundException
  15. OO第二单元总结(多线程的电梯调度)
  16. ArrayList迭代器源码分析
  17. grep用法
  18. 【.NET】.NET MVC4 微信扫一扫功能实现-附全部代码
  19. 最长增长子序列(LIS)
  20. sql 表值函数-将一个传入的字符串用2中分隔符拆分成临时表

热门文章

  1. inheritprototype原型继承封装及综合继承最简实例
  2. iOS 之 自动释放池
  3. jquery 组合键键盘事件
  4. PKU-1704-Georgia and Bob
  5. HttpSesstionActivationLIstener示例
  6. chrome与pdf的事情
  7. SoapUI:入门实例
  8. Microsoft IoT Starter Kit 开发初体验-反馈控制与数据存储
  9. Mybatis拦截器实现分页
  10. phpstrom 的一些常用设置