我是如何實現自釋放timer的

引言

我們都知道timer在使用的時候有很多坑,比如強引用target導致循環引用,甚至內存泄露問的,timer觸發時機不準的問題,子線程中的timer要手動加入runloop...
我在timer中的那些坑里已經說過這些問題了,在最后我們提到了自釋放這個概念,今天就來聊聊我是如何實現自釋放timer的。

兩個重要問題

我們知道timer的循環引用主要原因有兩個:
1.timer基于runloop。
2.timer強引用自己的target

理解了循環引用的原因,就邁出了實現自釋放timer的第一步。

我們現在分析自釋放timer要想達到效果關鍵點在哪里?

1.如何打破timer的循環引用?
2.如何做到自釋放?

這兩個問題是有先后關系的,只有解決了問題一,才能解決問題二,下面我們就來看看如何解決這兩個問題。

如何打破timer的循環引用?

解決timer循環引用的問題,我們就要看產生循環引用的原因:
1.timer基于runloop,這是一個事實,是無法改變的,這里可以把runloop簡單的看成是系統,就是說timer是基于系統的,如果timer的回調還沒有發生,那么系統是不會釋放這個timer的,不管這個timer是不是全局變量或局部變量,或者壓根就沒聲明。深刻理解了這一點,我們就會知道,這個是系統合理的做法,我們很難在這一點上有什么突破。

2.timer強引用自己的target,這也是一個事實,但是我們會想,可不是以不把這個target設置成使用timer的那個對象呢?比如一個vc要用timer,timer的target并不指向這個vc,這樣,這個vc要銷毀的時候就不受timer牽制了,即使在dealloc里手動調用invalidate也不會造成循環引用的尷尬了。

這是我想到的一個思路,那么問題又來了:我們把這個target指向誰呢?答案:NSTimer的類對象。類對象也是一個對象,不理解的可以看看我的另一篇文章為什么object_getClass(obj)與[OBJ class]返回的指針不同在這個文章總有說明,其實類對象就像是一個單例,每個類在程序運行的時候都有一個這樣的單例的類的對象存在于段內存中。

我們利用這一點可以把引用循環打破,但是問題又來了:如果不指向vc這個self,回調如何給到vc呢?

這里的解決辦法是,自定義一個timer的初始化方法,vc可以傳入一個block作為回調,這個block作為了這個timer對象的userInfo保存起來,因為我們把這個timer對象的target設置成了自身的類對象,這樣其實timer的回調是回調給了NSTimer,而回調方法傳遞的參數正式這個timer對象本身,從這個timer對象中取出userInfo字段,其實就是vc設置的block,此時執行這個block就把回調轉發給了vc。

到此就可以解決timer循環引用的問題了。

如何做到自釋放?

完成了第一步,其實就可以在dealloc中調用invalidate方法了,完成了第一步其實timer就變得容易使用了,但是要想做到極致,就是dealloc中也不想寫一句代碼就搞定這一切,就要想第二個問題:如何做到自釋放?

這個問題相對簡單一點,就是:其實應該在dealloc方法中去手動釋放,如何能做到在dealloc方法執行的時候不用寫代碼就能將timer釋放?有些人可能已經猜到了,答案就是AOP。我們這里交換的是dealloc的方法實現,但是如果我們直接用@selector(dealloc)的方式去交換dealloc的實現的時候編譯器是報錯的,大概意思就是不允許我們交換dealloc方法,我們這里可以用NSSelectorFromString(@"dealloc")的方式巧妙的避過這個問題。

當我們交換了dealloc方法,實際上就知道了使用timer的對象的dealloc時機,在這里面我們用runtime拿到該vc的ivarlist,判斷如果該ivar是timer,且該timer.isValid = YES,此時我們調用invalidate就可以了。

到此自釋放timer的思路就講完了,下面上代碼。

##GJTimer

GJTimer.h

/*!
 @header     GJTimer.h
 @abstract   NSTimer's category
 @discussion NSTimer會強引用target而導致有可能出現循環引用的問題,該Category主要解決循環引用
             并簡單的實現自釋放功能,一個對象的timer屬性或變量并不需要考慮在合適的時機調用invalidate
             timer會在該對象銷毀的時候自動invalidate。
 @author     guoxiaoliang850417@163.com
 */
 
#import <Foundation/Foundation.h>

typedef void (^GJSimpleBlock)();

@interface NSTimer(GJWeakTimer)

