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