定时器的用法
系统提供了8个创建方法,6个类创建方法,2个实例初始化方法。有三个方法直接将timer添加到了当前runloop的NSDefaultRunLoopMode中,而不需要我们自己添加,当然这样的代价是runloop只能是当前runloop,模式是NSDefaultRunLoopMode。
+ (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;
NSTimer添加到NSRunLoop
timer其实也是一种资源,所有的资源如果要起作用,就得加到runloop中去。同理timer这种资源要想起作用,那肯定也需要加到runloop中才会有效。如果一个runloop里面不包含任何资源的话,运行该runloop时会立马退出。
我们都知道iOS是通过runloop作为消息循环机制,主线程默认启动了runloop,可是子线程没有默认的runloop,因此,我们在子线程启动定时器是不生效的。解决的方式也简单,在子线程启动一下runloop就可以了。
dispatch_async(dispatch_get_global_queue(0,0), ^{
NSTimer* timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(Timered:) userInfo:nil repeats:YES];
[[NSRunLoopcurrentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
[[NSRunLoopcurrentRunLoop] run];
});
timer是runloop的一个触发源,由于timer是添加到runloop中使用的,一个timer可以被添加到runloop的多个模式,比如在主线程中runloop一般处于NSDefaultRunLoopMode,但是,比如UIScrollView或者它的子类UITableView、UICollectionView等滑动时runloop处于UITrackingRunLoopMode模式下,因此如果你想让timer在滑动的时候也能够触发,就可以分别添加到这两个模式下。这时候就应该使用runloop的NSRunLoopCommonModes(等效于NSDefaultRunLoopMode和NSEventTrackingRunLoopMode的结合)模式,能够让定时器在runloop两种模式切换时也不受影响。
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
关于强引用的问题
invalidate方法的介绍:
This method is the only way to remove a timer from an NSRunLoop object. The NSRunLoop object removes its strong reference to the timer, either just before the invalidate method returns or at some later point.
You must send this message from the thread on which the timer was installed. If you send this message from another thread, the input source associated with the timer may not be removed from its run loop, which could prevent the thread from exiting properly.
1. invalidate方法是唯一能从runloop中移除timer的方式,调用invalidate方法后,runloop会移除对timer的强引用。
2.timer的添加和timer的移除(invalidate)需要在同一个线程中,否则timer可能不能正确的移除,线程不能正确退出。
在这里首先声明一下:不是所有的timer都会造成循环引用。就像不是所有的block都会造成循环引用一样。以下两种timer不会有循环引用:
1. 非repeat类型的。非repeat类型的timer在执行完后,会自动调用invalidate方法,因此不会出现循环引用。
2.block类型的,iOS 10之后才支持,因此对于还要支持老版本的app来说,这个API还不能满足所有需求。当然block内部的循环引用也要避免。
再次声明:不是解决了循环引用,target就可以释放了,别忘了在持有timer的类dealloc的时候执行invalidate。
在除了iOS10新增的三个block方法外,其他方法方法都会有target(一般为self)。
因为runloop强引用了timer,
The receiver retains aTimer. To remove a timer from all run loop modes on which it is installed, send an invalidate message to the timer.
而且timer有强引用了target(self)。
The object to which to send the message specified by aSelector when the timer fires. The timer maintains a strong reference to target until it (the timer) is invalidated.
如果不释放timer,造成self也释放不了,导致内存泄漏。
我们常说的timer会产生循环引用 其实就是由于timer会对self进行强引用造成的。由于timer对target强引用的特性,如果要避免控制器不释放的问题,需要在特定的时机调用timer 的 invalidate方法,也就是提前结束timer。在通常情况下,这种方式是可以解决问题的,虽然需要警惕页面退出之前有没有结束timer,但毕竟解决了问题不是。但是,日常项目中通常是多人协作,如果该timer是一个view的属性,而这个view又需要让别人使用,那timer什么时候结束呢?让调用者来管理timer的结束显然是不合理的。更好的方式还是应该在dealloc 方法中结束timer,这样调用者根本无须关注timer。
解决循环引用,首先想到的方法就是让self对timer为弱引用weak或者time对target如self替换为weakSelf 然而这真的有用吗?
@interface TimerViewController()
@property(nonatomic,weak)NSTimer*timer;
@end
@implementationTimerViewController
- (void)dealloc {
[self.timer invalidate];
NSLog(@"dealloc");
}
- (void)viewDidLoad {
[superviewDidLoad];
NSTimer*timer = [NSTimer timerWithTimeInterval:1.0f target:selfselector:@selector(count) userInfo:nilrepeats:YES]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
self.timer = timer
;}
- (void)count {
NSLog(@"count");
}
@end
将self改成weakSelf
- (void)viewDidLoad {
[superviewDidLoad];
__weaktypeof(self) weakSelf =self;
NSTimer*timer = [NSTimer timerWithTimeInterval:1.0f target:weakSelf selector:@selector(count) userInfo:nilrepeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
self.timer = timer;
}
设置timer为weak
我们想通过self对timer的弱引用, 在self中的dealloc方法中让timer失效来达到相互释放的目的。但是, timer内部本身对于self有一个强引用。并且timer如果不调用invalidate方法,就会一直存在,所以就导致了self根本释放不了, 进而我们想通过在dealloc中设置timer失效来释放timer的方法也就行不通了。而且weak的timer,如果写成这样,还会导致崩溃,要手动加入runloop的。
self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(nslog:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
设置self为weakSelf
用__weak修饰self为weakSelf与普通的self的区别就在于, 这时候weakSelf作为一个参数传入block或者别的实例变量, block或实例变量都不会持有他, 也就是self的引用计数不会加1 ,在一般情况下 这时候就可以打破循环引用, 但是timer的内部机制决定了它必须通过设置invalidate来停止计时并释放, 在此之前, timer会强引用target, 所以也就不存在timer释放weakSelf, 即循环引用还是存在。
方法:block方法,来自《Effective Objective-C》第52条:别忘了NSTimer会保留其目标对象,其实iOS10开始系统也提供了该方法,但是的兼容更低的版本。
- (void)viewDidLoad {
[superviewDidLoad];
__weakidweakSelf =self;
NSTimer* timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer*timer) {
NSLog(@"block %@",weakSelf);
}];
}
@implementation NSTimer(BlockTimer)
+ (NSTimer*)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats blockTimer:(void(^)(NSTimer*))block{
NSTimer* timer = [NSTimer scheduledTimerWithTimeInterval:interval target:self selector:@selector(timered:) userInfo:[blockcopy] repeats:repeats];
returntimer;
}
+ (void)timered:(NSTimer*)timer {
void(^block)(NSTimer*timer) = timer.userInfo; block(timer);
}
@end
解释:将强引用的target变成了NSTimer的类对象。类对象本身是单例的,是不会释放的,所以强引用也无所谓。执行的block通过userInfo传递给定时器的响应函数timered:。循环引用被打破的结果是:timer的使用者强引用timer。timer强引用NSTimer的类对象。timer的使用者在block中通过weak的形式使用,因此是被timer弱引用。
如果在是在view中的定时器, 可以重写removeFromSuperview
- (void)removeFromSuperview {
[super removeFromSuperview];
[self.timer invalidate];
}
NSTimer的实时性
NSTimer不是一个实时系统,因此不管是一次性的还是周期性的timer的实际触发事件的时间可能都会跟我们预想的会有出入。差距的大小跟当前我们程序的执行情况有关系,比如可能程序是多线程的,而你的timer只是添加在某一个线程的runloop的某一种指定的runloop mode中,由于多线程通常都是分时执行的,而且每次执行的mode也可能随着实际情况发生变化。如果timer当前所处的线程正在进行大数据处理(假设为一个大循环),timer本次执行会等到这个大数据处理完毕之后才会继续执行。
这期间有可能会错过很多次timer的循环周期,但是timer并不会将前面错过的执行次数在后面都执行一遍,而是继续执行后面的循环,也就是在一个循环周期内只会执行一次循环。 无论循环延迟的多离谱,循环间隔都不会发生变化,在进行完大数据处理之后,有可能会立即执行一次timer循环,但是后面的循环间隔始终和第一次添加循环时的间隔相同。
后记
重复初始化造成的问题。
如果
- (void)viewDidLoad {
[super viewDidLoad];
_timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(nslog:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSDefaultRunLoopMode];
_timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"timer");
}];
[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSDefaultRunLoopMode];
}
- (void)nslog:(NSTimer*)timer{
NSLog(@"timer");
}
或者
- (void)viewDidLoad {
[super viewDidLoad];
_timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"timer");
}];
_timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(nslog:) userInfo:nil repeats:YES];
}
- (void)nslog:(NSTimer*)timer{
NSLog(@"timer");
}
多次把_timer加入runloop,即使调用了invalidate,_timer释放了,self释放了,还是会输出,造成内存泄漏。