一、NSTimer的類方法和實例初始化方法
這三個方法直接將timer添加到了當前runloop default mode,而不需要我們自己操作,當然這樣的代價是runloop只能是當前runloop,模式是default mode:
+ (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:
+ (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;
二、NSRunLoopCommonModes和Timer
當使用NSTimer
的scheduledTimerWithTimeInterval
方法時。事實上此時Timer會被加入到當前線程的Run Loop中,且模式是默認的NSDefaultRunLoopMode
。而如果當前線程就是主線程,也就是UI線程時,某些UI事件,比如UIScrollView
的拖動操作,會將Run Loop切換成NSEventTrackingRunLoopMode模式
,在這個過程中,默認的NSDefaultRunLoopMode
模式中注冊的事件是不會被執行的。也就是說,此時使用scheduledTimerWithTimeInterva
l添加到Run Loop中的Timer就不會執行。
所以為了設置一個不被UI干擾的Timer,我們需要手動創建一個Timer,然后使用NSRunLoop
的addTimer:forMode:
方法來把Timer按照指定模式加入到Run Loop中。這里使用的模式是:NSRunLoopCommonModes
,這個模式等效于
NSDefaultRunLoopMode
和NSEventTrackingRunLoopMode的結合
。(參考[Apple文檔]
- (void)viewDidLoad
{
[super viewDidLoad];
NSLog(@"主線程 %@", [NSThread currentThread]);
//創建Timer
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(timer_callback) userInfo:nil repeats:YES];
//使用NSRunLoopCommonModes模式,把timer加入到當前Run Loop中。
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
//timer的回調方法
- (void)timer_callback
{
NSLog(@"Timer %@", [NSThread currentThread]);
}
輸出:
主線程 <NSThread: 0x71501e0>{name = (null), num = 1}
Timer <NSThread: 0x71501e0>{name = (null), num = 1}
Timer <NSThread: 0x71501e0>{name = (null), num = 1}
Timer <NSThread: 0x71501e0>{name = (null), num = 1}
三、NSTimer中的循環引用
循環引用導致一些對象無法銷毀,一定的情況下會對我們造成影響,特別是我們要在dealloc
中釋放一些資源的時候。如:當開啟定時器以后,testTimerDeallo
方法一直執行,即使dismiss
此控制器以后,也是一直在打印,而且dealloc方法不會執行.循環引用造成了內存泄露,控制器不會被釋放.
問題分析
主要由于NSTimer對象和調用NSTimer的視圖控制器對象相互強引用了,其中NSTimer對視圖控制器的引用發生在最后一個參數reapets為YES的時候,因為需要重復執行操作,所以需要強引用調用對象,那么解決辦法有兩點:
- (1)讓視圖控制器對NSTimer的引用變成弱引用
- (2)讓NSTimer對視圖控制器的引用變成弱引用
分析一下兩種方法,第一種方法如果控制器對NSTimer的引用改為弱引用,則會出現NSTimer直接被回收,所以不可使,因此我們只能從第二種方法入手
解決辦法:
__weak typeof(self) weakSelf = self; 不能解決
使用一個NSTimer的Catagory,然后重寫初始化方法,在實現中利用block,從而在調用的時候可以使用weakSelf在block執行任務,從而解除NSTimer對target(視圖控制器)的強引用。
@interface NSTimer (JQUsingBlock)
+ (NSTimer *)jq_scheduledTimerWithTimeInterval:(NSTimeInterval)ti
block:(void(^)())block
repeats:(BOOL)repeats;
@end
@implementation NSTimer (JQUsingBlock)
+ (NSTimer *)jq_scheduledTimerWithTimeInterval:(NSTimeInterval)ti
block:(void(^)())block
repeats:(BOOL)repeats{
return [self scheduledTimerWithTimeInterval:ti
target:self
selector:@selector(jq_blockInvoke:)
userInfo:[block copy]
repeats:repeats];
}
+ (void)jq_blockInvoke:(NSTimer *)timer{
void(^block)() = timer.userInfo;
if (block) {
block();
}
}
@end
定義一個NSTimer
的類別,在類別中定義一個類方法。類方法有一個類型為塊的參數(定義的塊位于棧上,為了防止塊被釋放,需要調用copy
方法,將塊移到堆上)。使用這個類別的方式如下:
__weak ViewController *weakSelf = self;
_timer = [NSTimer jq_scheduledTimerWithTimeInterval:5.0
block:^{
__strong ViewController *strongSelf = weakSelf;
[strongSelf startCounting];
}
repeats:YES];
使用這種方案就可以防止NSTimer
對類的保留,從而打破了循環引用的產生。__strong ViewController *strongSelf = weakSelf
主要是為了防止執行塊的代碼時,類被釋放了。在類的dealloc
方法中,記得調用[_timer invalidate]
。
四、NSTimer和CADisplayLink的區別
CADisplayLink是一個能讓我們以和屏幕刷新率相同的頻率將內容畫到屏幕上的定時器。我們在應用中創建一個新的 CADisplayLink 對象,把它添加到一個 runloop
中,并給它提供一個target
和selector
在屏幕刷新的時候調用。
一但 CADisplayLink 以特定的模式注冊到runloop之后,每當屏幕需要刷新的時候,runloop就會調用CADisplayLink綁定的target上的selector,這時target可以讀到 CADisplayLink 的每次調用的時間戳,用來準備下一幀顯示需要的數據。例如一個視頻應用使用時間戳來計算下一幀要顯示的視頻數據。在UI做動畫的過程中,需要通過時間戳來計算UI對象在動畫的下一幀要更新的大小等等。
在添加進runloop的時候我們應該選用高一些的優先級,來保證動畫的平滑。可以設想一下,我們在動畫的過程中,runloop被添加進來了一個高優先級的任務,那么,下一次的調用就會被暫停轉而先去執行高優先級的任務,然后在接著執行CADisplayLink的調用,從而造成動畫過程的卡頓,使動畫不流暢。另外 CADisplayLink 不能被繼承。
1、創建方法
displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDisplayLink:)];
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
2、停止方法
[displayLink invalidate];
displayLink = nil;
當把CADisplayLink對象add到runloop中后,selector就能被周期性調用,類似于重復的NSTimer被啟動了;執行invalidate操作時,CADisplayLink對象就會從runloop中移除,selector調用也隨即停止,類似于NSTimer的invalidate
方法。
3、CADisplayLink 與 NSTimer有什么不同?
- (1)原理不同
CADisplayLink是一個能讓我們以和屏幕刷新率同步的頻率將特定的內容畫到屏幕上的定時器類。 CADisplayLink以特定模式注冊到runloop后, 每當屏幕顯示內容刷新結束的時候,runloop就會向 CADisplayLink指定的target發送一次指定的selector消息, CADisplayLink類對應的selector就會被調用一次。
NSTimer以指定的模式注冊到runloop后,每當設定的周期時間到達后,runloop會向指定的target發送一次指定的selector消息。
- (2)周期設置方式不同
iOS設備的屏幕刷新頻率(FPS)是60Hz,因此CADisplayLink的selector 默認調用周期是每秒60次,這個周期可以通過frameInterval屬性設置, CADisplayLink的selector每秒調用次數=60/ frameInterval。比如當 frameInterval設為2,每秒調用就變成30次。因此, CADisplayLink 周期的設置方式略顯不便。
NSTimer的selector調用周期可以在初始化時直接設定,相對就靈活的多。
- (3)精確度不同
iOS設備的屏幕刷新頻率是固定的,CADisplayLink在正常情況下會在每次刷新結束都被調用,精確度相當高。
NSTimer的精確度就顯得低了點,比如NSTimer的觸發時間到的時候,runloop如果在阻塞狀態,觸發時間就會推遲到下一個runloop周期。并且 NSTimer新增了tolerance屬性,讓用戶可以設置可以容忍的觸發的時間的延遲范圍。
- (4)使用場景
CADisplayLink使用場合相對專一,適合做UI的不停重繪,比如自定義動畫引擎或者視頻播放的渲染。
NSTimer的使用范圍要廣泛的多,各種需要單次或者循環定時處理的任務都可以使用。