OC中的三種定時器:CADisplayLink、NSTimer、GCD
我們先來看看CADiskplayLink, 點進頭文件里面看看, 用注釋來說明下
@interface CADisplayLink : NSObject { @private void *_impl; //指針 } + (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;
//唯一一個初始化方法 - (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode;
//將創建好點實例添加到RunLoop中 - (void)removeFromRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode; //從RunLoop中移除
- (void)invalidate;
//銷毀實例 @property(readonly, nonatomic) CFTimeInterval timestamp; //上一次Selector被調用到時間, 只讀 @property(readonly, nonatomic) CFTimeInterval duration; //屏幕刷新時間間隔, 目前iOS刷新頻率是60HZ, 所以刷新時間間隔是16.7ms @property(readonly, nonatomic) CFTimeInterval targetTimestamp CA_AVAILABLE_IOS_STARTING(10.0, 10.0, 3.0); //下一次被調用到時間
@property(getter=isPaused, nonatomic) BOOL paused; //設置為YES的時候會暫停事件的觸發 @property(nonatomic) NSInteger frameInterval CA_AVAILABLE_BUT_DEPRECATED_IOS (3.1, 10.0, 9.0, 10.0, 2.0, 3.0, "use preferredFramesPerSecond");
//事件觸發間隔。是指兩次selector觸發之間間隔幾次屏幕刷新,默認值為1
,也就是說屏幕每刷新一次,執行一次
selector,這個也可以間接用來控制動畫速度
@property(nonatomic) NSInteger preferredFramesPerSecond CA_AVAILABLE_IOS_STARTING(10.0, 10.0, 3.0);
//每秒現實多少幀
@end
從頭文件來看CADisplayLink的使用還是挺簡單的, 下面上代碼:
- (void)viewDidLoad { [super viewDidLoad]; self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(logCount)]; self.displayLink.frameInterval = 2; //屏幕刷新2次調用一次Selector [self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; } - (void)logCount { self.count ++; NSLog(@"Count = %ld", self.count); if (self.count > 99) { self.count = 0; [self.displayLink invalidate]; //直接銷毀 } }
代碼很簡單就不做說明了
需要注意的是CADisplayLink必須要添加到可以執行的RunLoop中才會執行, 當添加到某一個RunLoop后如果該RunLoop暫停或者該RunLoop的Model改變了, 計時器也會暫停
比如我們給TableView添加計時器到當前RunLoop的NSDefaultRunLoopMode model中, 當屏幕一半顯示時計時器可以正常調用, 但當我們用手滑動TableView時, 計時器就會暫停。
因為當滑動時, RunLoop會進入到UITrackingRunLoopMode
所以當我們發現計時器沒有運行時, 可以檢查下是否有加入到正確的mode中
那我們來說一下runloop的幾種mode:
- Default模式
定義:NSDefaultRunLoopMode
(Cocoa) kCFRunLoopDefaultMode (Core Foundation)
描述:默認模式中幾乎包含了所有輸入源(NSConnection除外),一般情況下應使用此模式。
- Connection模式
定義:NSConnectionReplyMode(Cocoa)
描述:處理NSConnection對象相關事件,系統內部使用,用戶基本不會使用。
- Modal模式
定義:NSModalPanelRunLoopMode(Cocoa)
描述:處理modal panels事件。
- Event tracking模式
定義:UITrackingRunLoopMode
(iOS)
NSEventTrackingRunLoopMode(cocoa)
描述:在拖動loop或其他user interface tracking loops時處於此種模式下,在此模式下會限制輸入事件的處理。例如,當手指按住UITableView拖動時就會處於此模式。
- Common模式
定義:NSRunLoopCommonModes
(Cocoa) kCFRunLoopCommonModes (Core Foundation)
描述:這是一個偽模式,其為一組run loop mode的集合,將輸入源加入此模式意味着在Common Modes中包含的所有模式下都可以處理。在Cocoa應用程序中,默認情況下Common Modes包含default modes,modal modes,event Tracking modes.可使用CFRunLoopAddCommonMode方法向Common Modes中添加自定義modes。
注:iOS中僅NSDefaultRunLoopMode,UITrackingRunLoopMode,NSRunLoopCommonModes三種可用mode。
CADisplayLink
基本用法剛剛介紹過。
優勢:依托於設備屏幕刷新頻率觸發事件,所以其觸發時間上是最准確的。也是最適合做UI不斷刷新的事件
,過渡相對流暢,無卡頓感。
缺點:
- 由於依托於屏幕刷新頻率,若果CPU不堪重負而影響了屏幕刷新,那么我們的觸發事件也會受到相應影響。
- selector觸發的時間間隔只能是duration的整倍數。
- selector事件如果大於其觸發間隔就會造成掉幀現象。
- CADisplayLink不能被繼承。
-------------------我是分割線---------------------
下面說說NSTImer, 一樣我們直接看頭文件並用注釋說明
@interface NSTimer : NSObject + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
//實例化方法, 響應事件用的NSInvocation, 需要手動添加到RunLoop中才會生效
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo; //實例化方法, 響應事件用的NSIvocation, 系統為自動幫你將timer添加到currentRunLoop中,defaultMode
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
//實例化方法, 響應事件用的PerformanceSelector, userInfo中可以用來傳參數,需要手動添加到RunLoop中
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
//實例化方法,響應事件用的PerformanceSelector, userInfo可以用來傳遞參數, 系統會自動幫你將timer添加到currentRunLoop中, defaultMode + (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));
//實例化方法, 以block的方式傳入要執行的內容, 需要手動添加到RunLoop中 + (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));
//實例化方法, 以block的方式傳入要執行的內容,系統會自動幫你將timer添加到currentRunLoop中,defaultMode - (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)); //跟上面類似, 只是多指定了一個開始時間
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(nullable id)ui repeats:(BOOL)rep NS_DESIGNATED_INITIALIZER; //跟上面類似, 只是多指定了一個開始時間
- (void)fire; //立即執行一次定時器方法, 注意不是立即開啟定時器 @property (copy) NSDate *fireDate; //當前事件的觸發事件, 一般用來做暫停和恢復 @property (readonly) NSTimeInterval timeInterval; //只讀屬性, 獲取當前timer的觸發間隔 @property NSTimeInterval tolerance NS_AVAILABLE(10_9, 7_0); //允許的誤差值 - (void)invalidate; //立即銷毀timer @property (readonly, getter=isValid) BOOL valid; //只讀屬性, 獲取當前timer是否有效 @property (nullable, readonly, retain) id userInfo; //只讀屬性, 初始化時傳入的用戶參數 @end
NSTimer的內容相對多一些但也更加靈活, 有一個地方需要注意的是timer開頭的實例化方法需要手動添加到RunLoop, Schedule開頭的會由系統幫你添加到RunLoop
fireDate
,設置當前timer的事件的觸發時間。通常我們使用這個屬性來做計時器的暫停與恢復
。
///暫停計時器 self.timer.fireDate = [NSDate distantFuture]; ///恢復計時器 self.timer.fireDate = [NSDate distantPast];
-
tolerance,
允許誤差時間
。我們知道NSTimer事件的觸發事件是不准確的
,完全取決於當前runloop
處理的時間。如果當前runloop在處理復雜運算
,則timer執行時間將會被推遲
,直到復雜運算結束后立即執行觸發事件
,之后再按照初始設置的節奏去執行
。當設置tolerance之后在允許范圍內的延遲可以觸發事件,超過的則不觸發。默認是時間間隔的1/10
網上很多人對fire方法的解釋其實並不正確。fire並不是立即激活定時器,而是立即執行一次定時器方法
。
當加入到runloop中timer不需要激活
即可按照設定的時間觸發事件。fire只是相當於手動讓timer觸發一次事件
。
如果timer設置的repeat為NO,則fire之后timer立即銷毀
。
如果timer的repeat為YES
,則到了之前設置的時間他依舊會按部就班的觸發事件
。
fire只是單獨觸發了一次事件,並不影響原timer的節奏
。
- 關於invalid方法
我們知道NSTimer使用的時候如果不注意的話,是會造成內存泄漏的
。原因是我們生成實例的時候,會對控制器retain一下。如果不對其進行管理則VC的永遠不會引用計數為零,進而造成內存泄漏。
所以,當我們不需要的timer的時候,請如下操作:
[self.timer invalid]; self.timer = nil;
這樣Timer會對VC進行一次release。所以一定不要忘記調用invalid方法
。
順便提一句,如果生成timer實例的時候repeat為NO
,那當觸發事件結束后,系統也會自動調用invalid一次
。
NSTimer的優勢:使用相對靈活,應用廣泛
劣勢:受runloop影響嚴重,同時易造成內存泄漏(調用invalid方法解決)
-------------------我是分割線---------------------
下面說說GCD計時器:dispatch_source_t
其實dispatch_source_t說為計時器不完全正確, 它實際上是GCD給我們用的一個源對象
還是先直接上代碼:
#import "ViewController.h" @interface ViewController () @property (nonatomic, assign) NSInteger count; @property (nonatomic, strong) dispatch_source_t tTimer; //GCD計時器一定要設置為成員變量, 否則會立即釋放 @end @implementation ViewController @synthesize tTimer; - (void)viewDidLoad { [super viewDidLoad]; //創建GCD timer資源, 第一個參數為源類型, 第二個參數是資源要加入的隊列 self.tTimer = \ dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue()); //設置timer信息, 第一個參數是我們的timer對象, 第二個是timer首次觸發延遲時間, 第三個參數是觸發時間間隔, 最后一個是是timer觸發允許的延遲值, 建議值是十分之一 dispatch_source_set_timer(self.tTimer, dispatch_walltime(NULL, 0 * NSEC_PER_SEC), 0.32 * NSEC_PER_SEC, 0); //設置timer的觸發事件 dispatch_source_set_event_handler(self.tTimer, ^{ [self logCount]; }); //激活timer對象 dispatch_resume(self.tTimer); } - (void)logCount { self.count ++; NSLog(@"Count = %ld", self.count); if (self.count > 99) { self.count = 0; //暫停timer對象 dispatch_suspend(self.tTimer); //銷毀timer, 注意暫停的timer資源不能直接銷毀, 需要先resume再cancel, 否則會造成內存泄漏 //dispatch_source_cancel(self.tTimer); } }
注釋已經很清楚了, 就不再逐條解釋(上面代碼會呦循環引用的問題, 大家自己改下)
需要注意的是, GCD timer資源必須設定為成員變量, 否則會在創建完畢后立即釋放
suspend掛起或暫停后的timer要先resume才能cancel, 掛起的timer直接cancel會造成內存泄漏
GCDTimer的優勢:不受當前runloopMode的
影響。
劣勢:雖然說不受runloopMode的影響,但是其計時效應仍不是百分之百准確的。
另外,他的觸發事件也有可能被阻塞,當GCD內部管理的所有線程都被占用時,其觸發事件將被延遲
。
好吧GCD我也沒用玩轉, 只說這些。 后面會找時間專門研究下