Objective-C三種定時器CADisplayLink / NSTimer / GCD的使用


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我也沒用玩轉, 只說這些。 后面會找時間專門研究下


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM