NSTimer不就是定时器吗,这个平时经常用的,
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
self->startTimer = [NSTimer scheduledTimerWithTimeInterval:self.time target:self selector:@selector(start:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] run];
});
这样写之后不做其他处理的同学断点下dealloc方法,你会情不自禁的说三句“我擦、我擦、我擦”。(😄)
NSTimer导致的内存泄漏
少年郎之前一直都是这样用NSTimer,在排查一个页面内存泄漏的时候发现VC pop之后dealloc死活不走,该弱化的对象已经弱化,该处理的方法已经处理,即便是所有成员变量的引用计数也都一一排查确定正常之后dealloc方法还是不走,最后只能向一直认为没有问题的定时器下手,问题居然还真他妈的出现在这。向之前写的项目默哀半分钟,之前用错你了😢。
查过资料了解到,为了保证参数的生命周期,NSTimer会对target对象retain一次,做强引用。以保证即便target销毁了,定时器还能正常调用timeEvent。因为定时器要加到RunLoop中,所以RunLoop强引用着NSTimer,一般情况下你的target就是当前的控制器,如果你想让控制器如你所愿的销毁了,首先得销毁NSTimer。不然NSTimer强引用着self,self就无法销毁,从而导致内存泄漏。
销毁定时器的方法
[startTimer invalidate];
startTimer = nil;
timer被schedule的时候,timer会持有target对象,NSRunLoop对象会持有timer。当invalidate被调用时,NSRunLoop对象会释放对timer的持有,timer会释放对target的持有。除此之外,没有途径可以释放timer对target的持有。所以解决内存泄露就必须撤销timer,若不撤销,target对象将永远无法释放。
销毁timer
如果可以的话在viewWillDisappear方法里销毁timer,或者点击返回按钮的时候销毁timer,不过这些都是局限性。如果要再push一个VC,视图消失的时候销毁timer就会有问题,返回按钮的方法里销毁还要考虑左滑手势...,这些感觉都不太“科学”。
看到有大佬把target特殊处理成不是当前控制器,timer就不再强引用self,dealloc方法自然就能正常调用。
- (void)dealloc
{
[startTimer invalidate];
startTimer = nil;
}
具体实现代码
#import <Foundation/Foundation.h>
typedef void (^WXWTimerBlock)(id userInfo);
@interface WXWTimer : NSObject
+ (NSTimer *) scheduledTimerWithTimeInterval:(NSTimeInterval)interval
target:(id)aTarget
selector:(SEL)aSelector
userInfo:(id)userInfo
repeats:(BOOL)repeats;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
block:(WXWTimerBlock)block
userInfo:(id)userInfo
repeats:(BOOL)repeats;
@end
@interface WXWTimerTarget : NSObject
@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL selector;
@property (nonatomic, weak) NSTimer* timer;
@end
@implementation WXWTimerTarget
- (void)timeAction:(NSTimer *)timer {
if(self.target) {
[self.target performSelector:self.selector withObject:timer.userInfo afterDelay:0.0f];
} else {
[self.timer invalidate];
}
}
@end
@implementation WXWTimer
+ (NSTimer *) scheduledTimerWithTimeInterval:(NSTimeInterval)interval
target:(id)aTarget
selector:(SEL)aSelector
userInfo:(id)userInfo
repeats:(BOOL)repeats {
WXWTimerTarget* timerTarget = [[WXWTimerTarget alloc] init];
timerTarget.target = aTarget;
timerTarget.selector = aSelector;
timerTarget.timer = [NSTimer scheduledTimerWithTimeInterval:interval
target:timerTarget
selector:@selector(timeAction:)
userInfo:userInfo
repeats:repeats];
return timerTarget.timer;
}
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
block:(WXWTimerBlock)block
userInfo:(id)userInfo
repeats:(BOOL)repeats {
NSMutableArray *userInfoArray = [NSMutableArray arrayWithObject:[block copy]];
if (userInfo != nil) {
[userInfoArray addObject:userInfo];
}
return [self scheduledTimerWithTimeInterval:interval
target:self
selector:@selector(timerBlock:)
userInfo:[userInfoArray copy]
repeats:repeats];
}
+ (void)timerBlock:(NSArray*)userInfo {
WXWTimerBlock block = userInfo[0];
id info = nil;
if (userInfo.count == 2) {
info = userInfo[1];
}
if (block) {
block(info);
}
}
@end
NSTimer与RunLoop的关系
之前写了个类似淘宝的消息滚动控件。本来也没有注意到这个问题,换工作面试的时候面试官看到这个问我怎么实现的,直接回答说:“用NSTimer加个定时器,设置重复滚动就可以了”。面试官笑了笑没说什么。场面一度十分尴尬。回来之后查了下才知道人家想要的回答应该是下面这样的,而不是我认为的那样😂。
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timeEvent) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
苹果加入RunLoop的默认的模式是NSDefaultRunLoopMode,当你滑动scrollView的时候runloop将会切换到UITrackingRunLoopMode、在加入的时候设置NSRunLoopCommonModes模式。这样所有模式都可以工作了。
非主线程中创建定时器需要让定时器执行
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timeEvent) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] run];
});
RunLoop用来循环处理响应事件,每个线程都有一个RunLoop,苹果不允许自己创建RunLoop, 而且只有主线程的RunLoop是默认打开的,其他线程的RunLoop如果需要使用就必须手动打开。scheduledTimerWithTimeInterval这个方法创建好NSTimer以后会自动将它添加到当前线程的RunLoop,非主线程中只有调用run方法定时器才能开始工作