你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發綜合 >> NSTimer定時器進階 詳細介紹,循環引用分析與解決

NSTimer定時器進階 詳細介紹,循環引用分析與解決

編輯:IOS開發綜合

本文將為大家說明NSTimer定時器進階 詳細介紹,循環引用分析與解決的說明,具體操作請看面的內容

引言

定時器:A timer waits until a certain time interval has elapsed and then fires, sending a specified message to a target object. 翻譯如下:在固定的時間間隔被觸發,然後給指定目標發送消息。總結為三要素吧:時間間隔、被觸發、發送消息(執行方法)

按照官方的描述,我們也確實是這麼用的;但是裡面有很多細節,你是否了解呢?

它會被添加到runloop,否則不會運行,當然添加的runloop不存在也不會運行; 還要指定添加到的runloop的哪個模式,而且還可以指定添加到runloop的多個模式,模式不對也是不會運行的 runloop會對timer有強引用,timer會對目標對象進行強引用(是否隱約的感覺到坑了。。。) timer的執行時間並不准確,系統繁忙的話,還會被跳過去 invalidate調用後,timer停止運行後,就一定能從runloop中消除嗎,資源????

呵呵。。。下面會解決這些問題

定時器的一般用法

控制器中添加定時器,例如:

- (void)viewDidLoad {
    NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1 target:self selector:@selector(timerFire) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    self.timer = timer;
}

- (void)timerFire {
    NSLog(@"timer fire");
}

上面的代碼就是我們使用定時器最常用的方式,可以總結為2個步驟:創建,添加到runloop

系統提供了8個創建方法,6個類創建方法,2個實例初始化方法。

有三個方法直接將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;

對上面所有方法參數做個說明:

ti(interval):定時器觸發間隔時間,單位為秒,可以是小數。如果值小於等於0.0的話,系統會默認賦值0.1毫秒 invocation:這種形式用的比較少,大部分都是block和aSelector的形式 yesOrNo(rep):是否重復,如果是YES則重復觸發,直到調用invalidate方法;如果是NO,則只觸發一次就自動調用invalidate方法 aTarget(t):發送消息的目標,timer會強引用aTarget,直到調用invalidate方法 aSelector(s):將要發送給aTarget的消息,如果帶有參數則應:- (void)timerFireMethod:(NSTimer *)timer聲明 userInfo(ui):傳遞的用戶信息。使用的話,首先aSelector須帶有參數的聲明,然後可以通過[timer userInfo]獲取,也可以為nil,那麼[timer userInfo]就為空 date:觸發的時間,一般情況下我們都寫[NSDate date],這樣的話定時器會立馬觸發一次,並且以此時間為基准。如果沒有此參數的方法,則都是以當前時間為基准,第一次觸發時間是當前時間加上時間間隔ti block:timer觸發的時候會執行這個操作,帶有一個參數,無返回值

添加到runloop,參數timer是不能為空的,否則拋出異常

- (void)addTimer:(NSTimer *)timer forMode:(NSRunLoopMode)mode;

另外,系統提供了一個- (void)fire;方法,調用它可以觸發一次:

對於重復定時器,它不會影響正常的定時觸發 對於非重復定時器,觸發後就調用了invalidate方法,既使正常的還沒有觸發 NSTimer添加到NSRunLoop

如同引言中說的那樣,timer必須添加到runloop才有效,很明顯要保證兩件事情,一是runloop存在(運行),另一個才是添加。確保這兩個前提後,還有runloop模式的問題。

一個timer可以被添加到runloop的多個模式,比如在主線程中runloop一般處於NSDefaultRunLoopMode,而當滑動屏幕的時候,比如UIScrollView或者它的子類UITableView、UICollectionView等滑動時runloop處於UITrackingRunLoopMode模式下,因此如果你想讓timer在滑動的時候也能夠觸發,就可以分別添加到這兩個模式下。或者直接用NSRunLoopCommonModes一個模式集,包含了上面的兩種模式。

但是一個timer只能添加到一個runloop(runloop與線程一一對應關系,也就是說一個timer只能添加到一個線程)。如果你非要添加到多個runloop,則只有一個有效

關於強引用的問題

還是經常使用到的代碼

- (void)viewDidLoad {
    // 代碼標記1
    NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1 target:self selector:@selector(timerFire) userInfo:nil repeats:YES];
    // 代碼標記2
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    // 代碼標記3
    self.timer = timer;
}

