深挖NSTimer

1.没有scheduled方式初始化的,需要我们手动调用addTimer:forMode:方法,将timer添加到一个runloop中.

举例:

//默认添加到RunLoop中
NSTimer *timer1 = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerFired:) userInfo:nil repeats:NO];
//手动添加到RunLoop中
NSTimer *timer2 = [NSTimer timerWithTimeInterval:3.0 target:self selector:@selector(timerFired:) userInfo:nil repeats:NO];
[[NSRunLoop currentRunLoop] addTimer:timer2 forMode:NSDefaultRunLoopMode];

2.scheduled方式创建的timer不会立即执行,需要调用fire函数或者手动调用一次目标方法。

3.timer并不是一种实时机制,会存在延迟,而且延迟的程度跟当前线程的执行情况有关。通常会有50ms-100ms的延迟。NSTimer不是一个实时系统,因此不管是一次性的还是周期性的timer的实际触发事件的时间可能都会跟我们预想的会有出入。差距的大小跟当前我们程序的执行情况有关系,比如可能程序是多线程的,而你的timer只是添加在某一个线程的runloop的某一种指定的runloopmode中,由于多线程通常都是分时执行的,而且每次执行的mode也可能随着实际情况发生变化。

4.当用户滑动显示屏,则会出现Timer失效,方法不调用的情况

因为一般Timer是运行在RunLoop的default mode上,而ScrollView在用户滑动时,主线程RunLoop会切换到UITrackingRunLoopMode。而这个时候,Timer就不会运行,方法得不到fire。

第一种解决方法:修改RunLoop 运行模式为 NSRunLoopCommonModes

_timer = [NSTimer timerWithTimeInterval:5 target:self selector:@selector(update:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];

第二种解决方法:在子线程中启动timer,添加timer到runloop,注意:子线程中RunLoop需要手动配置开启。

 - (void)viewDidLoad {
     [super viewDidLoad];
     NSThread *thread  = [[NSThread alloc] initWithTarget:self selector:@selector(threadEntry) object:nil];
    thread.name = @"TimerThread";
    [thread start];
    _timerThread = thread;
}

- (void)threadEntry {
    _timer = [NSTimer timerWithTimeInterval:5 target:self selector:@selector(update:) userInfo:nil repeats:YES];
    //子线程RunLoop手动配置开启
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    [runLoop addTimer:_timer forMode:NSRunLoopCommonModes];
    [runLoop run];
}

- (void)cancel {
    //NSTimer要在同一个线程中创建和关闭。因为创建的Timer的时候已经把Timer加入到该线程对应的RunLoop中,这个RunLoop设置了这个Timer为一个事件。因此要在同一个线程中才能cancel这个Timer。
    [self performSelector:@selector(invalidTimer) onThread:_timerThread withObject:nil waitUntilDone:YES];
}

- (void)invalidTimer {
    if (_timer) {
        [_timer invalidate];
        _timer = nil;
    }
}

5.更为实时的timer,GCDtimer

uint64_t interval = 1 * NSEC_PER_SEC;
//创建一个专门执行timer回调的GCD队列
dispatch_queue_t queue = dispatch_queue_create("timerQueue", 0);
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
//使用dispatch_source_set_timer函数设置timer参数
dispatch_source_set_timer(_timer, dispatch_time(DISPATCH_TIME_NOW, 0), interval, 0);
//设置回调
dispatch_source_set_event_handler(_timer, ^(){
    NSLog(@"Timer %@", [NSThread currentThread]);
});
dispatch_resume(_timer);//dispatch_source默认是Suspended状态,通过dispatch_resume函数开始它

其中的dispatch_source_set_timer的最后一个参数,是最后一个参数(leeway),它告诉系统我们需要计时器触发的精准程度。所有的计时器都不会保证100%精准,这个参数用来告诉系统你希望系统保证精准的努力程度。如果你希望一个计时器每5秒触发一次,并且越准越好,那么你传递0为参数。另外,如果是一个周期性任务,比如检查email,那么你会希望每10分钟检查一次,但是不用那么精准。所以你可以传入60,告诉系统60秒的误差是可接受的。它的意义在于降低资源消耗。

6.GCD方式的一次性timer

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{        NSLog(@"dispatch_after enter timer");
   });

7.递归调用实现的timer

 - (void)dispatchAfterTimer {
    __weak typeof (self) weakSelf = self;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"dispatch_after enter timer,thread = %@", [NSThread currentThread]);
        [weakSelf dispatchAfterTimer];
    });
}

8.NSTimer都会对它的target强引用,我们需要小心对待这个target的生命周期问题。

比如这样的代码:

self.detectTimer = [NSTimer scheduledTimerWithTimeInterval:0.5
target:self
selector:@selector(someMethod)
userInfo:nil
repeats:YES];

