OC 如何正確釋放GCD定時器(dispatch_source_t)以及防止Crash

Dispatch Source Timer 的使用以及注意事項

Dispatch Source Timer 是一種與 Dispatch Queue 結合使用的定時器。當需要在後臺 queue 中定期執行任務的時候,使用 Dispatch Source Timer 要比使用 NSTimer 更加自然,也更加高效(無需在 main queue 和後臺 queue 之前切換)。

使用如下:

@property (nonatomic,strong) dispatch_source_t timer;

/** 創建定時器對象

* para1: DISPATCH_SOURCE_TYPE_TIMER 爲定時器類型

* para2-3: 中間兩個參數對定時器無用

* para4: 最後爲在什麼調度隊列中使用

*/

_gcdTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));

/** 設置定時器

* para2: 任務開始時間

* para3: 任務的間隔

* para4: 可接受的誤差時間,設置0即不允許出現誤差

* Tips: 單位均爲納秒

*/

dispatch_source_set_timer(_gcdTimer, DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC, 0.0 * NSEC_PER_SEC);

/** 設置定時器任務

* 可以通過block方式

* 也可以通過C函數方式

*/

dispatch_source_set_event_handler(_gcdTimer, ^{

    static int gcdIdx = 0;

    NSLog(@"GCD Method: %d", gcdIdx++);

    NSLog(@"%@", [NSThread currentThread]);

});

// 啓動任務,GCD計時器創建後需要手動啓動

dispatch_resume(_gcdTimer);

如果需要暫停定時器調用:

dispatch_suspend(timer);

dispatch_suspend 和 dispatch_resume 應該是成對出現的。兩者分別會減少和增加 dispatch 對象的掛起計數,但是沒有 API 獲取當前是掛起還是執行狀態,所以需要自己記錄。

 

dispatch_suspend 暫停隊列並不意味着當前執行的 block 暫停

當暫停派發隊列時需要注意,調用 dispatch_suspend 暫停一個隊列,並不意味着暫停當前正在執行的 block,而是 block 可以執行完,但是接下來的 block 會被暫停,直到 dispatch_resume 被調用。

dispatch_suspend 狀態下無法釋放

如果調用 dispatch_suspend 後 timer 是無法被釋放的。一般情況下會發生崩潰並報“EXC_BAD_INSTRUCTION”錯誤,看下 GCD 源碼dispatch source release 的時候判斷了當前是否是在暫停狀態。

所以,dispatch_suspend 狀態下直接釋放當前控制器或者釋放定時器,會導致定時器崩潰。

並且初始狀態(未調用dispatch_resume)、掛起狀態,都不能直接調用dispatch_source_cancel(timer),調用就會導致app閃退。

建議一:儘量不使用dispatch_suspend,在dealloc方法中,在dispatch_resume狀態下直接使用dispatch_source_cancel來取消定時器。

建議二:使用懶加載創建定時器,並且記錄當timer 處於dispatch_suspend的狀態。這些時候,只要在 調用dealloc 時判斷下,已經調用過 dispatch_suspend 則再調用下 dispatch_resume後再cancel,然後再釋放timer。

 

停止 Timer

停止 Dispatch Timer 有兩種方法,一種是使用 dispatch_suspend,另外一種是使用 dispatch_source_cancel。

dispatch_suspend 嚴格上只是把 Timer 暫時掛起,它和 dispatch_resume 是一個平衡調用,兩者分別會減少和增加 dispatch 對象的掛起計數。當這個計數大於 0 的時候,Timer 就會執行。在掛起期間,產生的事件會積累起來,等到 resume 的時候會融合爲一個事件發送。

dispatch_source_cancel 則是真正意義上的取消 Timer。被取消之後如果想再次執行 Timer,只能重新創建新的 Timer。這個過程類似於對 NSTimer 執行 invalidate。

關於取消 Timer,另外一個很重要的注意事項,dispatch_suspend 之後的 Timer,是不能被釋放的!下面的代碼會引起崩潰:

- (void)dealloc

{

    dispatch_suspend(_timer);

_timer =nil;// EXC_BAD_INSTRUCTION 崩潰

}

因此使用 dispatch_suspend 時,Timer 本身的實例需要一直保持。使用 dispatch_source_cancel 則沒有這個限制:

- (void)dealloc

{

    dispatch_source_cancel(_timer);

_timer =nil;// OK

}

總結:

Dispatch Source使用最多的就是用來實現定時器,source創建後默認是暫停狀態,需要手動調用dispatch_resume啓動定時器。

Dispatch Source定時器使用時也有一些需要注意的地方,不然很可能會引起crash:

1、循環引用:因爲dispatch_source_set_event_handler回調是個block,在添加到source的鏈表上時會執行copy並被source強引用,如果block裏持有了self,self又持有了source的話,就會引起循環引用。正確的方法是使用weak+strong或者提前調用dispatch_source_cancel取消timer。

2、dispatch_resume和dispatch_suspend調用次數需要平衡,如果重複調用dispatch_resume則會崩潰,因爲重複調用會讓dispatch_resume代碼裏if分支不成立,從而執行了DISPATCH_CLIENT_CRASH("Over-resume of an object")導致崩潰。

3、source在suspend狀態下,如果直接設置source = nil或者重新創建source都會造成crash。正確的方式是在resume狀態下調用dispatch_source_cancel(source)釋放當前的source。

4、儘量不使用dispatch_suspend,在dealloc方法中,在dispatch_resume狀態下直接使用dispatch_source_cancel來取消定時器。

5、使用懶加載創建定時器,並且記錄當timer 處於dispatch_suspend的狀態。這些時候,只要在 調用dealloc 時判斷下,已經調用過 dispatch_suspend 則再調用下 dispatch_resume後再cancel,然後再釋放timer。


 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章