- (void)timerFire {
    NSLog(@"timer fire");
}

假設代碼中的視圖控制器由UINavigationController管理,且self.timer是strong類型,則強引用可以表示如下:

NSTimer定時器進階 詳細介紹,循環引用分析與解決

上面有四根強引用線,它們是如何產生的呢,這個也必須搞清楚?

L1:這個簡單,nav push 控制器的時候會強引用,即在push的時候產生; L2:是在代碼標記3的位置產生; L3:是在代碼標記1的位置產生,至此L2與L3已經產生了循環引用,雖然timer還沒有添加到runloop L4:是在代碼標記2的位置產生

根據上圖就很清晰了,我們經常說到timer與self會造成循環引用,並不是因為runloop引起,而是timer本身會對self有強引用。

invalidate方法

invalidate方法有2個功能:一是將timer從runloop中移除,那麼圖中的L4就消失,二是timer本身也會釋放它持有資源,比如target、userinfo、block(關於block強引用self具體參考這裡:http://www.cnblogs.com/mddblog/p/4754190.html),那麼強引用L3就消失。如果self.timer是weak引用,也就是L2是弱引用,那麼timer的引用計數就為0了,timer本身也就被釋放了。如果你此時又調用addTimer:forMode:則會拋異常,因為timer為nil,因此當控制器使用weak方式引用timer時,應注意這點

之後的timer也就永遠無效了,調用它的getter方法isValid返回是NO,即使你再次將它正確的添加到runloop,也不會觸發,因為timer已對target、block釋放了。

timer只有這一個方法可以完成此操作,所以我們取消一個timer必須要調用此方法。而在添加到runloop前,可以使用它的getter方法isValid來判斷,一個是防止為nil,另一個是防止為無效。

然而就像引言中說的那個聳人聽聞的問題一樣,invalidate方法調用必須在timer添加到的runloop所在的線程,如果不在的話:雖然timer本身會釋放掉它自己持有的資源比如target、userinfo、block,圖中的L3會消失。但是runloop不會釋放timer,即圖中的L4不會消失,假設,self被pop了-->L1無效-->self引用計數為0,self釋放-->L2也消失。此時就剩runloop、timer、L4,timer也就永遠不會釋放了,造成內存洩露。

下面不得不面對另一個問題,runloop退出或者本身被釋放不就可以了嗎???

這才真心是一個頭疼的問題:是的,沒錯,runloop退出甚至自身釋放後,L4消失,timer也就釋放了。。。可以參考之前那篇關於runloop退出釋放的問題NSRunLoop原理詳解——不再有盲點:http://www.jianshu.com/p/4263188ed940

這裡補充一點,timer沒有被釋放,那麼它會作為runloop的輸入源,從而阻止runloop的退出(runloop的退出是會釋放掉timer的)。

只關心runloop的退出就好,至於釋放就別深究了,或者就當它不釋放(我的理解是隨著線程釋放而釋放)

關於強引用再舉個常見例子

重復的添加timer,例如下面的代碼:

// 無論self.timer是strong還是weak
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:2 target:self selector:@selector(timerHandle) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
}

每點擊一次屏幕就會添加一次,就會造成重復添加,你的timerHandle方法會被調用多次,添加幾次就調用幾次。。。

假設點擊了2次屏幕,即創建2了個timer,我們標記為t1,t2。我們分析一下:第二次的時候,self.timer引用t2,雖然不在引用t1但是,runloop還在引用它,所以不會釋放,不用說t2也是不會釋放的。

那麼如何解決呢?setter方法裡面調用invalidate即可:

- (void)setTimer:(NSTimer *)timer {
    [_timer invalidate];
    _timer = timer;
}

其實記住兩條即可

timer不用了,一定要調用invalidate 一般是target釋放的同時,才會知道timer不用了,那麼怎麼捕獲target被釋放了呢?dealloc方法肯定是不行的。如果是控制器的話可以嘗試監聽pop方法的調用(nav的代理),viewDidDisappear方法裡面(但要記著,再次展示的時候從新添加。。。)

不調用invalidate方法,target是不會被釋放的,因為圖中的L4,L3一直存在

timer執行是否准時

不准時!

第一種不准時:有可能跳過去 線程處理比耗時的事情時會發生 還有就是timer添加到的runloop模式不是runloop當前運行的模式,這種情況經常發生。