- (void)dealloc {
    [self.detectTimer invalidate];
    self.detectTimer = nil;
}

由于timer对target:self的retain,造成self退出后根本没有调用dealloc,以至于viewController不会被释放导致内存泄露。

那么怎么解决这一问题呢?

方法一:把计时器的释放操作放到viewWillDisappear中来管理。

代码大概长这样:

- (void)viewWillDisappear:(BOOL)animated {
   [super viewWillDisappear:animated];
   [self.detectTimer invalidate];
   self.detectTimer = nil;
}

当页面需要频繁地切入切出时这个方法缺点明显。

方法二:使用MSWeaker

MSWeaker实现了一个利用GCD的弱引用的timer。原理是利用一个新的对象,在这个对象中创建了一个队列,然后在这个队列中创建gcd timer.注意其中用到了dispatch_set_target_queue(self.privateSerialQueue, dispatchQueue); 这个是将privateSerialQueue队列的执行操作放到队列dispatchQueue 中去.参考代码:

NSString *privateQueueName = [NSString stringWithFormat:@"com.mindsnacks.msweaktimer.%p", self];
        self.privateSerialQueue = dispatch_queue_create([privateQueueName cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_SERIAL);
        dispatch_set_target_queue(self.privateSerialQueue, dispatchQueue);

self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,
                                            0,
                                            0,
                                            self.privateSerialQueue);

- (void)resetTimerProperties
{
    int64_t intervalInNanoseconds = (int64_t)(self.timeInterval * NSEC_PER_SEC);
    int64_t toleranceInNanoseconds = (int64_t)(self.tolerance * NSEC_PER_SEC);

    dispatch_source_set_timer(self.timer,
                              dispatch_time(DISPATCH_TIME_NOW, intervalInNanoseconds),
                              (uint64_t)intervalInNanoseconds,
                              toleranceInNanoseconds
                              );
}

- (void)schedule
{
    [self resetTimerProperties];

    __weak MSWeakTimer *weakSelf = self;

    dispatch_source_set_event_handler(self.timer, ^{
        [weakSelf timerFired];
    });

    dispatch_resume(self.timer);
}

方法三:考虑引入一个对象,在这个对象中弱引用self,然后将这个对象传递给timer的构建方法 这里可以参考YYWeakProxy建立这个对象.

@interface YYWeakProxy : NSProxy
@property (nonatomic, weak, readonly) id target;
- (instancetype)initWithTarget:(id)target;
+ (instancetype)proxyWithTarget:(id)target;
@end
@implementation YYWeakProxy
- (instancetype)initWithTarget:(id)target {
_target = target;
return self;
}
+ (instancetype)proxyWithTarget:(id)target {
return [[YYWeakProxy alloc] initWithTarget:target];
}
//当不能识别方法时候,就会调用这个方法,在这个方法中,我们可以将不能识别的传递给其它对象处理
//由于这里对所有的不能处理的都传递给_target了,所以methodSignatureForSelector和forwardInvocation不可能被执行的,所以不用再重载了吧
//其实还是需要重载methodSignatureForSelector和forwardInvocation的,为什么呢?因为_target是弱引用的,所以当_target可能释放了,当它被释放了的情况下,那么forwardingTargetForSelector就是返回nil了.然后methodSignatureForSelector和forwardInvocation没实现的话,就直接crash了!!!
//这也是为什么这两个方法中随便写的!!!
- (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];
}
- (BOOL)isEqual:(id)object {
return [_target isEqual:object];
}
- (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

使用方式:

self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:[YYWeakProxy proxyWithTarget:self ] selector:@selector(animationTimerDidFired:)  userInfo:nil repeats:YES];

方法四:block方式来解决循环引用

@interface NSTimer (XXBlocksSupport)
+ (NSTimer *)xx_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                     block:(void(^)())block
                                   repeats:(BOOL)repeats;
@end
@implementation NSTimer (XXBlocksSupport)
+ (NSTimer *)xx_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                     block:(void(^)())block
                                   repeats:(BOOL)repeats
{
return [self scheduledTimerWithTimeInterval:interval
                                      target:self
                                    selector:@selector(xx_blockInvoke:)
                                    userInfo:[block copy]
                                     repeats:repeats];
}
+ (void)xx_blockInvoke:(NSTimer *)timer {
void (^block)() = timer.userinfo;
if(block) {
    block();
}
}
@end

注意以上NSTimer的target是NSTimer类对象,类对象本身是个单例,此处虽然也是循环引用,但是由于类对象不需要回收,所以没有问题.但是这种方式要注意block的间接循环引用,当然了,解决block的间接循环引用很简单,定义一个weak变量,在block中使用weak变量即可

参考:

用Block解决NSTimer循环引用

weak NSTimer

弱引用NSTimer对象

NSTimer和实现弱引用的timer的方式