iOS 深入理解NSTimer与RunLoop及内存泄漏问题

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

demo 地址 https://github.com/yinbowang/YBTimerDemo

推荐阅读更多精彩内容