引言
NSTimer內(nèi)存泄漏真的是因?yàn)関c與timer循環(huán)引用嗎?不是!
小伙伴們都知道,循環(huán)引用會(huì)造成內(nèi)存泄漏,所謂循環(huán)引用無非就是強(qiáng)指針連成一個(gè)圈。但是,沒連成圈的強(qiáng)指針引用同樣可能造成內(nèi)存泄漏,如NSTimer
注意:timer內(nèi)存泄漏,部分童鞋認(rèn)為是vc與timer循環(huán)引用造成的,這種說法是錯(cuò)誤的!
正文
- 內(nèi)存泄漏
NSTimer內(nèi)存泄漏的坑很多人都遇到過,為避免內(nèi)存泄漏,部分童鞋是這么做的:
- (void)dealloc {
[_timer invalidate];
}
更有甚者是這么做的:
- (void)dealloc {
[_timer invalidate];
_timer = nil;
}
然而并沒有什么...用!
通常會(huì)這么寫:
@interface NSTimerExample ()
@property (nonatomic, weak) NSTimer *timer0;
@property (nonatomic, weak) NSTimer *timer1;
@end
- (void)viewDidLoad {
[super viewDidLoad];
{
NSTimer *one = [NSTimer scheduledTimerWithTimeInterval:1.f target:self selector:@selector(tick:) userInfo:@"timer0" repeats:YES];
_timer0 = one;
}
}
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo
這句代碼timer會(huì)強(qiáng)引用target,即timer強(qiáng)引用vc,然而vc并沒有強(qiáng)引用timer,哪來的vc與timer循環(huán)引用?但是,如果vc沒有強(qiáng)引用timer,timer是如何存活的?
其實(shí),上句代碼默認(rèn)將timer加入到currentRunLoop中,currentRunLoop會(huì)強(qiáng)引用timer,而currentRunLoop就是mainRunLoop,mainRunLoop一直存活,所以timer可以存活
如,我們還會(huì)顯式的這么寫(這種runloop模式,在scrollview滑動(dòng)時(shí)同樣可以工作):
- (void)viewDidLoad {
[super viewDidLoad];
{
NSTimer *one = [NSTimer timerWithTimeInterval:1.f target:self selector:@selector(tick:) userInfo:@"timer1" repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:one forMode:NSRunLoopCommonModes];
_timer1 = one;
}
}
回到問題,為啥vc的dealloc方法進(jìn)不去?
從以上關(guān)系圖可見,只要runLoop存活,vc必然存活,所以vc的dealloc方法自然就不會(huì)執(zhí)行。因此,將timer的銷毀方法放在dealloc中必然造成內(nèi)存泄漏!
基于這種關(guān)系鏈,只要銷毀兩條線中的任意一條,就不會(huì)出現(xiàn)內(nèi)存泄漏
所以出現(xiàn)了這種方案:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
NSTimer *one = [NSTimer scheduledTimerWithTimeInterval:1.f target:self selector:@selector(tick:) userInfo:@"timer0" repeats:YES];
_timer0 = one;
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[_timer0 invalidate];
}
這樣做簡單粗暴,直接將兩條線都銷毀,確實(shí)沒有內(nèi)存泄漏,可以滿足部分場景。但是如果在這個(gè)vc基礎(chǔ)上push一個(gè)新vc,而原vc的定時(shí)器還要繼續(xù)工作,這種方案顯然無法滿足需求。
雖然NSRunLoop提供了addTimer接口,但是并沒有提供removeTimer接口,顯然,runLoop與timer這條線無法直接銷毀,所以只能從vc與timer持有關(guān)系入手。
(CFRunLoopRef有這種接口)
CF_EXPORT void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFRunLoopMode mode);
CF_EXPORT void CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFRunLoopMode mode);
思路很簡單,以NSProxy為基類,創(chuàng)建一個(gè)weak proxy類,弱引用target即可,關(guān)系圖如下:
proxy弱引用vc,所以vc可以釋放,當(dāng)vc執(zhí)行dealloc,在dealloc內(nèi)部銷毀timer即可
proxy可以這么寫:
NS_ASSUME_NONNULL_BEGIN
@interface JKWeakProxy : NSProxy
@property (nonatomic, weak, readonly) id target;
+ (instancetype)proxyWithTarget:(id)target;
@end
NS_ASSUME_NONNULL_END
@implementation JKWeakProxy
- (instancetype)initWithTarget:(id)target {
_target = target;
return self;
}
+ (instancetype)proxyWithTarget:(id)target {
return [[self alloc] initWithTarget:target];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [_target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
if ([_target respondsToSelector:invocation.selector]) {
[invocation invokeWithTarget:_target];
}
}
- (NSUInteger)hash {
return [_target hash];
}
- (Class)superclass {
return [_target superclass];
}
- (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;
}
- (NSString *)description {
return [_target description];
}
- (NSString *)debugDescription {
return [_target debugDescription];
}
@end
此時(shí)代碼只要這樣寫就可以了:
- (void)viewDidLoad {
[super viewDidLoad];
{
JKWeakProxy *proxy = [JKWeakProxy proxyWithTarget:self];
NSTimer *one = [NSTimer scheduledTimerWithTimeInterval:1.f target:proxy selector:@selector(tick:) userInfo:@"proxyTimer0" repeats:YES];
_proxyTimer0 = one;
}
}
- (void)dealloc {
[_proxyTimer0 invalidate];
}
到這里,timer內(nèi)存泄漏的坑已經(jīng)完美解決。
再簡單說說timer與runloop的組合使用
- NSTimer && NSRunLoop && autoreleasepool && 多線程
前面已經(jīng)說過如何在scrollview滑動(dòng)時(shí),讓timer繼續(xù)工作
NSTimer *one = [NSTimer timerWithTimeInterval:1.f target:self selector:@selector(tick:) userInfo:@"timer1" repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:one forMode:NSRunLoopCommonModes];
_timer1 = one;
簡單說下NSRunLoopCommonModes,NSRunLoopCommonModes是一種偽模式,它表示一組runLoopMode的集合,具體包含:NSDefaultRunLoopMode、NSTaskDeathCheckMode、UITrackingRunLoopMode。由于含有UITrackingRunLoopMode,所以可以在滑動(dòng)時(shí)繼續(xù)工作。
多線程中又是如何使用timer的呢?
{
dispatch_async(dispatch_get_global_queue(0, 0), ^{
JKWeakProxy *proxy = [JKWeakProxy proxyWithTarget:self];
NSTimer *one = [NSTimer scheduledTimerWithTimeInterval:1.f target:proxy selector:@selector(tick:) userInfo:@"dispatchTimer0" repeats:YES];
[[NSRunLoop currentRunLoop] run];
_dispatchTimer0 = one;
});
}
多線程中需要顯示啟動(dòng)runloop,為啥?
我們無法主動(dòng)創(chuàng)建runloop,只能通過[NSRunLoop currentRunLoop]
、CFRunLoopGetCurrent()
,[NSRunLoop mainRunLoop]
、CFRunLoopGetMain()
來獲取runloop。除主線程外,子線程創(chuàng)建后并不存在runloop,主動(dòng)獲取后才有runloop。當(dāng)子線程銷毀,runloop也隨之銷毀。
上句代碼為,獲取子線程的runloop,并開啟runloop,所以timer才可以正常運(yùn)行。而在主線程中,mainRunLoop已經(jīng)在程序運(yùn)行時(shí)默認(rèn)開啟,所以不需要顯示啟動(dòng)runloop。
問題又來了,runloop開啟后,timer會(huì)一直在子線程中運(yùn)行,所以子線程不會(huì)銷毀,因此runloop無法自動(dòng)停止,這似乎又是個(gè)死循環(huán)。既然無法自動(dòng)停止,可以選擇其他方式停止runloop
- runUntilDate
{
dispatch_async(dispatch_get_global_queue(0, 0), ^{
JKWeakProxy *proxy = [JKWeakProxy proxyWithTarget:self];
NSTimer *one = [NSTimer scheduledTimerWithTimeInterval:1.f target:proxy selector:@selector(tick:) userInfo:@"dispatchTimer0" repeats:YES];
// [[NSRunLoop currentRunLoop] run];
NSDate *date = [NSDate dateWithTimeIntervalSinceNow:5.f];
[[NSRunLoop currentRunLoop] runUntilDate:date];
_dispatchTimer0 = one;
});
}
這樣可以讓runloop在5s后銷毀,但是如果銷毀時(shí)間不確定怎么辦?
這涉及到runloop的運(yùn)行機(jī)制:
runloop在指定mode模式下運(yùn)行,每種mode至少要有一個(gè)mode item,如果runloop在某種mode模式下不含mode item,那么runloop直接退出。mode item有3種,分別為: Source/Timer/Observer,它們對(duì)應(yīng)CFRunLoopSourceRef/CFRunLoopTimerRef/CFRunLoopObserverRef。Source為事件源,Timer為時(shí)間觸發(fā)器,Observer為觀察者。
OK,了解這些之后現(xiàn)在可以銷毀子線程了
- invalidate
如果runloop在某種特停情況下要退出,由于上述代碼runloop的mode item只有Timer,所以只要銷毀timer,runloop就會(huì)退出
[_timer invalidate];
- NSTimer并非真實(shí)的時(shí)間機(jī)制
什么意思?即NSTimer并非基于我們現(xiàn)實(shí)生活中的物理時(shí)間。似乎還不是太好懂,可以從以下幾點(diǎn)理解:
- timer需要在某種特定的runLoopMode下運(yùn)行,如果當(dāng)前mode為非指定mode,timer不會(huì)被觸發(fā),直到mode變成指定mode,timer開始運(yùn)行
- 如果在指定mode下運(yùn)行,但timer觸發(fā)事件的時(shí)間點(diǎn)runloop剛好在處理其他事件,timer對(duì)應(yīng)的事件不會(huì)被觸發(fā),直到下一次runloop循環(huán)
- 如果timer設(shè)置精度過高,由于runloop可能存在大量mode item,timer精度過高極有可能timer對(duì)應(yīng)處理事件時(shí)間點(diǎn)出現(xiàn)誤差(精度最好不超過0.1s)
Tip: runloop的內(nèi)部是通過dispatch_source_t和mach_absolute_time()控制時(shí)間的,因此要實(shí)現(xiàn)精確的定時(shí)器,應(yīng)使用dispatch_source_t或者mach_absolute_time()。CADisplayLink在某種程度上也可以當(dāng)做定時(shí)器,但與NSTimer一樣,并不準(zhǔn)確。由于默認(rèn)刷新頻率與屏幕相同,因此可以用來檢測FPS
- autoreleasepool
既然說到runloop,簡單說下autoreleasepool。runloop會(huì)默認(rèn)創(chuàng)建autoreleasepool,在runloop睡眠前或者退出前會(huì)執(zhí)行pop操作
autoreleasepool的簡單應(yīng)用
NSLog(@"begin");
for (NSUInteger i = 0; i < 10000; i++) {
NSString *str = [NSString stringWithFormat:@"hello %zd", i];
NSLog(@"%@", str);
}
NSLog(@"end");
可以看到,內(nèi)存一直在增加,并且for循環(huán)結(jié)束后,內(nèi)存仍然不會(huì)釋放
可以將代碼加上autoreleasepool:
NSLog(@"begin");
for (NSUInteger i = 0; i < 10000; i++) {
@autoreleasepool {
NSString *str = [NSString stringWithFormat:@"hello %zd", i];
NSLog(@"%@", str);
}
}
NSLog(@"end");
可以看到,內(nèi)存幾乎沒有變化