NSTimer平時用的很多,如果不是真正的懂它,會發生各種各樣的問題。如你滑動tableview的時候定時器不走了。或者是出現,內存不能釋放的問題。
NSTimer是基于RunLoop的
- 先看看定時器的創建
[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timeEvent) userInfo:nil repeats:YES];
NSTimer *timer = [[NSTimer alloc]initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:1] interval:1 target:self selector:@selector(timeEvent) userInfo:nil repeats:YES];
一般情況下我們都是用第一種方法。為什么不用第二種呢??因為第二種如果只是創建一個timer對象發現timeEvent不調用。看看下面的代碼
NSTimer *timer = [[NSTimer alloc]initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:1] interval:1 target:self selector:@selector(timeEvent) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
發現定時器起作用了,這說明定時器是基于RunLoop的,如果沒有了RunLoop的驅動,定時器是不會執行的。
那么第一種為什么會執行呢?因為第一種在創建了定時器之后會把定時器加到RunLoop中,不用我們自己手動加。蘋果給這一步做了,所以定時器得以很好的玩耍。現在我們已經通過上面證實了NSTimer得加到RunLoop中才會執行。
讓NSTimer在滑動tableview的時候繼續工作
如果你在viewdidload創建了一個定時器,然后又創建了tableview。當你滑動tableview的時候定時器不走了。這又是為什么呢?
這個問題又跟RunLoop的幾個模式有關
Default mode(NSDefaultRunLoopMode)
默認模式中幾乎包含了所有輸入源(NSConnection除外),一般情況下應使用此模式。
Connection mode(NSConnectionReplyMode)
處理NSConnection對象相關事件,系統內部使用,用戶基本不會使用。
Modal mode(NSModalPanelRunLoopMode)
處理modal panels事件。
Event tracking mode(UITrackingRunLoopMode)
在拖動loop或其他user interface tracking loops時處于此種模式下,在此模式下會限制輸入事件的處理。例如,當手指按住UITableView拖動時就會處于此模式。
Common mode(NSRunLoopCommonModes)
這是一個偽模式,其為一組run loop mode的集合,將輸入源加入此模式意味著在Common Modes中包含的所有模式下都可以處理。在Cocoa應用程序中,默認情況下Common Modes包含default modes,modal modes,event Tracking modes.
剛才不是說蘋果給定時器加到RunLoop了嗎?蘋果加的是默認的模式NSDefaultRunLoopMode,當你滑動tableview的時候runloop將會切換到UITrackingRunLoopMode,切換了模式,當然不工作了。因為你是默認的不是托拽的。解決辦法就是蘋果給我們默認的模式,我們再修改下,改為NSRunLoopCommonModes模式。這樣所有模式都可以處理了。
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timeEvent) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
你這次滑動tableview的時候發現定時器還在執行。
多線程下創建定時器然后讓定時器執行
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timeEvent) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] run];
});
RunLoop用來循環處理響應事件,每個線程都有一個RunLoop,蘋果不允許自己創建RunLoop,scheduledTimerWithTimeInterval這個方法創建好NSTimer以后會自動將它添加到當前線程的RunLoop,但是只有主線程的RunLoop是默認打開的,而其他線程的RunLoop如果需要使用就必須手動打開,所以我們用了這句 [[NSRunLoop currentRunLoop] run];打開線程的NSRunLoop。定時器就開始工作了。
NSTimer容易導致內存泄漏。
為了保證參數的生命周期,NSTimer會對target對象做強引用,也就是retain一次。為什么要強引用你?因為如果target銷毀了,我定時器還怎么來做事?誰來替我調用timeEvent呢?所以它就強引用target,因為定時器是加到RunLoop中的,所以RunLoop強引用著NSTimer,一般情況下我們的target就是當前的控制器,如果我們想讓控制器如我所愿的銷毀了,首先得除掉NSTimer這個禍害。要不NSTimer強引用著self,self就銷毀不了。所以經常會因為不理解它導致我們的內存泄漏。
現在的唯一辦法就是先讓定時器死了。然后self也就釋放了。定時器唯一死的辦法就是
[self.timer invalidate];
self.timer = nil;
當一個timer被schedule的時候,timer會持有target對象,NSRunLoop對象會持有timer。當invalidate被調用時,NSRunLoop對象會釋放對timer的持有,timer會釋放對target的持有。除此之外,我們沒有途徑可以釋放timer對target的持有。所以解決內存泄露就必須撤銷timer,若不撤銷,target對象將永遠無法釋放。
解決NSTimer的內存泄漏。
有的人會在viewWillDisappear里銷毀定時器。有的人會在返回按鈕里銷毀定時器。雖然能解決問題但是不太友好。
我們想如果target不是咱們的控制器,timer不就不強引用self了嗎?
如果timer不強引用self,那么self就可以盡情的釋放了。通過這個思路可以寫出一個自定義的YBTimer,通過一個YBTimerTarget作為timer 和self的紐帶,timer強引用YBTimerTarget,YBTimerTarget和self是弱引用,這樣我們就不會造成循環引用了。而且可以在dealloc里銷毀定時器
- (void)dealloc
{
[self.timer invalidate];
self.timer = nil;
}
YBTimer實現
#import <Foundation/Foundation.h>
typedef void (^YBTimerBlock)(id userInfo);
@interface YBTimer : NSObject
+ (NSTimer *) scheduledTimerWithTimeInterval:(NSTimeInterval)interval
target:(id)aTarget
selector:(SEL)aSelector
userInfo:(id)userInfo
repeats:(BOOL)repeats;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
block:(YBTimerBlock)block
userInfo:(id)userInfo
repeats:(BOOL)repeats;
@end
#import "YBTimer.h"
//------------------------------YBTimerTargetBegin-------------------------------------
@interface YBTimerTarget : NSObject
@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL selector;
@property (nonatomic, weak) NSTimer* timer;
@end
@implementation YBTimerTarget
- (void)timeAction:(NSTimer *)timer {
if(self.target) {
[self.target performSelector:self.selector withObject:timer.userInfo afterDelay:0.0f];
} else {
[self.timer invalidate];
}
}
@end
//------------------------------YBTimerTargetEnd-------------------------------------
@implementation YBTimer
+ (NSTimer *) scheduledTimerWithTimeInterval:(NSTimeInterval)interval
target:(id)aTarget
selector:(SEL)aSelector
userInfo:(id)userInfo
repeats:(BOOL)repeats {
YBTimerTarget* timerTarget = [[YBTimerTarget alloc] init];
timerTarget.target = aTarget;
timerTarget.selector = aSelector;
timerTarget.timer = [NSTimer scheduledTimerWithTimeInterval:interval
target:timerTarget
selector:@selector(timeAction:)
userInfo:userInfo
repeats:repeats];
return timerTarget.timer;
}
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
block:(YBTimerBlock)block
userInfo:(id)userInfo
repeats:(BOOL)repeats {
NSMutableArray *userInfoArray = [NSMutableArray arrayWithObject:[block copy]];
if (userInfo != nil) {
[userInfoArray addObject:userInfo];
}
return [self scheduledTimerWithTimeInterval:interval
target:self
selector:@selector(timerBlock:)
userInfo:[userInfoArray copy]
repeats:repeats];
}
+ (void)timerBlock:(NSArray*)userInfo {
YBTimerBlock block = userInfo[0];
id info = nil;
if (userInfo.count == 2) {
info = userInfo[1];
}
if (block) {
block(info);
}
}
@end