NSTimer 是系統(tǒng)提供的定時(shí)器廊宪,系統(tǒng)提供的api也比較簡單,使用很方便女轿,項(xiàng)目開發(fā)中會(huì)經(jīng)常用到箭启。然而,在使用NSTimer時(shí)蛉迹,如果不注意傅寡,非常容易引起內(nèi)存泄露的問題。本文總結(jié)了下NSTimer 引起內(nèi)存泄露問題的原因,以及解決方案荐操。
NSTimer的使用
通常情況下大猛,NSTimer 是作為controller或者view的一個(gè)屬性來使用:
gif播放的定時(shí)器
*/@property (nonatomic, strong) NSTimer *gifPlayTimer;
timer初始化:
NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:0.01 target:self selector:@selector(refreshPlayTime) userInfo:nil repeats:YES];
? ? self.gifPlayTimer = timer;
? ? [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
該timer的作用是每隔0.01秒會(huì)執(zhí)行一次self 的 refreshPlayTime 方法。
這樣使用是沒有問題的淀零,每0.01秒確實(shí)會(huì)執(zhí)行一次 refreshPlayTime方法挽绩。
然而當(dāng)該控制器退出之后,會(huì)發(fā)現(xiàn)timer仍舊在執(zhí)行驾中,每隔0.01秒還是會(huì)調(diào)用 refreshPlayTime方法唉堪,而且,控制器退出了肩民,但是該控制器的 dealloc 方法并沒有被調(diào)用唠亚,也就是該控制器沒有被釋放,有內(nèi)存泄露的問題持痰。
既然timer沒有停止灶搜,那么手動(dòng)調(diào)用timer的 invalidate方法試一下。
通常情況下工窍,我們希望在控制器釋放的時(shí)候結(jié)束timer割卖,也就是在 dealloc 方法中將timer給停掉。代碼如下:
- (void)dealloc{
? ? [self.gifPlayTimer invalidate];
}
然而患雏,并沒有什么用鹏溯,在退出控制器之后,timer仍舊生效淹仑。原因上面其實(shí)也說了丙挽,因?yàn)榭刂破鞯?dealloc方法根本沒有被調(diào)用。為什么控制器不會(huì)被釋放匀借?以及如何解決颜阐?
NSTimer對(duì)target的強(qiáng)引用
首先看一下NSTimer初始化方法的官方文檔介紹:
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
注意target參數(shù)的描述:
The object to which to send the message specified by aSelector when the timer fires. The timer maintains a strong reference to target until it (the timer) is invalidated.
注意:文檔中寫的很清楚,timer對(duì)target會(huì)有一個(gè)強(qiáng)引用吓肋,直到timer is invalidated凳怨。也就是說,在timer調(diào)用 invalidate方法之前蓬坡,timer對(duì)target一直都有一個(gè)強(qiáng)引用猿棉。這也是為什么控制器的dealloc 方法不會(huì)被調(diào)用的原因。
由于timer對(duì)target強(qiáng)引用的特性屑咳,如果要避免控制器不釋放的問題萨赁,需要在特定的時(shí)機(jī)調(diào)用timer 的 invalidate方法,也就是提前結(jié)束timer兆龙。在通常情況下杖爽,這種方式是可以解決問題的敲董,雖然需要警惕頁面退出之前有沒有結(jié)束timer,但畢竟解決了問題不是慰安。但是腋寨,日常項(xiàng)目中通常是多人協(xié)作,如果該timer是一個(gè)view的屬性化焕,而這個(gè)view又需要讓別人使用萄窜,那timer什么時(shí)候結(jié)束呢?讓調(diào)用者來管理timer的結(jié)束顯然是不合理的撒桨。更好的方式還是應(yīng)該在dealloc 方法中結(jié)束timer查刻,這樣調(diào)用者根本無須關(guān)注timer。
那么如何解決呢凤类?
timer修飾符改為weak
上述代碼中穗泵,self強(qiáng)引用了timer,timer又強(qiáng)引用了self谜疤,導(dǎo)致timer不能釋放佃延,self也一直不能釋放,那么如果timer的修飾符是weak夷磕,能解決這個(gè)問題嘛履肃?
@property (nonatomic, weak) NSTimer *gifPlayTimer;
經(jīng)過驗(yàn)證,使用weak修飾timer并不能解決問題企锌。Why?
看一下
- (void)addTimer:(NSTimer *)timer forMode:(NSRunLoopMode)mode;
方法的文檔介紹:
The receiver retains aTimer. To remove a timer from all run loop modes on which it is installed, send an invalidate message to the timer.
也就是說榆浓,runLoop會(huì)對(duì)timer有強(qiáng)引用于未,因此撕攒,timer修飾符是weak,timer還是不能釋放烘浦,timer的target也就不能釋放抖坪。
target用weak來修飾
既然timer強(qiáng)引用了target,導(dǎo)致target一直不能釋放闷叉,如果target用weak來修飾擦俐,能解決這個(gè)問題嘛?
__weak typeof(self) weakSelf = self;
NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:0.01 target:weakSelf selector:@selector(refreshPlayTime) userInfo:nil repeats:YES];
self.gifPlayTimer = timer;
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
經(jīng)過驗(yàn)證握侧,并沒有解決問題蚯瞧。Why?
實(shí)際上,上面的寫法和直接使用self的并沒有太大的區(qū)別品擎,唯一的區(qū)別是這種寫法timer的target有可能是nil埋合,不過這種可能性太低了。
使用中間target的方式
這種方法的思路是:新建一個(gè)中間對(duì)象Object萄传,該中間對(duì)象對(duì)timer真正的target有一個(gè)弱引用甚颂,寫代碼時(shí),timer的target 是Object。timer觸發(fā)的方法仍舊是真正target中的方法振诬。
部分代碼如下:
@interface _YYImageWeakProxy : NSProxy
// 對(duì)target有一個(gè)弱引用
@property (nonatomic, weak, readonly) id target;
- (instancetype)initWithTarget:(id)target;
+ (instancetype)proxyWithTarget:(id)target;
timer的初始化方法:
NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:0.01 target:[_YYImageWeakProxy proxyWithTarget:self] selector:@selector(refreshPlayTime) userInfo:nil repeats:YES];
self.gifPlayTimer = timer;
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
使用這種方式驗(yàn)證一下蹭睡,是可以解決問題的,target的dealloc方法會(huì)被調(diào)用赶么。
這種方式避免了timer直接引用target肩豁,因此self的dealloc方法會(huì)調(diào)用,在dealloc方法中移除timer即可辫呻。
使用這種方式timer的方法如何執(zhí)行呢蓖救?其實(shí)通過上面的代碼也可以看出,借助了NSProxy類印屁。NSProxy類是和NSObject平級(jí)的類循捺,該類的作用可以簡單的理解為一個(gè)代理,將消息轉(zhuǎn)發(fā)給另一個(gè)對(duì)象雄人。?
看一下_YYImageWeakProxy中的代碼:
@implementation _YYImageWeakProxy
- (id)forwardingTargetForSelector:(SEL)selector {
? ? return _target;
}
- (void)forwardInvocation:(NSInvocation *)invocation {
? ? void *null = NULL;
? ? [invocation setReturnValue:&null];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
? ? return [NSObject instanceMethodSignatureForSelector:@selector(init)];
}
- (BOOL)respondsToSelector:(SEL)aSelector {
? ? return [_target respondsToSelector:aSelector];
}
- (Class)class {
? ? return [_target class];
}
- (BOOL)isKindOfClass:(Class)aClass {
? ? return [_target isKindOfClass:aClass];
}
- (BOOL)isMemberOfClass:(Class)aClass {
? ? return [_target isMemberOfClass:aClass];
}
- (BOOL)conformsToProtocol:(Protocol *)aProtocol {
? ? return [_target conformsToProtocol:aProtocol];
}
- (BOOL)isProxy {
? ? return YES;
}
@end
主要完成了消息轉(zhuǎn)發(fā)的功能从橘,將其接收到的消息,轉(zhuǎn)發(fā)給target础钠,這樣timer就能正確觸發(fā)對(duì)應(yīng)的方法
NSTimer+YYAdd
YYKit框架中提供了NSTimer的一個(gè)分類恰力,NSTimer+YYAdd,使用該分類中的方法旗吁,能夠解決問題踩萎,target的dealloc方法會(huì)被調(diào)用『艿觯看一下使用方法:
NSTimer * timer = [NSTimer timerWithTimeInterval:3.0f block:^(NSTimer * _Nonnull timer) {
? ? ? ? ? ? [weakSelf hideControlViewWithAnimation];
? ? ? ? } repeats:YES];
_hiddenTimer = timer;
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)seconds block:(void (^)(NSTimer *timer))block repeats:(BOOL)repeats;
該方法是NSTimer+YYAdd 提供的香府。那么該Category是如何解決timer強(qiáng)引用target的問題呢?看一下其內(nèi)部實(shí)現(xiàn)
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)seconds block:(void (^)(NSTimer *timer))block repeats:(BOOL)repeats {
? ? return [NSTimer timerWithTimeInterval:seconds target:self selector:@selector(_yy_ExecBlock:) userInfo:[block copy] repeats:repeats];
}
+ (void)_yy_ExecBlock:(NSTimer *)timer {
? ? if ([timer userInfo]) {
? ? ? ? void (^block)(NSTimer *timer) = (void (^)(NSTimer *timer))[timer userInfo];
? ? ? ? block(timer);
? ? }
}
注意:timer的target變成了self码倦,也就是timer企孩,而_yy_ExecBlock方法實(shí)際上就是執(zhí)行timer的回調(diào)。
也就是說袁稽,NSTimer+YYAdd中的解決方式也是使用中間類的方式勿璃,只不過這里的中間類正好是NSTimer對(duì)象,寫起來更簡單一些推汽。具體引用關(guān)系如下:?
invalidate方法注意事項(xiàng)
看一下invalidate方法的介紹:
兩點(diǎn):
(1)invalidate方法是唯一能從runloop中移除timer的方式补疑,調(diào)用invalidate方法后,runloop會(huì)移除對(duì)timer的強(qiáng)引用歹撒。
(2)timer的添加和timer的移除(invalidate)需要在同一個(gè)線程中莲组,否則timer可能不能正確的移除,線程不能正確退出栈妆。
在之前自己就是這樣解決循環(huán)引用的:
控制器中
- (void)viewDidDisappear:(BOOL)animated {
? ? [super viewDidDisappear:animated];
? ? [self.timer invalidate];
? ? self.timer = nil;
}
view中
- (void)removeFromSuperview {
? ? [super removeFromSuperview];
? ? [self.timer invalidate];
? ? self.timer = nil;
}
在某些情況下胁编,這種做法是可以解決問題的厢钧,但是有時(shí)卻會(huì)引起其他問題,比如控制器push到下一個(gè)控制器嬉橙,viewDidDisappear后早直,timer被釋放,此時(shí)再回來市框,timer已經(jīng)不復(fù)存在了霞扬。
2. 給self添加中間件proxy
考慮到循環(huán)引用的原因,改方案就是需要打破這些相互引用關(guān)系枫振,因此添加一個(gè)中間件喻圃,弱引用self,同時(shí)timer引用了中間件粪滤,這樣通過弱引用來解決了相互引用斧拍,如圖:
接下來看看怎么實(shí)現(xiàn)這個(gè)中間件,直接上代碼:
@interface ZYWeakObject()
@property (weak, nonatomic) id weakObject;
@end
@implementation ZYWeakObject
- (instancetype)initWithWeakObject:(id)obj {
? ? _weakObject = obj;
? ? return self;
}
+ (instancetype)proxyWithWeakObject:(id)obj {
? ? return [[ZYWeakObject alloc] initWithWeakObject:obj];
}
僅僅添加了weak類型的屬性還不夠杖小,為了保證中間件能夠響應(yīng)外部self的事件肆汹,需要通過消息轉(zhuǎn)發(fā)機(jī)制,讓實(shí)際的響應(yīng)target還是外部self予权,這一步至關(guān)重要昂勉,主要涉及到runtime的消息機(jī)制。
/**
* 消息轉(zhuǎn)發(fā)扫腺,讓_weakObject響應(yīng)事件
*/
- (id)forwardingTargetForSelector:(SEL)aSelector {
? ? return _weakObject;
}
- (void)forwardInvocation:(NSInvocation *)invocation {
? ? void *null = NULL;
? ? [invocation setReturnValue:&null];
}
- (BOOL)respondsToSelector:(SEL)aSelector {
? ? return [_weakObject respondsToSelector:aSelector];
}
接下來就可以這樣使用中間件了:
// target要設(shè)置成weakObj岗照,實(shí)際響應(yīng)事件的是self
ZYWeakObject *weakObj = [ZYWeakObject proxyWithWeakObject:self];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:weakObj selector:@selector(changeText) userInfo:nil repeats:YES];