引言
我們都知道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組里,這是我和同事們一起成立的一個組,希望大家支持這個組里的其他項目,多謝。