1. 程式人生 > >封裝一個GCD定時器,徹底解決定時器迴圈引用、釋放時機問題

封裝一個GCD定時器,徹底解決定時器迴圈引用、釋放時機問題

相信大家在開發中都會使用到定時器, 但又常常對定時器的迴圈引用問題, NSTimer 釋放時機的選擇上,勞神費力! 讀了本文,這些再也不是問題!
關於 NSTimer 建立定時器的方法,我就不多做描述了,網上很多例子,但也總覺得很麻煩。本文主要講使用GCD的方法。

今天在重構程式碼的時候,發現專案中好幾個地方都用到了定時器,就想著封裝一個定時器的方法,以後用著方便,也可以豐富自己的工具類庫;寫的時候,發現 GCD定時器方法並不被執行,細看之下感覺跟以前寫的也沒什麼區別啊,但為什麼就沒有執行呢?

void dispatchTimer(double timeInterval, void (^handler)())
{
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0
); dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0,queue); dispatch_source_set_timer(timer,dispatch_time(DISPATCH_TIME_NOW, 0),1.0*NSEC_PER_SEC, 0); dispatch_source_set_event_handler(timer, ^{ dispatch_async(dispatch_get_main_queue(), ^{ handler(); }); }); dispatch_resume(timer); }

細想一下,原來是因為 timer 是個區域性變數,當函式執行完之後,會被自動釋放,也就不會再執行handler的方法了;忽略了以前在控制器中實現的時候 都會用一個屬性來記錄下 timer;

如何保證 在執行 handler 時 timer 不被釋放呢? 我需要對 timer 有一個強引用, 如果按原來的思路在用到定時器的類裡用屬性記錄下timer,把timer作為引數傳入,我就需要每次呼叫這個函式的時候先建立好一個 定時器,那麼封裝的這個方法也就沒有什麼意義了。

後來想到一個方法: 給回撥 handler 裡新增一個 dispatch_source_t timer 引數,在 dispatch_source_set_event_handler 的block裡回撥 handler時將區域性變數 timer 回傳; 這個時候dispatch_source_set_event_handler的 block 相當於對 timer 進行了一次強引用,這樣一來 timer 也就不會被過早釋放了;
如有對 block 記憶體管理不慎明白的可以看一下這篇文章

block對外部變數的記憶體管理

void dispatchTimer(double timeInterval,void (^handler)(dispatch_source_t timer))
{
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        dispatch_source_t timer =dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,0, 0, queue);
        dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), (uint64_t)(timeInterval *NSEC_PER_SEC), 0);
        // 設定回撥
        dispatch_source_set_event_handler(timer, ^{
            dispatch_async(dispatch_get_main_queue(), ^{
                //編譯時block會對timer物件強引用,使timer不會被過早釋放
                if (handler) handler(timer);
            });
        });
        // 啟動定時器
        dispatch_resume(timer);
}

在使用的時候才發現,handler 裡新增的那個 dispatch_source_t timer 引數,原來也是必須要有的,我需要 timer物件 來結束定時器或銷燬定時器。o(╯□╰)o 啊,原以為解決了一個問題,沒想到這是必須的一個引數。o(╯□╰)o o(╯□╰)o o(╯□╰)o

後來考慮到,當使用定時器的物件銷燬的時候,可能並沒有手動對定時器進行釋放,於是又完善了一下, 添加了一個 target 引數,當 target 銷燬時,就將 定時器銷燬。哈哈,終於功德圓滿了。

先看看使用效果吧:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    __block int timeCount = 60;
    dispatchTimer(self, 1.0, ^(dispatch_source_t timer) {

        if (timeCount < 0) {
            dispatch_source_cancel(timer);
        } else {
            NSLog(@"%d", timeCount);
            timeCount -= 5;
        }
    });

}

注意:

呼叫 dispatch_source_cancel(timer); 就會將 timer 物件銷燬,唯一要考慮的就是 timer 的結束時機;即使你不主動釋放timer, 也不會造成迴圈引用哦 O(∩_∩)O~

上原始碼:

/**
 開啟一個定時器

 @param target 定時器持有者
 @param timeInterval 執行間隔時間
 @param handler 重複執行事件
 */
void dispatchTimer(id target, double timeInterval,void (^handler)(dispatch_source_t timer))
{
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        dispatch_source_t timer =dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,0, 0, queue);
        dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), (uint64_t)(timeInterval *NSEC_PER_SEC), 0);
        // 設定回撥
    __weak __typeof(target) weaktarget  = target;
        dispatch_source_set_event_handler(timer, ^{
            if (!weaktarget)  {
                dispatch_source_cancel(timer);
            } else {
                dispatch_async(dispatch_get_main_queue(), ^{
                    if (handler) handler(timer);
                });
            }
        });
        // 啟動定時器
        dispatch_resume(timer);
}

將此函式copy到你的工具類中就可以放心使用了。

媽媽再也不用擔心 我的定時器的迴圈引用問題了! 哈哈,有沒有很 激動!

如有問題,歡迎評論交流!