標籤:and interval cpu 調用 產生 拖動 ase sign 研究
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:
定義:NSDefaultRunLoopMode
(Cocoa) kCFRunLoopDefaultMode (Core Foundation)
描述:預設模式中幾乎包含了所有輸入源(NSConnection除外),一般情況下應使用此模式。
定義:NSConnectionReplyMode(Cocoa)
描述:處理NSConnection對象相關事件,系統內部使用,使用者基本不會使用。
定義:NSModalPanelRunLoopMode(Cocoa)
描述:處理modal panels事件。
定義:UITrackingRunLoopMode
(iOS)
NSEventTrackingRunLoopMode(cocoa)
描述:在拖動loop或其他user interface tracking loops時處於此種模式下,在此模式下會限制輸入事件的處理。例如,當手指按住UITableView拖動時就會處於此模式。
定義: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];
網上很多人對fire方法的解釋其實並不正確。fire並不是立即啟用定時器,而是立即執行一次定時器方法
。
當加入到runloop中timer不需要啟用
即可按照設定的時間觸發事件。fire只是相當於手動讓timer觸發一次事件
。
如果timer設定的repeat為NO,則fire之後timer立即銷毀
。
如果timer的repeat為YES
,則到了之前設定的時間他依舊會按部就班的觸發事件
。
fire只是單獨觸發了一次事件,並不影響原timer的節奏
。
我們知道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我也沒用玩轉, 只說這些。 後面會找時間專門研究下
Objective-C三種定時器CADisplayLink / NSTimer / GCD的使用