1. 什么是NSTimer
??官方的解釋“A timer provides a way to perform a delayed action or a periodic action. The timer waits until a certain time interval has elapsed and then fires, sending a specified message to a specified object. ” 翻譯過來就是NSTimer 提供了一種執行延遲動作或周期動作的方法。可以指定時間間隔,向一個對象發送消息。
??NSTimer是iOS最常用的定時器工具之一,比如用來定時更新界面,定時發送請求等等。但是在使用過程中,有很多需要注意的地方,稍微不注意就會產生 bug、crash、內存泄漏。
2. NSTimer的頭文件
// Use the timerWithTimeInterval:invocation:repeats: or timerWithTimeInterval:target:selector:userInfo:repeats: class method to create the timer object without scheduling it on a run loop. (After creating it, you must add the timer to a run loop manually by calling the addTimer:forMode: method of the corresponding NSRunLoop object.)
// 創建一個定時器,但是么有添加到運行循環,我們需要在創建定時器后手動的調用 NSRunLoop 對象的 addTimer:forMode: 方法。
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
// Creates and returns a new NSTimer object and schedules it on the current run loop in the default mode.
// 創建一個timer并把它指定到一個默認的runloop模式中,并且在 TimeInterval時間后 啟動定時器
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
// Use the timerWithTimeInterval:invocation:repeats: or timerWithTimeInterval:target:selector:userInfo:repeats: class method to create the timer object without scheduling it on a run loop. (After creating it, you must add the timer to a run loop manually by calling the addTimer:forMode: method of the corresponding NSRunLoop object.)
// 創建一個定時器,但是么有添加到運行循環,我們需要在創建定時器后手動的調用 NSRunLoop 對象的 addTimer:forMode: 方法。
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
// Creates and returns a new NSTimer object and schedules it on the current run loop in the default mode.
// 創建一個timer并把它指定到一個默認的runloop模式中,并且在 TimeInterval時間后 啟動定時器
+ (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 NS_DESIGNATED_INITIALIZER;
iOS10之后新加入的方法,使用block避免循環引用引起的內存泄漏問題
/// Creates and returns a new NSTimer object initialized with the specified block object. This timer needs to be scheduled on a run loop (via -[NSRunLoop addTimer:]) before it will fire.
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
/// Creates and returns a new NSTimer object initialized with the specified block object and schedules it on the current run loop in the default mode.
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
/// Initializes a new NSTimer object using the block as the main body of execution for the timer. This timer needs to be scheduled on a run loop (via -[NSRunLoop addTimer:]) before it will fire.
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
// You can use this method to fire a repeating timer without interrupting its regular firing schedule. If the timer is non-repeating, it is automatically invalidated after firing, even if its scheduled fire date has not arrived.
// 啟動 Timer 觸發Target的方法調用但是并不會改變Timer的時間設置。 即 time沒有到達到,Timer會立即啟動調用方法且沒有改變時間設置,當時間 time 到了的時候,Timer還是會調用方法。
- (void)fire;
// 這是設置定時器的啟動時間,常用來管理定時器的啟動與停止
@property (copy) NSDate *fireDate;
// 啟動定時器
timer.fireDate = [NSDate distantPast];
//停止定時器
timer.fireDate = [NSDate distantFuture];
// 開啟
[time setFireDate:[NSDate distanPast]]
// NSTimer 關閉
[time setFireDate:[NSDate distantFunture]]
//繼續。
[timer setFireDate:[NSDate date]];
// 這個是一個只讀屬性,獲取定時器調用間隔時間
@property (readonly) NSTimeInterval timeInterval;
// Setting a tolerance for a timer allows it to fire later than the scheduled fire date, improving the ability of the system to optimize for increased power savings and responsiveness. The timer may fire at any time between its scheduled fire date and the scheduled fire date plus the tolerance. The timer will not fire before the scheduled fire date. For repeating timers, the next fire date is calculated from the original fire date regardless of tolerance applied at individual fire times, to avoid drift. The default value is zero, which means no additional tolerance is applied. The system reserves the right to apply a small amount of tolerance to certain timers regardless of the value of this property.
// As the user of the timer, you will have the best idea of what an appropriate tolerance for a timer may be. A general rule of thumb, though, is to set the tolerance to at least 10% of the interval, for a repeating timer. Even a small amount of tolerance will have a significant positive impact on the power usage of your application. The system may put a maximum value of the tolerance.
// 這是7.0之后新增的一個屬性,因為NSTimer并不完全精準,通過這個值設置誤差范圍
@property NSTimeInterval tolerance NS_AVAILABLE(10_9, 7_0);
// 停止 Timer ---> 唯一的方法將定時器從循環池中移除
- (void)invalidate;
// 獲取定時器是否有效
@property (readonly, getter=isValid) BOOL valid;
// 獲取參數信息---> 通常傳入的是 nil
@property (nullable, readonly, retain) id userInfo;
3. NSTimer的一般用法
3.1 初始化方法
系統提供了8個創建方法,6個類方法,2個實例初始化方法。
- 有三個方法直接將timer添加到了當前runloop,而不需要我們自己操作,當然這樣的代價是runloop只能是當前runloop,模式是NSDefaultRunLoopMode
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
- 下面五種創建,不會自動添加到runloop,還需調用addTimer:forMode:方法添加到指定的mode中
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(id)ui repeats:(BOOL)rep;
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
初始化方法的參數說明
ti(NSTimeInterval):定時器觸發間隔時間,單位為秒,可以是小數。如果值小于等于0.0的話,系統會默認賦值0.1毫秒
invocation(NSInvocation):這種形式用的比較少,大部分都是block和aSelector的形式
yesOrNo(BOOL):是否重復,如果是YES則重復觸發,直到調用invalidate方法;如果是NO,則只觸發一次就自動調用invalidate方法
aTarget(id):發送消息的目標,timer會強引用aTarget,直到調用invalidate方法
aSelector(SEL):將要發送給aTarget的消息,如果帶有參數則參數為:(NSTimer *)timer
userInfo(id):傳遞的用戶信息。使用的話,首先aSelector須帶有參數的聲明,然后可以通過[timer userInfo]獲取,也可以為nil,那么[timer userInfo]就為空
date(NSDate):觸發的時間,一般情況下我們都寫[NSDate date],這樣的話定時器會立馬觸發一次,并且以此時間為基準。如果沒有此參數的方法,則都是以當前時間為基準,第一次觸發時間是當前時間加上時間間隔ti
block(void (^)(NSTimer *timer)):timer觸發的時候會執行這個操作,帶有一個參數,無返回值
3.2 NSTimer的觸發
-
-(void)fire方法說明:
調用fire的時候,立即觸發timer的方法,該方法觸發不影響計時器原本的計時,只是新增一次觸發
1. 對于重復定時器,它不會影響正常的定時觸發。 啟動timer觸發target的方法調用但是并不會改變timer的時間設置interval。 即 interval沒有到達到,timer會立即啟動調用方法且沒有改變時間設置,當時間interval到了的時候,timer還是會調用selector。
2. 對于非重復定時器,觸發后就調用了invalidate方法。既使interval的時間周期內還沒有觸發
- NSTimer在添加到runloop時,timer開始計時,即使runloop沒有開啟(run)。在構造NSTimer的時候,如果不是馬上開始計時,可以先使用timerWithTimeInterval,隨后再手動加入runloop上
- 當NSTimer進入后臺的時,NSTimer計時暫停,進入前臺繼續
4. NSTimer和Runloop
上面構造函數我們可以看到,當我們把timer添加到runloop的時候會指定NSRunLoopMode(scheduledTimerWithTimeInterval默認使用NSDefaultRunLoopMode),iOS支持的有下面兩種模式
NSDefaultRunLoopMode:默認的運行模式,用于大部分操作,除了NSConnection對象事件。
NSRunLoopCommonModes:是一個模式集合,當綁定一個事件源到這個模式集合的時候就相當于綁定到了集合內的每一個模式。
下面三種是內部框架支持(AppKit)
NSConnectionReplyMode:用來監控NSConnection對象的回復的,很少能夠用到。
NSModalPanelRunLoopMode:用于標明和Mode Panel相關的事件。
NSEventTrackingRunLoopMode:用于跟蹤觸摸事件觸發的模式(例如UIScrollView上下滾動)。
當timer添加到主線程的runloop時,某些UI事件(如:UIScrollView或者它的子類UITableView、UICollectionView等滑動時)會將runloop切換到NSEventTrackingRunLoopMode模式下。在這個模式下,NSDefaultRunLoopMode模式注冊的事件是不會被執行的,也就是通過scheduledTimerWithTimeInterval方法添加到runloop的NSTimer這時候是不會被執行的。為了讓NSTimer不被UI事件干擾,我們需要將注冊到runloop的timer的mode設為NSRunLoopCommonModes,這個模式等效于NSDefaultRunLoopMode和NSEventTrackingRunLoopMode的結合
5. NSTimer循環引用的問題
5.1 NSTimer的造成循環引用的原因分析
如下,創建一個計時器
NSTimer *timer = [[NSTimer alloc] timerWithTimeInterval:1 target:self selector:@selector(timerFire) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
上述代碼,將創建一個無限循環的 timer,并在當前線程的 runloop 中開始執行。
如下圖所示:
Timer 添加到 Runloop 的時候,會被 Runloop 強引用;Timer 又會有一個對 Target 的強引用(也就是 self);也就是說 NSTimer 強引用了 self ,導致 self 一直不能被釋放掉,所以也就走不到 self 的 dealloc 里。
主要是 NSTimer 對 target 是強引用的。如果在target調用dealloc之前沒有釋放timer就會造成內存的泄漏,或者生命周期超出開發者的預期。
那么,[timer invalidate] 要什么時候調用?
有些人會在 self 的 dealloc 里面調用,這幾乎可以確定是錯誤的。因為 timer 會引用住 self,在 timer 停止之前,是不會釋放 self 的,self 的 dealloc 也不可能會被調用。
5.2 NSTimer的造成循環引用的解決方案
- NSTimer在構造函數會對target強引用,在調用invalidate時,會移除去target的強引用
- NSTimer被加到Runloop的時候,會被runloop強引用持有,在調用invalidate的時候,會從runloop刪除
- 當定時器是不重復的(repeat=NO),在執行完觸發函數后,會自動調用invalidate解除runloop的注冊和接觸對target的強引用
5.2.1 根據業務需要,在適當的地方啟動 timer 和 停止 timer。比如 timer 是頁面用來更新頁面內部的 view 的,那可以選擇在頁面顯示的時候啟動 timer,頁面不可見的時候停止 timer。比如:
- (void)viewWillAppear
{
[super viewWillAppear];
self.timer =
[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(update) userInfo:nil repeats:YES];
}
- (void)viewDidDisappear
{
[super viewDidDisappear];
[self.timer invalidate];
}
5.2.2 weakSelf:
問題的關鍵就在于 self 被 NSTimer 強引用了,如果我們能打破這個強引用問題自然而然就解決了。所以一個很簡單的想法就是:weakSelf
_weak typeof(self) weakSelf = self;
_timer = [NSTimer scheduledTimerWithTimeInterval:3.0f
target:weakSelf
selector:@selector(timerFire:)
userInfo:nil
repeats:YES];
然而這并沒有什么卵用,這里的 __weak 和 __strong 唯一的區別就是:如果在這兩行代碼執行的期間 self 被釋放了, NSTimer 的 target 會變成 nil 。
5.2.3 target:既然沒辦法通過 __weak 把 self 抽離出來,我們可以造個假的 target 給 NSTimer 。這個假的 target 類似于一個中間的代理人,它做的唯一的工作就是挺身而出接下了 NSTimer 的強引用。實現方式如下:
@interface WeakTimer : NSObject
@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL selector;
@property (nonatomic, weak) NSTimer* timer;
@end
@implementation WeakTimer
- (void) fire:(NSTimer *)timer {
if(self.target) {
[self.target performSelector:self.selector withObject:timer.userInfo];
} else {
[self.timer invalidate];
}
}
@end
然后我們再封裝個假的 scheduledTimerWithTimeInterval 方法,但是在調用的時候已經偷梁換柱了:
+ (NSTimer *) scheduledTimerWithTimeInterval:(NSTimeInterval)interval
target:(id)aTarget
selector:(SEL)aSelector
userInfo:(id)userInfo
repeats:(BOOL)repeats {
WeakTimer* timerTarget = [[WeakTimer 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;
}
5.2.4 block:如果能用 block 來調用 NSTimer 那豈不是更好了。我們可以這樣來實現:
@interface NSTimer (XP)
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)seconds block:(void (^)(NSTimer *timer))block repeats:(BOOL)repeats;
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)seconds block:(void (^)(NSTimer *timer))block repeats:(BOOL)repeats;
@end
@implementation NSTimer (XP)
+ (void)_xp_timerBlock:(NSTimer *)timer {
if ([timer userInfo]) {
void (^block)(NSTimer *timer) = (void (^)(NSTimer *timer))[timer userInfo];
block(timer);
}
}
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)seconds block:(void (^)(NSTimer *timer))block repeats:(BOOL)repeats {
return [NSTimer scheduledTimerWithTimeInterval:seconds target:self selector:@selector(_xp_timerBlock:) userInfo:[block copy] repeats:repeats];
}
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)seconds block:(void (^)(NSTimer *timer))block repeats:(BOOL)repeats {
return [NSTimer timerWithTimeInterval:seconds target:self selector:@selector(_xp_timerBlock:) userInfo:[block copy] repeats:repeats];
}
@end
使用,以scheduledTimerWithTimeInterval方法為例:
__weak typeof(self) weakSelf = self;
[NSTimer scheduledTimerWithTimeInterval:1.F block:^(NSTimer * _Nonnull timer) {
NSLog(@"%@", weakSelf);
} repeats:YES];
這4種解決方法中個人比較偏向與使用block方法,并且在iOS10中系統已經新增了block方法,可見這也是Apple力推的方法。
5.3 停止 timer 可能會導致 self 對象銷毀
值得注意的是,調用 [timer invalidate] 停止 timer,此時 timer 會釋放 target,如果 timer 是最后一個持有 target 的對象,那么此次釋放會直接觸發 target 的 。比如:
- (void)onEnterBackground:(id)sender
{
[self.timer invalidate];
[self.view stopAnimation]; // dangerous!
}
以上代碼,加入第一行的 invalidate 之后,self 被銷毀了,那么第二行訪問 self.view 時候,就會觸發野指針 crash。因為 Objective-C 的方法里面,self 是沒有被 retain 的。這種情況,有個臨時的解決方案如下:
- (void)onEnterBackground:(id)sender
{
__weak id weakSelf = self;
[self.timer invalidate];
[weakSelf.view stopAnimation]; // dangerous!
}
將 self 改為弱引用。但是也是一個臨時解決方案。正確解決方法是,查出其它對象沒有引用 self 的時候,為什么 timer 還沒停止。這個案例告訴大家,當見到 invalidate 被調用之后很神奇地出現了 self 野指針 crash 的時候,不要驚訝,就是 timer 沒處理好。
6. 多線程
如果我們不在主線程使用Timer的時候,即使我們把timer添加到runloop,也不能被觸發,因為主線程的runloop默認是開啟的,而其他線程的runloop默認沒有實現runloop,并且在后臺線程使用NSTimer不能通過fire啟動定時器,只能通過runloop不斷的運行下去
- (void)viewDidLoad {
[super viewDidLoad];
// 使用新線程
[NSThread detachNewThreadSelector:@selector(startNewThread) toTarget:self withObject:nil];
}
- (void)startNewThread {
self.timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(timerTest:) userInfo:nil repeats:YES];
// 添加到runloop
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addTimer:self.timer forMode:NSDefaultRunLoopMode];
// 非主線程需要手動運行runloop,run方法會阻塞,直到沒有輸入源的時候返回(例如:timer從runloop中移除,invalidate)
[runLoop run]
}
7. NSTimer準確性
NSTimer觸發是不精確的,如果由于某些原因錯過了觸發時間,例如執行了一個長時間的任務,那么NSTimer不會延后執行,而是會等下一次觸發,相當于等公交錯過了,只能等下一趟車,tolerance屬性可以設置誤差范圍
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(timerTest:) userInfo:nil repeats:YES];
// 誤差范圍1s內
timer.tolerance = 1;
7.1 第一種不準時:有可能跳過去
7.1.1 線程處理比耗時的事情時會發生
7.1.2 還有就是timer添加到的runloop模式不是runloop當前運行的模式,這種情況經常發生。
對于第一種情況我們不應該在timer上下功夫,而是應該避免這個耗時的工作。那么第二種情況,作為開發者這也是最應該去關注的地方,要留意,然后視情況而定是否將timer添加到runloop多個模式
雖然跳過去,但是,接下來的執行不會依據被延遲的時間加上間隔時間,而是根據之前的時間來執行。比如:
定時時間間隔為2秒,t1秒添加成功,那么會在t2、t4、t6、t8、t10秒注冊好事件,并在這些時間觸發。假設第3秒時,執行了一個超時操作耗費了5.5秒,則觸發時間是:t2、t8.5、t10,第4和第6秒就被跳過去了,雖然在t8.5秒觸發了一次,但是下一次觸發時間是t10,而不是t10.5。
7.2 第二種不準時:不準點
比如上面說的t2、t4、t6、t8、t10,并不會在準確的時間觸發,而是會延遲個很小的時間,原因也可以歸結為2點:
1. RunLoop為了節省資源,并不會在非常準確的時間點觸發
2. 線程有耗時操作,或者其它線程有耗時操作也會影響
以我來講,從來沒有特別準的時間,
iOS7以后,Timer 有個屬性叫做 Tolerance (時間寬容度,默認是0),標示了當時間點到后,容許有多少最大誤差。
它只會在準確的觸發時間到加上Tolerance時間內觸發,而不會提前觸發(是不是有點像我們的火車,只會晚點。。。)。另外可重復定時器的觸發時間點不受Tolerance影響,即類似上面說的t8.5觸發后,下一個點不會是t10.5,而是t10 + Tolerance,不讓timer因為Tolerance而產生漂移(突然想起嵌入式令人頭疼的溫漂)。
其實對于這種不準點,對我們開發影響并不大(基本是毫秒妙級別以下的延遲),很少會用到非常準點的情況。
如果對精度有要求,可以使用GCD定時器
8. 后臺運行
NSTimer不支持后臺運行(真機),但是模擬器上App進入后臺的時候,NSTimer還會持續觸發
如果需要后臺運行可以通過下面兩種方式支持
讓App支持后臺運行(運行音頻)(在后臺可以觸發)
記錄離開和進入App的時間,手動控制計時器(在后臺不能觸發)
第一種控制起來比較麻煩,通常建議手動控制,不在后臺觸發計時
9. Perform Delay
[NSObject performSelector:withObject:afterDelay:] 和 [NSObject performSelector:withObject:afterDelay:inMode:] 我們簡稱為 Perform Delay,他們的實現原理就是一個不循環(repeat 為 NO)的 timer。所以使用這兩個接口的注意事項跟使用 timer 類似。需要在適當的地方調用 [NSObject cancelPreviousPerformRequestsWithTarget:selector:object:]
-
NSObject對象有一個performSelector可以用于延遲執行一個方法,其實該方法內部是啟用一個Timer并添加到當前線程的runloop,原理與NSTimer一樣,所以在非主線程使用的時候,需要保證線程的runloop是運行的,否則不會得到執行
如下:
- (void)viewDidLoad { [super viewDidLoad]; [NSThread detachNewThreadSelector:@selector(startNewThread) toTarget:self withObject:nil]; } - (void)startNewThread { // test方法不會觸發,因為runloop默認不開啟 [self performSelector:@selector(test) withObject:nil afterDelay:1]; } - (void)test { NSLog(@"test trigger"); }
10. 暫停、重新開啟定時器
10.1 暫停、重新開啟
//暫停
[_timer setFireDate:[NSDate distantFuture]];
//重新開啟
[_timer setFireDate:[NSDate distantPast]];
比如,在頁面消失的時候關閉定時器,然后等頁面再次打開的時候,又開啟定時器。(主要是為了防止它在后臺運行,暫用CPU)可以使用下面的代碼實現:
//頁面將要進入前臺,開啟定時器
-(void)viewWillAppear:(BOOL)animated {
//開啟定時器
[_timer setFireDate:[NSDate distantPast]];
}
//頁面消失,進入后臺不顯示該頁面,關閉定時器
-(void)viewDidDisappear:(BOOL)animated {
//關閉定時器
[_timer setFireDate:[NSDate distantFuture]];
}
10.2 銷毀
if(_timer){
[_timer invalidate];
_timer = nil;
}
總結
總的來說使用NSTimer有兩點需要注意
- NSTimer只有被注冊到runloop才能起作用,fire不是開啟定時器的方法,只是觸發一次定時器的方法
- NSTimer會強引用target。invalidate取消runloop的注冊和target的強引用,如果是非重復的定時器,則在觸發時會自動調用invalidate
通常我們自己封裝GCD定時器使用起來更為方便,不會有這些問題