相信做iOS开发的童靴对NSTimer应该不会陌生,使用它遇到的坑还真不少。下面我就结合自己项目中遇到的问题,讨论一下NSTimer在使用的中我们要避开的那些坑:
坑1:创建的方式
Apple API为我们提供了一下几种创建NSTimer的方式:
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(nullable id)ui repeats:(BOOL)rep;
- timerWithTimeInterval开头的构造方法,我们可以创建一个定时器,但是默认没有添加到runloop中,我们需要在创建定时器后,需要手动将其添加到NSRunLoop中,否则将不会循环执行。
- scheduledTimerWithTimeInterval开头的构造方法,从此构造方法创建的定时器,它会默认将其指定到一个默认的runloop中,并且timerInterval时候后,定时器会自启动。
- init是默认的初始化方法,需要我们手动添加到runloop中,并且还需要手动触发fire,才能启动定时器。
NSTimer的创建和释放必须放在同一个线程中,所以我们的创建实例的时候,一定要特别留意这几个创建方式的区别,我更喜欢使用第4个创建方法。
坑2:循环引用
提出问题:我们使用scheduledTimerWithTimeInterval创建一个NSTimer实例后,timer会自动添加到runloop中,此时会被runloop强引用,而timer又会对target强引用,这样就形成强引用循环了。如果不手动停止timer,那么self这个VC将不能被释放,尤其是当我们这个VC是push进来的,pop将不会被释放。
解决办法:问题的关键在于self被timer强引用了,如果我们能打破这个强引用,那问题就解决了。
方案1:在VC的dealloc中释放timer?
在提出问题中,我们已经知道形成了循环引用了,那VC就不能得到释放,dealloc方法也不会执行,那在dealloc中释放timer是解决不了问题的。
方案2:在VC的viewWillDisappear中释放timer?
这样的确能在一定程度上解决问题,如果当我们VC再push一个新的界面时,VC没有释放,那么timer也就不能释放。所以这种方案不是最理想的。
方案3:直接弱引用self(VC)
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:weakSelf selector:@selector(countDownHandler) userInfo:nil repeats:YES];
然并卵,在block中,block是对变量进行拷贝,注意拷贝的是变量本身而不是对象。以上面的代码为例,block只是对变量weakSelf拷贝了一份,相当于在block的内存中,定义了一个__weak blockWeak对象,然后执行了blockWeak = weakSelf,并没有引起对象持有权的变化。回过头来看看timer,虽然我们将weakSelf传入timer构造方法中,虽然我们看似弱引用的self对象,但target的说明中明确提到是强引用了这个target,也就是说timer强引用了一个弱引用的变量,结果还是强引用,这和你直接传self进来效果是一样的,并不能解除强引用循环。这样的做唯一作用是如果在timer运行期间self被释放了,timer的target也就置为nil,仅此而已。
方案4:我们可以创建一个临时的target,让timer强引用这个临时变量对象,在这个临时对象中弱引用self。这个target类似于一个代理,它的工作就是背锅,接下timer的强引用工作。
直接上代码:
#import <Foundation/Foundation.h>
typedef void(^SFWeakTimerBlock)(id userInfo);
@interface SFWeakTimer : NSObject
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
target:(id)aTarget
selector:(SEL)aSelector
userInfo:(id)userInfo
repeats:(BOOL)repeats;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
block:(SFWeakTimerBlock)block
userInfo:(id)userInfo
repeats:(BOOL)repeats;
@end
#import "SFWeakTimer.h"
@interface SFWeakTimerTarget : NSObject
@property (weak, nonatomic) id target;
@property (assign, nonatomic) SEL selector;
@property (weak, nonatomic) NSTimer *timer;
- (void)fire:(NSTimer *)timer;
@end
@implementation SFWeakTimerTarget
- (void)fire:(NSTimer *)timer {
if (self.target && [self.target respondsToSelector:self.selector]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self.target performSelector:self.selector withObject:timer.userInfo afterDelay:0.0f];
#pragma clang diagnostic pop
} else {
[self.timer invalidate];
}
}
@end
@implementation SFWeakTimer
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
target:(id)aTarget
selector:(SEL)aSelector
userInfo:(id)userInfo
repeats:(BOOL)repeats {
SFWeakTimerTarget *timerTarget = [[SFWeakTimerTarget alloc] init];
timerTarget.target = aTarget;
timerTarget.selector = aSelector;
timerTarget.timer = [NSTimer scheduledTimerWithTimeInterval:interval target:timerTarget selector:@selector(fire:) userInfo:userInfo repeats:repeats];
return timerTarget.timer;
}
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
block:(SFWeakTimerBlock)block
userInfo:(id)userInfo
repeats:(BOOL)repeats {
return [self scheduledTimerWithTimeInterval:interval target:self selector:@selector(sf_timerUsingBlockWithObjects:) userInfo:@[[block copy], userInfo] repeats:repeats];
}
+ (void)sf_timerUsingBlockWithObjects:(NSArray *)objects {
SFWeakTimerBlock block = [objects firstObject];
id userInfo = [objects lastObject];
if (block) {
block(userInfo);
}
}
@end
当前也可以参考YYKit/YYWeakProxy中的例子,githud中有YYKit的使用教程。
问题解决,破费!
坑3:NSDefaultRunLoopMode搞怪
提出问题:当使用NSTimer的scheduledTimerWithTimeInterval的方法时,事实上此时的timer会被加入到当前线程的runloop中,默认为NSDefaultRunLoopMode。如果当前线程是主线程,某些事件,如UIScrollView的拖动时,会将runloop切换到NSEventTrackingRunLoopMode模式,在拖动的过程中,默认的NSDefaultRunLoopMode模式中注册的事件是不会被执行的。从而此时的timer也就不会触发。
解决办法:把创建好的timer手动添加到指定模式中,此处为NSRunLoopCommonModes,这个模式其实就是NSDefaultRunLoopMode与NSEventTrackingRunLoopMode的结合。
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];