NSTimer平时用的很多,如果不是真正的懂它,会发生各种各样的问题。如你滑动tableview的时候定时器不走了。或者是出现,内存不能释放的问题。
NSTimer是基于RunLoop的
- 先看看定时器的创建
[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timeEvent) userInfo:nil repeats:YES];
NSTimer *timer = [[NSTimer alloc]initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:1] interval:1 target:self selector:@selector(timeEvent) userInfo:nil repeats:YES];
一般情况下我们都是用第一种方法。为什么不用第二种呢??因为第二种如果只是创建一个timer对象发现timeEvent不调用。看看下面的代码
NSTimer *timer = [[NSTimer alloc]initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:1] interval:1 target:self selector:@selector(timeEvent) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
发现定时器起作用了,这说明定时器是基于RunLoop的,如果没有了RunLoop的驱动,定时器是不会执行的。
那么第一种为什么会执行呢?因为第一种在创建了定时器之后会把定时器加到RunLoop中,不用我们自己手动加。苹果给这一步做了,所以定时器得以很好的玩耍。现在我们已经通过上面证实了NSTimer得加到RunLoop中才会执行。
让NSTimer在滑动tableview的时候继续工作
如果你在viewdidload创建了一个定时器,然后又创建了tableview。当你滑动tableview的时候定时器不走了。这又是为什么呢?
这个问题又跟RunLoop的几个模式有关
Default mode(NSDefaultRunLoopMode)
默认模式中几乎包含了所有输入源(NSConnection除外),一般情况下应使用此模式。
Connection mode(NSConnectionReplyMode)
处理NSConnection对象相关事件,系统内部使用,用户基本不会使用。
Modal mode(NSModalPanelRunLoopMode)
处理modal panels事件。
Event tracking mode(UITrackingRunLoopMode)
在拖动loop或其他user interface tracking loops时处于此种模式下,在此模式下会限制输入事件的处理。例如,当手指按住UITableView拖动时就会处于此模式。
Common mode(NSRunLoopCommonModes)
这是一个伪模式,其为一组run loop mode的集合,将输入源加入此模式意味着在Common Modes中包含的所有模式下都可以处理。在Cocoa应用程序中,默认情况下Common Modes包含default modes,modal modes,event Tracking modes.
刚才不是说苹果给定时器加到RunLoop了吗?苹果加的是默认的模式NSDefaultRunLoopMode,当你滑动tableview的时候runloop将会切换到UITrackingRunLoopMode,切换了模式,当然不工作了。因为你是默认的不是托拽的。解决办法就是苹果给我们默认的模式,我们再修改下,改为NSRunLoopCommonModes模式。这样所有模式都可以处理了。
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timeEvent) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
你这次滑动tableview的时候发现定时器还在执行。
多线程下创建定时器然后让定时器执行
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,scheduledTimerWithTimeInterval这个方法创建好NSTimer以后会自动将它添加到当前线程的RunLoop,但是只有主线程的RunLoop是默认打开的,而其他线程的RunLoop如果需要使用就必须手动打开,所以我们用了这句 [[NSRunLoop currentRunLoop] run];打开线程的NSRunLoop。定时器就开始工作了。
NSTimer容易导致内存泄漏。
为了保证参数的生命周期,NSTimer会对target对象做强引用,也就是retain一次。为什么要强引用你?因为如果target销毁了,我定时器还怎么来做事?谁来替我调用timeEvent呢?所以它就强引用target,因为定时器是加到RunLoop中的,所以RunLoop强引用着NSTimer,一般情况下我们的target就是当前的控制器,如果我们想让控制器如我所愿的销毁了,首先得除掉NSTimer这个祸害。要不NSTimer强引用着self,self就销毁不了。所以经常会因为不理解它导致我们的内存泄漏。
现在的唯一办法就是先让定时器死了。然后self也就释放了。定时器唯一死的办法就是
[self.timer invalidate];
self.timer = nil;
当一个timer被schedule的时候,timer会持有target对象,NSRunLoop对象会持有timer。当invalidate被调用时,NSRunLoop对象会释放对timer的持有,timer会释放对target的持有。除此之外,我们没有途径可以释放timer对target的持有。所以解决内存泄露就必须撤销timer,若不撤销,target对象将永远无法释放。
解决NSTimer的内存泄漏。
有的人会在viewWillDisappear里销毁定时器。有的人会在返回按钮里销毁定时器。虽然能解决问题但是不太友好。
我们想如果target不是咱们的控制器,timer不就不强引用self了吗?
如果timer不强引用self,那么self就可以尽情的释放了。通过这个思路可以写出一个自定义的YBTimer,通过一个YBTimerTarget作为timer 和self的纽带,timer强引用YBTimerTarget,YBTimerTarget和self是弱引用,这样我们就不会造成循环引用了。而且可以在dealloc里销毁定时器
- (void)dealloc
{
[self.timer invalidate];
self.timer = nil;
}
YBTimer实现
#import <Foundation/Foundation.h>
typedef void (^YBTimerBlock)(id userInfo);
@interface YBTimer : NSObject
+ (NSTimer *) scheduledTimerWithTimeInterval:(NSTimeInterval)interval
target:(id)aTarget
selector:(SEL)aSelector
userInfo:(id)userInfo
repeats:(BOOL)repeats;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
block:(YBTimerBlock)block
userInfo:(id)userInfo
repeats:(BOOL)repeats;
@end
#import "YBTimer.h"
//------------------------------YBTimerTargetBegin-------------------------------------
@interface YBTimerTarget : NSObject
@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL selector;
@property (nonatomic, weak) NSTimer* timer;
@end
@implementation YBTimerTarget
- (void)timeAction:(NSTimer *)timer {
if(self.target) {
[self.target performSelector:self.selector withObject:timer.userInfo afterDelay:0.0f];
} else {
[self.timer invalidate];
}
}
@end
//------------------------------YBTimerTargetEnd-------------------------------------
@implementation YBTimer
+ (NSTimer *) scheduledTimerWithTimeInterval:(NSTimeInterval)interval
target:(id)aTarget
selector:(SEL)aSelector
userInfo:(id)userInfo
repeats:(BOOL)repeats {
YBTimerTarget* timerTarget = [[YBTimerTarget 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:(YBTimerBlock)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 {
YBTimerBlock block = userInfo[0];
id info = nil;
if (userInfo.count == 2) {
info = userInfo[1];
}
if (block) {
block(info);
}
}
@end