對於第一種情況我們不應該在timer上下功夫,而是應該避免這個耗時的工作。那麼第二種情況,作為開發者這也是最應該去關注的地方,要留意,然後視情況而定是否將timer添加到runloop多個模式

雖然跳過去,但是,接下來的執行不會依據被延遲的時間加上間隔時間,而是根據之前的時間來執行。比如:

定時時間間隔為2秒,t1秒添加成功,那麼會在t2、t4、t6、t8、t10秒注冊好事件,並在這些時間觸發。假設第3秒時,執行了一個超時操作耗費了5.5秒,則觸發時間是:t2、t8.5、t10,第4和第6秒就被跳過去了,雖然在t8.5秒觸發了一次,但是下一次觸發時間是t10,而不是t10.5。

第二種不准時:不准點

比如上面說的t2、t4、t6、t8、t10,並不會在准確的時間觸發,而是會延遲個很小的時間,原因也可以歸結為2點:

RunLoop為了節省資源,並不會在非常准確的時間點觸發 線程有耗時操作,或者其它線程有耗時操作也會影響

以我來講,從來沒有特別准的時間,

IOS7以後,Timer 有個屬性叫做 Tolerance (時間寬容度,默認是0),標示了當時間點到後,容許有多少最大誤差。

它只會在准確的觸發時間到加上Tolerance時間內觸發,而不會提前觸發(是不是有點像我們的火車,只會晚點。。。)。另外可重復定時器的觸發時間點不受Tolerance影響,即類似上面說的t8.5觸發後,下一個點不會是t10.5,而是t10 + Tolerance,不讓timer因為Tolerance而產生漂移(突然想起嵌入式令人頭疼的溫漂)。

其實對於這種不准點,對我們開發影響並不大(基本是毫秒妙級別以下的延遲),很少會用到非常准點的情況。

GCD定時器簡單介紹

其實這種我們平時也經常用(一次性定時):

void dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t block);

when接受兩種類型參數:dispatch_time相對時間,相對系統的時間,比如上面相對於DISPATCH_TIME_NOW;dispatch_walltime是絕對時間,比如某年月日某時分秒。。。之後由GCD幫我們計算一個相對時間。下面說下dispatch_time,支持納秒級別

dispatch_time_t when = dispatch_time (DISPATCH_TIME_NOW, 1);// 還沒這麼用過1納秒的延遲

應該很准確了,但是定時時間到後只是將block添加到指定的queue,去執行。這樣的話,執行時間也是不保證的,首先執行線程要等待內核的調度,其次執行線程正好沒有其它事情做。如果還需要創建線程的話,就更浪費時間了。所以這個也是不符合我們期望的

when也支持DISPATCH_TIME_NOW,但是這樣就沒意義了,不如直接調用dispatch_async。而至於DISPATCH_TIME_FOREVER就更。。。

重復性定時,代碼示例如下:

// 需要強引用
@property (nonatomic, strong)dispatch_source_t gcdTime;

- (void)gcdTimerTest {
    // 這裡需要強引用
    self.gcdTime = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
    // 開始時間支持納秒級別
    dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)2 * NSEC_PER_SEC);
    // 2秒執行一次
    uint64_t dur = (uint64_t)(2.0 * NSEC_PER_SEC);
    // 最後一個參數是允許的誤差,即使設為零,系統也會有默認的誤差
    dispatch_source_set_timer(self.gcdTime, start, dur, 0);
    // 設置回調
    dispatch_source_set_event_handler(self.gcdTime, ^{
        NSLog(@"---%@---%@",[NSThread currentThread],self);
    });
    dispatch_resume(self.gcdTime);
}

取消定時器:dispatch_cancel(self.gcdTimer);,取消後再次調用dispatch_source_set_timer是沒有用的。self.gcdTimer已不可用

雖然支持納秒級別,但是定時也是不准的,上面的例子使用的是dispatch_get_global_queue隊列,執行線程也是不確定的。所以在實際開發中這種很少用,好處是它不受runloop mode限制

以上就是這篇文章的全部內容了,希望大家能夠喜歡。

【NSTimer定時器進階 詳細介紹,循環引用分析與解決】的相關資料介紹到這裡,希望對您有所幫助! 提示:不會對讀者因本文所帶來的任何損失負責。如果您支持就請把本站添加至收藏夾哦!

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved