iOS NSTimer的詳細用法

一、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;
下面五種創(chuàng)建,不會自動添加到runloop,還需調(diào)用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

當使用NSTimerscheduledTimerWithTimeInterval方法時。事實上此時Timer會被加入到當前線程的Run Loop中,且模式是默認的NSDefaultRunLoopMode。而如果當前線程就是主線程,也就是UI線程時,某些UI事件,比如UIScrollView的拖動操作,會將Run Loop切換成NSEventTrackingRunLoopMode模式,在這個過程中,默認的NSDefaultRunLoopMode模式中注冊的事件是不會被執(zhí)行的。也就是說,此時使用scheduledTimerWithTimeInterval添加到Run Loop中的Timer就不會執(zhí)行。
所以為了設(shè)置一個不被UI干擾的Timer,我們需要手動創(chuàng)建一個Timer,然后使用NSRunLoopaddTimer:forMode:方法來把Timer按照指定模式加入到Run Loop中。這里使用的模式是:NSRunLoopCommonModes,這個模式等效于
NSDefaultRunLoopModeNSEventTrackingRunLoopMode的結(jié)合。(參考[Apple文檔]

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    NSLog(@"主線程 %@", [NSThread currentThread]);
    //創(chuàng)建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的回調(diào)方法
- (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中的循環(huán)引用

循環(huán)引用導致一些對象無法銷毀,一定的情況下會對我們造成影響,特別是我們要在dealloc中釋放一些資源的時候。如:當開啟定時器以后,testTimerDeallo方法一直執(zhí)行,即使dismiss此控制器以后,也是一直在打印,而且dealloc方法不會執(zhí)行.循環(huán)引用造成了內(nèi)存泄露,控制器不會被釋放.
問題分析

主要由于NSTimer對象和調(diào)用NSTimer的視圖控制器對象相互強引用了,其中NSTimer對視圖控制器的引用發(fā)生在最后一個參數(shù)reapets為YES的時候,因為需要重復執(zhí)行操作,所以需要強引用調(diào)用對象,那么解決辦法有兩點:

  • (1)讓視圖控制器對NSTimer的引用變成弱引用
  • (2)讓NSTimer對視圖控制器的引用變成弱引用

分析一下兩種方法,第一種方法如果控制器對NSTimer的引用改為弱引用,則會出現(xiàn)NSTimer直接被回收,所以不可使,因此我們只能從第二種方法入手

解決辦法:

__weak typeof(self) weakSelf = self; 不能解決
使用一個NSTimer的Catagory,然后重寫初始化方法,在實現(xiàn)中利用block,從而在調(diào)用的時候可以使用weakSelf在block執(zhí)行任務(wù),從而解除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的類別,在類別中定義一個類方法。類方法有一個類型為塊的參數(shù)(定義的塊位于棧上,為了防止塊被釋放,需要調(diào)用copy方法,將塊移到堆上)。使用這個類別的方式如下:

__weak ViewController *weakSelf = self;
_timer = [NSTimer jq_scheduledTimerWithTimeInterval:5.0
                                              block:^{
                                                  __strong ViewController *strongSelf = weakSelf;
                                                  [strongSelf startCounting];
                                              }
                                            repeats:YES];

使用這種方案就可以防止NSTimer對類的保留,從而打破了循環(huán)引用的產(chǎn)生。__strong ViewController *strongSelf = weakSelf主要是為了防止執(zhí)行塊的代碼時,類被釋放了。在類的dealloc方法中,記得調(diào)用[_timer invalidate]。

四、NSTimer和CADisplayLink的區(qū)別

CADisplayLink是一個能讓我們以和屏幕刷新率相同的頻率將內(nèi)容畫到屏幕上的定時器。我們在應(yīng)用中創(chuàng)建一個新的 CADisplayLink 對象,把它添加到一個 runloop中,并給它提供一個targetselector 在屏幕刷新的時候調(diào)用。

一但 CADisplayLink 以特定的模式注冊到runloop之后,每當屏幕需要刷新的時候,runloop就會調(diào)用CADisplayLink綁定的target上的selector,這時target可以讀到 CADisplayLink 的每次調(diào)用的時間戳,用來準備下一幀顯示需要的數(shù)據(jù)。例如一個視頻應(yīng)用使用時間戳來計算下一幀要顯示的視頻數(shù)據(jù)。在UI做動畫的過程中,需要通過時間戳來計算UI對象在動畫的下一幀要更新的大小等等。

在添加進runloop的時候我們應(yīng)該選用高一些的優(yōu)先級,來保證動畫的平滑??梢栽O(shè)想一下,我們在動畫的過程中,runloop被添加進來了一個高優(yōu)先級的任務(wù),那么,下一次的調(diào)用就會被暫停轉(zhuǎn)而先去執(zhí)行高優(yōu)先級的任務(wù),然后在接著執(zhí)行CADisplayLink的調(diào)用,從而造成動畫過程的卡頓,使動畫不流暢。另外 CADisplayLink 不能被繼承。

1、創(chuàng)建方法
    displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDisplayLink:)];
    [displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
2、停止方法
    [displayLink invalidate];
    displayLink = nil;

當把CADisplayLink對象add到runloop中后,selector就能被周期性調(diào)用,類似于重復的NSTimer被啟動了;執(zhí)行invalidate操作時,CADisplayLink對象就會從runloop中移除,selector調(diào)用也隨即停止,類似于NSTimer的invalidate方法。

3、CADisplayLink 與 NSTimer有什么不同?
  • (1)原理不同

CADisplayLink是一個能讓我們以和屏幕刷新率同步的頻率將特定的內(nèi)容畫到屏幕上的定時器類。 CADisplayLink以特定模式注冊到runloop后, 每當屏幕顯示內(nèi)容刷新結(jié)束的時候,runloop就會向 CADisplayLink指定的target發(fā)送一次指定的selector消息, CADisplayLink類對應(yīng)的selector就會被調(diào)用一次。

NSTimer以指定的模式注冊到runloop后,每當設(shè)定的周期時間到達后,runloop會向指定的target發(fā)送一次指定的selector消息。

  • (2)周期設(shè)置方式不同

iOS設(shè)備的屏幕刷新頻率(FPS)是60Hz,因此CADisplayLink的selector 默認調(diào)用周期是每秒60次,這個周期可以通過frameInterval屬性設(shè)置, CADisplayLink的selector每秒調(diào)用次數(shù)=60/ frameInterval。比如當 frameInterval設(shè)為2,每秒調(diào)用就變成30次。因此, CADisplayLink 周期的設(shè)置方式略顯不便。

NSTimer的selector調(diào)用周期可以在初始化時直接設(shè)定,相對就靈活的多。

  • (3)精確度不同

iOS設(shè)備的屏幕刷新頻率是固定的,CADisplayLink在正常情況下會在每次刷新結(jié)束都被調(diào)用,精確度相當高。

NSTimer的精確度就顯得低了點,比如NSTimer的觸發(fā)時間到的時候,runloop如果在阻塞狀態(tài),觸發(fā)時間就會推遲到下一個runloop周期。并且 NSTimer新增了tolerance屬性,讓用戶可以設(shè)置可以容忍的觸發(fā)的時間的延遲范圍。

  • (4)使用場景

CADisplayLink使用場合相對專一,適合做UI的不停重繪,比如自定義動畫引擎或者視頻播放的渲染。

NSTimer的使用范圍要廣泛的多,各種需要單次或者循環(huán)定時處理的任務(wù)都可以使用。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容