NSTimer定时器进阶——详细介绍,循环引用分析与解决

引言

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

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

timer的强引用

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

  • 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限制

转载于:https://www.cnblogs.com/mddblog/p/6517377.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/269873.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

HTML - 超文本标记语言 (Hyper Text Markup Language)

HTML - 超文本标记语言 (Hyper Text Markup Language) HTML是建设网站/网页制作主要语言。 HTML是一种易于学习的标记语言。 HTML使用像 <p> 尖括号内标记标签来定义网页的内容&#xff1a; HTML 实例 <html><body><h1>My First Heading</h1><…

AOP切入同类调用方法不起作用,AopContext.currentProxy()帮你解决这个坑

原来在springAOP的用法中&#xff0c;只有代理的类才会被切入&#xff0c;我们在controller层调用service的方法的时候&#xff0c;是可以被切入的&#xff0c;但是如果我们在service层 A方法中&#xff0c;调用B方法&#xff0c;切点切的是B方法&#xff0c;那么这时候是不会切…

AopContext.currentProxy();为什么能获取到代理对象

在同一个类中&#xff0c;非事务方法A调用事务方法B&#xff0c;事务失效&#xff0c;得采用AopContext.currentProxy().xx()来进行调用&#xff0c;事务才能生效。 B方法被A调用&#xff0c;对B方法的切入失效&#xff0c;但加上AopContext.currentProxy()创建了代理类&#x…

@Async注解导致循环依赖,BeanCurrentlyInCreationException异常

使用Async异步注解导致该Bean在循环依赖时启动报BeanCurrentlyInCreationException异常的根本原因分析&#xff0c;以及提供解决方案 今天在自己项目中使用Async的时候&#xff0c;碰到了一个问题&#xff1a;Spring循环依赖&#xff08;circular reference&#xff09;问题。 …

人工智能:图像数字化相关的知识介绍

❤️作者主页&#xff1a;IT技术分享社区 ❤️作者简介&#xff1a;大家好,我是IT技术分享社区的博主&#xff0c;从事C#、Java开发九年&#xff0c;对数据库、C#、Java、前端、运维、电脑技巧等经验丰富。 ❤️个人荣誉&#xff1a; 数据库领域优质创作者&#x1f3c6;&#x…

《深入理解Java虚拟机》读书笔记

堆分配参数&#xff1a; -XX:PrintGC 使用该参数&#xff0c;虚拟机启动后&#xff0c;只要遇到GC就会打印日志&#xff1b; -XX&#xff1a;UseSerialGC 配置串行回收器&#xff1b; -XX&#xff1a;PrintGCDeltails 可以查看详细信息&#xff0c;包括各个区的情况 -Xms&#…

线程可见性和关键字volatile

线程可见性 可以看到程序变量running没volatile是死循环 加了volatile成功输出 public class VolitaleTest {private static volatile boolean running true;public static void main(String[] args) {Thread thread new Thread(() ->{long i 0L;while (running){i;}Sys…

每秒钟承载600万订单级别的无锁并行计算框架 Disruptor学习

1.来源 Disruptor是英国外汇交易公司LMAX开发的一个高性能队列&#xff0c;研发的初衷是解决内部的内存队列的延迟问题&#xff0c;而不是分布式队列。基于Disruptor开发的系统单线程能支撑每秒600万订单&#xff0c;2010年在QCon演讲后&#xff0c;获得了业界关注。 2.应用背…

logisim输出变成红色的e_新车实拍解析 福特Mustang Mach-E亮点实拍图解

福特Mustang Mach-E新车主要针对造型设计对外进行了首次亮相发布&#xff0c;对新车内饰以及具体新车方面的数据信息暂未公布。如果消费者想要了解这款新车&#xff0c;大家可以继续关注《杨总继续观察》带来这款新车的详细报道。新车在设计上可以看作是一款福特野马的电动跨界…

castle windsor学习----- Services and Components 两者的定义

转载于:https://www.cnblogs.com/lanpingwang/p/6534208.html

html5 接收蓝牙广播_蓝牙定位技术浅析(化工厂应用)

蓝牙定位基于RSSI(Received Signal Strength Indication&#xff0c;信号场强指示)定位原理。根据定位端的不同&#xff0c;蓝牙定位方式分为网络侧定位和终端侧定位。由于蓝牙由于是近场通信其定位精度取决于点位的部署密度&#xff0c;一般会设计成7-8米一个定位基站&#xf…

catia如何整列加工_”模具加工“最全面的诠释,你真的都懂了吗?

1定义模具加工(Mold Making)是指成型和制坯工具的加工&#xff0c;此外还包括剪切模和模切模具。通常情况下&#xff0c;模具有上模和下模两部分组成。将钢板放置在上下模之间&#xff0c;在压力机的作用下实现材料的成型&#xff0c;当压力机打开时&#xff0c;就会获得由模具…

mysql中基本的DDL语句(关注一下,以后会继续更新喔!)

一.简单介绍sql 一.简单介绍sql &#xff08;1&#xff09;结构化查询语言&#xff08;Structured Query Language,SQL&#xff09;是一种有特殊目的的编程语言&#xff0c;也是一种数据库查询和一些程序设计语言&#xff0c;专用于存储及调用数据以及查询、更新和管理关系数…

[ST2017] Lab1: Triangle type and Junit test

Lab1 3014218071 王汉超 Request: Write a program with a function(getTriangleStatus) and test it, which takes three double number l1, l2, l3 as lengths of triangle sides and calculates whether the triangle is equilateral, isosceles, or scalene. Contents: 一.…

说说图片加载不出来_晒晒我的新家,朋友都说好看,恨不得天天宅在卧室不出来...

我和老婆是高中同学&#xff0c;早年因为没有考上大学&#xff0c;我早早的来北京闯事业&#xff0c;也吃了很多苦&#xff0c;好在那时机会很多&#xff0c;做什么都能赚点钱&#xff0c;生活质量也逐渐好了起来&#xff0c;配得上我老婆这样的大学生&#xff0c;今年和老婆结…