iOS 使用NSTimer中的那些坑

相信做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 

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];




你可能感兴趣的:(NSTimer,weakTimer,GCD,iOS开发技巧)