原文地址:http://www.lxweimin.com/p/0c050af6c5ee
我們常常會延遲某件任務的執行,或者讓某件任務周期性的執行。然后也會在某些時候需要取消掉之前延遲執行的任務。
延遲操作的方案一般有三種:
1.NSObject的方法:
![Uploading 640_942994.jpeg . . .]
2.使用NSTimer的方法:
3.使用GCD的方法:
一般情況下,我們選擇使用GCD的dispatch_after。
因為如果不用GCD,編碼需要注意以下三個細節:
1.必須保證有一個活躍的runloop。
performSelector和scheduledTimerWithTimeInterval方法都是基于runloop的。我們知道,當一個應用啟動時,系統會開啟一個主線程,并且把主線程的runloop激活,也就是run起來,并且主線程的runloop是不會停止的。所以,當這兩個方法在主線程可以被正常調用。但情況往往不是這樣的。實際編碼中,我們更多的邏輯是放在子線程中執行的。而子線程的runloop是默認關閉的。這時如果不手動激活runloop,performSelector和scheduledTimerWithTimeInterval的調用將是無效的。
2.NSTimer的創建與撤銷必須在同一個線程操作、performSelector的創建與撤銷必須在同一個線程操作。
3.內存管理有潛在泄露的風險
scheduledTimerWithTimeInterval方法將target設為A對象時,A對象會被這個timer所持有,也就是會被retain一次,timer會被當前的runloop所持有。performSelector:withObject:afterDelay:方法實際上是在當前線程的runloop里幫你創建的一個timer去執行任務,所以和scheduledTimerWithTimeInterval方法一樣會retain其調用對象。但是,我們往往不希望因為這些延遲操作而影響對象的生命周期,更甚至是,導致對象無法釋放。舉個例子:
你創建的對象X中有一個實例變量_timer,方法fireTimer觸發一個timer,方法cancel取消這個timer。在對象X需要銷毀的時候,需要將它的timer取消掉。
不幸的是,dealloc方法將永遠不會被調用。因為timer的引用,對象X的引用計數永遠不會降到0,dealloc方法也就不會被調用。這時如果不調用cancel,對象X將永遠無法釋放,造成內存泄露。想想一個對象若不調用某一個方法就會造成內存泄露,這該是多大一個坑。
This method is the only way to remove a timer from an NSRunLoop object. The NSRunLoop object removes its strong reference to the timer, either just before the invalidate method returns or at some later point.
If it was configured with target and user info objects, the receiver removes its strong references to those objects as well.
You must send this message from the thread on which the timer was installed. If you send this message from another thread, the input source associated with the timer may not be removed from its run loop, which could prevent the thread from exiting properly.
上面摘自蘋果官方文檔對invalidate方法的解釋。可以看到,當一個timer被schedule的時候,timer會持有target對象,NSRunLoop對象會持有timer。當invalidate被調用時,NSRunLoop對象會釋放對timer的持有,timer會釋放對target的持有。除此之外,我們沒有途徑可以釋放timer對target的持有。所以解決內存泄露就必須撤銷timer,若不撤銷,target對象將永遠無法釋放。
若使用dispatch_after,系統會幫我們處理線程級的邏輯,這樣也我們更易于享受系統對線程所做的優化。除此之外,我們不用關心runloop的問題。并且調用的對象也不會被強行持有,這樣上述的內存問題也不復存在。當然,需要注意block會持有其傳入的對象,但這可以通過weakself解決。所以在這種延遲操作方案中,使用dispatch_after更佳。
但是呢,dispatch_after有個致命的弱點:dispatch_after一旦執行后,就不能撤銷了。而performSelector可以使用cancelPreviousPerformRequestsWithTarget方法撤銷,NSTimer也可以調用invalidate進行撤銷。(注意:撤銷任務與創建timer任務必須在同一個線程,即同一個runloop)所以我們還是得用NSTimer或者performSelector嗎?
NO,其實GCD也有timer的功能。用GCD來實現一個timer:
![Uploading 640-2_270235.jpeg . . .]
這樣我們就規避了NSTimer的三個缺陷。
到這里問題基本得到了解決,但是我們還可以做的更好:)
1.GCD的timer使用的API比較冗余,每次使用都會copy代碼。2.沒有repeats的選項,若只想執行一次還得自己寫標記位控制。這些問題我們都可以封裝成一個統一的API:
這樣,外部只需調用這個兩個接口,用起來和NSTimer一樣方便!
上面的代碼就創建了一個名叫myTimer的timer,這個timer將在2 seconds后執行一個block,隨后timer自動停止并被釋放。當然,如果repeats參數傳入的是YES,那么這么timer會一個周期接一個周期的執行,直到你cancel掉這個timer。
當然,你可以在self對象的dealloc方法里面做cancel,這樣保證了timer恰好運行于整個對象的生命周期中。這是NSTimer和performSelector所做不到的事情。你也可以通過queue參數控制這個timer所添加到的線程,也就是action最終執行的線程。傳入nil則會默認放到子線程中執行。UI相關的操作需要傳入dispatch_get_main_queue()以放到主線程中執行。
Well, we can actually do even better.
注意到,我們經常遇到的場景是,在開始新一次計時的時候,取消掉上一次的計時。也就是每次schedule之前先cancel。這部分對任務處理的能力,也是可以集成到我們的組件中的。我們可以向外部提供一個枚舉類型的選項,以選擇其對任務的處理類型:
或者場景是,在開始新一次計時的時候,取消上一次的計時,但是將上一次計時的任務,合并到新的一次計時中,最終一并執行。
針對這兩種場景,JX_GCDTimerManager提供了兩個option選項:
如果你不care這些使用場景的話,默認使用AbandonPreviousAction就行了。需要注意的是,同一個timer建議保持同一個任務處理方式,即相同的ActionOption,如果需要切換option,請注意一下切換的銜接問題。
大家也可以自行去對actionOption做擴展,以滿足常見的使用場景。
詳細的實現,見git源碼。
綜上,選擇使用GCD的技術有助于我們提高代碼的健壯性與穩定性。
注:文中若有錯誤,恭請斧正哈!:)