博文地址:http://ifujun.com/ios-timer-pan-dian/html
在iOS的开发过程当中,Timer是一个很常见的功能。苹果提供给了咱们好几种能够达到Timer效果的方法,我尝试在这里盘点一下。ios
NSTimer
是咱们最多见的一种Timer,咱们从NSTimer
开始提及。git
NSTimer
的用法很简单,我的比较经常使用的是下面这个方法:github
[NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:@selector(test) userInfo:nil repeats:nil];
有这么一道面试题,题目是这样的:面试
UITableViewCell上有个UILabel,显示NSTimer实现的秒表时间,手指滚动cell过程当中,label是否刷新,为何?数据结构
咱们来试验一下:多线程
经过试验,咱们发现,在手拖拽或者滑动的过程当中,label并无更新,NSTimer
也没有循环。app
那这是为何呢?这与RunLoop有关。在NSTimer的官方文档上,苹果是这么说的:框架
A timer is not a real-time mechanism; it fires only when one of the run loop modes to which the timer has been added is running and able to check if the timer’s firing time has passed.less
意思就是说,NSTimer并非一种实时机制,它只会在下面条件知足的状况下才会启动:
NSTimer被添加到的RunLoop模式正在运行。
NSTimer设定的启动时间尚未过去。
关键在于第一点,咱们刚才的NSTimer
默认添加在NSDefaultRunLoopMode
上,而UIScrollView
在滑动的时候,RunLoop会自动切换到 UITrackingRunLoopMode
,NSTimer
并无添加到这个RunLoop模式上,天然是不会启动的。
因此,若是咱们想要NSTimer
在UIScrollView
滑动的时候也会启动的话,只要将NSTimer
添加到NSRunLoopCommonModes
上便可。NSRunLoopCommonModes
是RunLoop模式的集合。
咱们试验一下:
self.timer = [NSTimer scheduledTimerWithTimeInterval:0.8f target:self selector:@selector(autoIncrement) userInfo:nil repeats:YES]; [[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
在上面的为什么中止的分析中,咱们了解到,NSTimer
只会在它所加入的RunLoop上启动和循环。若是在相似于上面的状况,而NSTimer
又只是加入NSDefaultRunLoopMode
的话,这时候的NSTimer
在切换RunLoop模式以后,必然没有精确可言。
那么,若是RunLoop一直运行在NSTimer
被加入到的模式上,或者加入到的是NSRunLoopCommonModes
的模式上,是否就是精确的呢?
首先,咱们假设线程正在进行一个比较大的连续运算,这时候,咱们的NSTimer
会被准时启动吗?
我在程序中每0.5秒打印一下”test”,而后用很慢的办法去计算质数,运行结果以下:
在计算质数的过程当中,线程彻底阻塞,并不打印”test”,等到执行完成才开始打印,第一个”start”和第二个”test”中间间隔了13秒。
因此NSTimer
是否精确,很大程度上取决于线程当前的空闲状况。
除此以外,还有一点我想说起一下。
在NSTimer的头文件中,苹果新增了一个属性,叫作tolerance
,咱们能够理解为容差。苹果的意思是若是设定了tolerance
值,那么:
设定时间 <= NSTimer的启动时间 <= 设定时间 + tolerance
那么,这个有什么用呢,由于通常来讲,咱们想要的就是精确。苹果的解释是:
Setting a tolerance for a timer allows it to fire later than the scheduled fire date, improving the ability of the system to optimize for increased power savings and responsiveness.
意思就是,设定容差能够起到省电和优化系统响应性的做用。
tolerance
若是不设定的话,默认为0。那么,是否必定能够精确呢?苹果在头文件中提到了这么一点:
The system reserves the right to apply a small amount of tolerance to certain timers regardless of the value of this property.
意思就是,哪怕为0,系统依然有权利去设置一个很小的容差。
Even a small amount of tolerance will have a significant positive impact on the power usage of your application.
毕竟一个很小的容差均可以对电量产生一个很大的积极的影响。
因此,从上面的论述中咱们能够看到,即便RunLoop模式正确,当前线程并不阻塞,系统依然可能会在NSTimer
上加上很小的的容差。
NSTimer
提供了一个invalidate
方法,用于终止NSTimer
。可是,这里涉及到一个多线程的问题。假设,我在A线程启动NSTimer
,在B线程调用invalidate
方法来终止NSTimer
,那么,NSTimer
是否会终止呢。
咱们来试验一下:
dispatch_async(dispatch_get_main_queue(), ^{ self.timer = [NSTimer scheduledTimerWithTimeInterval:0.5f target:self selector:@selector(test) userInfo:nil repeats:YES]; }); dispatch_async(dispatch_get_global_queue(0, 0), ^{ [self.timer invalidate]; });
结果是并不会中止。
在NSTimer的官方文档上,苹果是这么说的:
You should always call the invalidate method from the same thread on which the timer was installed.
因此,咱们必须哪一个线程调用,哪一个线程终止。
在NSTimer的官方文档上,苹果提到:
NSTimer is “toll-free bridged” with its Core Foundation counterpart, CFRunLoopTimerRef. See Toll-Free Bridgingfor more information on toll-free bridging.
NSTimer
能够直接桥接到CFRunLoopTimerRef
上,二者的数据结构是相同的,是能够互换的。咱们能够理解为,NSTimer
是objc版本的CFRunLoopTimerRef
封装。
CFRunLoopTimerRef
通常用法以下:
NSTimeInterval fireDate = CFAbsoluteTimeGetCurrent(); CFRunLoopTimerRef timer = CFRunLoopTimerCreateWithHandler(kCFAllocatorDefault, fireDate, 0.5f, 0, 0, ^(CFRunLoopTimerRef timer) { NSLog(@"test"); }); CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer, kCFRunLoopCommonModes);
其余用法能够参考苹果的CFRunLoopTimerRef文档。
CoreFoundation框架中有主要有5个RunLoop类:
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
CFRunLoopTimerRef
属于其中之一。
关于RunLoop,ibireme大神写了一个很是好的文章,你们能够围观学习一下:
http://blog.ibireme.com/2015/05/18/runlo...
既然CFRunLoopTimer is “toll-free bridged” with its Cocoa Foundation counterpart, NSTimer.
,那么若是在使用CF框架写内容的话,能够直接使用,不然,仍是使用NSTimer
吧。
dispatch_after
的用法比较简单,通常设定一个时间,而后指定一个队列,好比Main Dispatch Queue
。
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ NSLog(@"test"); });
和上面说到的NSTimer
和CFRunLoopTimerRef
不一样,NSTimer
和CFRunLoopTimerRef
是在指定的RunLoop上注册启动时间,而dispatch_after
是在指定的时间后,将整个执行的Block块添加到指定队列的RunLoop上。
因此,若是此队列处于繁重任务或者阻塞之中,dispatch_after
的Block块确定是要延后执行的。
假设如今有这么一个状况,dispatch_after
中引用了Self
,那么在设定时间以前,Self
能够被释放吗?
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ self.label.text = @"ok"; });
我来试验一下,我从A-VC push到B-VC,以后再pop回来,B-VC有个5秒的dispatch_after
:
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ self.label.text = @"ok"; NSLog(@"-- %@",self.description); });
咱们看一下输出:
结果是,在pop回去以后,Self
并无获得释放,在dispatch_after
的Block块执行完成以后,Self
才获得正确释放。
MLeaksFinder是一个开源的iOS内存泄露检测工具。做者颇有想法,他在ViewController
被pop或dismiss以后,在必定时间以后(默认为3秒),去看看这个ViewController
和它的subviews
是否还存在。
基本上是相似于这样:
__weak id weakSelf = self; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [weakSelf assertNotDealloc]; });
这里若是直接使用Self
的话,ViewController
被pop或者dismiss以后,依然是没法释放的,而__weak
就能够解决这个问题。在这里,Self
若是被正确释放的话,weakSelf
天然会变成nil
。
咱们修改一下咱们试验的代码:
__weak typeof(self) weakSelf = self; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ weakSelf.label.text = @"ok"; NSLog(@"-- %@",weakSelf.description); });
OK,没有问题。
这依然是对CFRunLoopTimerRef
的封装,和NSTimer
同源。既然是同源,那么一样具备NSTimer
的RunLoop特性。依据苹果文档中提到内容,performSelector:withObject:afterDelay:
运行的RunLoop模式是NSDefaultRunLoopMode
,那么,ScrollView
滚动的时候,RunLoop会切换,performSelector:withObject:afterDelay:
天然不会被回调。
This method sets up a timer to perform the aSelector message on the current thread’s run loop. The timer is configured to run in the default mode (NSDefaultRunLoopMode).
performSelector:withObject:afterDelay:
的用法也比较简单。
[self performSelector:@selector(test) withObject:nil afterDelay:1.0f];
假设是Self
去调用performSelector:withObject:afterDelay:
方法,在Delay时间未到以前,Self
可否被释放呢?
咱们试验一下:
[self performSelector:@selector(printDescription) withObject:nil afterDelay:5.0f];
结果和上面的dispatch_after
同样,咱们修改一下代码,再看一下:
__weak typeof(self) weakSelf = self; [weakSelf performSelector:@selector(printDescription) withObject:nil afterDelay:5.0f];
很遗憾,没有用。可是咱们能够取消这个performSelector
。
这种方法能够取消Target指定的带执行的方法。
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(printDescription) object:nil];
这种方法能够取消Target的全部待执行的方法。
[NSObject cancelPreviousPerformRequestsWithTarget:self];
和performSelector
不一样的是,performSelector:withObject:afterDelay:
并不会出现”PerformSelector may cause a leak because its selector is unknown.”的警告。
那么,这是否意味着performSelector:withObject:afterDelay:
能够正确释放返回值呢?
若是如今performSelector:withObject:afterDelay:
所执行的Selector
并不肯定,而且可能会返回一个对象,那么系统可否正确释放这个返回值呢?
咱们试验一下,这里printDescriptionA
和printDescriptionB
方法各会返回一个不一样类型的View
(此View是新建的对象),printDescriptionC
会返回Void
。
NSArray *array = @[@"printDescriptionA", @"printDescriptionB", @"printDescriptionC"]; NSString *selString = array[arc4random()%3]; NSLog(@"sel = %@", selString); SEL tempSel = NSSelectorFromString(selString); if ([self respondsToSelector:tempSel]) { [self performSelector:tempSel withObject:nil afterDelay:3.0f]; }
几回尝试以后,我发现,这是能够正常释放的。
在Effective Objective-C 2.0一书中,做者在第42条上提到:
performSelector系列方法在内存管理方面容易有疏失。它没法肯定要执行的选择子具体是什么,于是ARC编译器也就没法插入适当的内存管理方法。
关于这个问题,stackoverflow上也有不少讨论:
http://stackoverflow.com/questions/70172...
我不知道如何触发这种内存泄露,有知道的告诉我一声,学习一下,谢谢。