网站建设多钱,生产企业网站欣赏,东莞网站的优化,无锡手机网站制作费用引言 定时器#xff1a;A timer waits until a certain time interval has elapsed and then fires, sending a specified message to a target object. 翻译如下#xff1a;在固定的时间间隔被触发#xff0c;然后给指定目标发送消息。总结为三要素吧#xff1a;时间间隔、… 引言 定时器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; 对上面所有方法参数做个说明 ti(interval)定时器触发间隔时间单位为秒可以是小数。如果值小于等于0.0的话系统会默认赋值0.1毫秒invocation这种形式用的比较少大部分都是block和aSelector的形式yesOrNo(rep)是否重复如果是YES则重复触发直到调用invalidate方法如果是NO则只触发一次就自动调用invalidate方法aTarget(t)发送消息的目标timer会强引用aTarget直到调用invalidate方法aSelector(s)将要发送给aTarget的消息,如果带有参数则应- (void)timerFireMethod:(NSTimer *)timer声明userInfo(ui)传递的用户信息。使用的话首先aSelector须带有参数的声明然后可以通过[timer userInfo]获取也可以为nil那么[timer userInfo]就为空date触发的时间一般情况下我们都写[NSDate date]这样的话定时器会立马触发一次并且以此时间为基准。如果没有此参数的方法则都是以当前时间为基准第一次触发时间是当前时间加上时间间隔tiblocktimer触发的时候会执行这个操作带有一个参数无返回值添加到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类型则强引用可以表示如下 上面有四根强引用线它们是如何产生的呢这个也必须搞清楚 L1这个简单nav push 控制器的时候会强引用即在push的时候产生L2是在代码标记3的位置产生L3是在代码标记1的位置产生至此L2与L3已经产生了循环引用虽然timer还没有添加到runloopL4是在代码标记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、L4timer也就永远不会释放了造成内存泄露。 下面不得不面对另一个问题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:(NSSetUITouch * *)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是不会被释放的因为图中的L4L3一直存在 timer执行是否准时 不准时 第一种不准时有可能跳过去 线程处理比耗时的事情时会发生还有就是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点 RunLoop为了节省资源并不会在非常准确的时间点触发线程有耗时操作或者其它线程有耗时操作也会影响 以我来讲从来没有特别准的时间 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_NOWdispatch_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