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




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章