/**
 *  創建timer對象
 *  @param ti      timeInterval
 *  @param yesOrNo repeat
 *  @param block   timer處理具體事件的回調
 *  @param aTarget 持有timer作為屬性或字段的類
 *  @return timer對象
 */
+ (NSTimer *)gjw_scheduledWithTimeInterval:(NSTimeInterval)ti repeats:(BOOL)yesOrNo block:(GJSimpleBlock)block target:(id)aTarget;

/**
 *  暫停
 */
- (void)gjw_pauseTimer;

/**
 *  復位
 */
- (void)gjw_resumeTimer;

/**
 *  delay interval 之后復位
 *  @param interval timeInterval
 */
- (void)gjw_resumeTimerAfterTimeInterval:(NSTimeInterval)interval;

@end

GJTimer.m

#import "GJTimer.h"
#import <objc/runtime.h>

@interface NSObject(GJTimerTarget)

@end

@implementation NSObject(GJTimerTarget)

- (void)gjw_dealloc {
    u_int count;
    Ivar *ivars = class_copyIvarList([self class], &count);
    for (int i = 0; i < count; i++) {
        NSString *ivarName = [[NSString alloc] initWithCString:ivar_getTypeEncoding(ivars[i]) encoding:NSUTF8StringEncoding];
        if ([ivarName rangeOfString:@"NSTimer"].location != NSNotFound) {
            NSTimer *tempTimer = (NSTimer *)object_getIvar(self, ivars[i]);
            if (tempTimer.isValid) {
                [tempTimer invalidate];
            }
        }
    }
    [self gjw_dealloc];
}

@end

@implementation NSTimer(GJWeakTimer)

#pragma mark - public methods
+ (NSTimer *)gjw_scheduledWithTimeInterval:(NSTimeInterval)ti repeats:(BOOL)yesOrNo block:(GJSimpleBlock)block target:(id)aTarget {
    NSTimer *tempTimer = [NSTimer scheduledTimerWithTimeInterval:ti target:self selector:@selector(p_blockInvoke:) userInfo:[block copy] repeats:yesOrNo];
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        method_exchangeImplementations(class_getInstanceMethod([aTarget class], NSSelectorFromString(@"dealloc")), class_getInstanceMethod([aTarget class], @selector(gjw_dealloc)));
    });
    return tempTimer;
}

- (void)gjw_pauseTimer {
    if (self.isValid) {
        [self setFireDate:[NSDate distantFuture]];
    }
}

- (void)gjw_resumeTimer {
    [self gjw_resumeTimerAfterTimeInterval:0];
}

- (void)gjw_resumeTimerAfterTimeInterval:(NSTimeInterval)interval {
    if (![self isValid]) {
        [self setFireDate:[NSDate dateWithTimeIntervalSinceNow:interval]];
    }
}

#pragma mark - private methods
+ (void)p_blockInvoke:(NSTimer *)sender {
    GJSimpleBlock block = sender.userInfo;
    if (block) {
        block();
    }
}

@end

##****結論
以上是我實現自釋放timer的思路,希望對大家有幫助,源碼方法了github上的一個GJGroup組里,這是我和同事們一起成立的一個組,希望大家支持這個組里的其他項目,多謝。

歡迎大家和我交流溝通,若文章中有錯誤和紕漏,懇請指正,謝謝,如果感覺評論不方便歡迎微博私信。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 我們常用NSTimer的方式 如下代碼所示,是我們最常見的使用timer的方式 當使用NSTimer的schedu...
    yohunl閱讀 1,699評論 1 17
  • 引言 我們在使用timer的時候多多少少都遇到過一些坑,今天就來說說timer使用中的那些坑 1.循環引用導致的內...
    二亮子閱讀 3,253評論 0 10
  • 之前要做一個發送短信驗證碼的倒計時功能,打算用NSTimer來實現,做的過程中發現坑還是有不少的。 基本使用 NS...
    WeiHing閱讀 4,402評論 1 8
  • # 前言 反復地復習iOS基礎知識和原理,打磨知識體系是非常重要的,本篇就是重新溫習iOS的內存管理。 內存管理是...
    Vein_閱讀 828評論 0 2
  • 十多年前,我還是個不諳世事的小女孩。那時候姑奶奶背著個長凳,領我去看僮子戲,當時我的心情與多數人是一致的:無趣、敷...
    北嶺的燕子閱讀 2,098評論 19 5