NSTimer那些事

2017-03-17  本文已影响448人  Sunli_

定时器:

定时器的一般用法

- (void)viewDidLoad {
    NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1 target:self selector:@selector(timerFire) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    self.timer = timer;
}

- (void)timerFire {
    NSLog(@"timer fire");
}

上面的代码就是我们使用定时器最常用的方式,可以总结为2个步骤:创建,添加到runloop

系统提供了8个创建方法,6个类创建方法,2个实例初始化方法。

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;

- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(id)ui repeats:(BOOL)rep;

- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;

对上面所有方法参数做个说明:

添加到runloop,参数timer是不能为空的,否则抛出异常
- (void)addTimer:(NSTimer *)timer forMode:(NSRunLoopMode)mode
另外,系统提供了一个- (void)fire方法,调用它可以触发一次:

对于重复定时器,它不会影响正常的定时触发
对于非重复定时器,触发后就调用了invalidate方法,既使正常的还没有触发

NSTimer添加到NSRunLoop

timer必须添加到Runloop才有效,很明显要保证两件事情,一是Runloop存在(运行),另一个才是添加。确保这两个前提后,还有Runloop模式的问题。

一个timer可以被添加到Runloop的多个模式,比如在主线程中runloop一般处于NSDefaultRunLoopMode,而当滑动屏幕的时候,比如UIScrollView或者它的子类UITableView、UICollectionView等滑动时Runloop处于UITrackingRunLoopMode模式下,因此如果你想让timer在滑动的时候也能够触发,就可以分别添加到这两个模式下。或者直接用NSRunLoopCommonModes一个模式集,包含了上面的两种模式。

但是一个timer只能添加到一个Runloop(Runloop与线程一一对应关系,也就是说一个timer只能添加到一个线程)。如果你非要添加到多个Runloop,则只有一个有效

关于强引用的问题

一般我们使用NSTimer如下图所示

- (void)viewDidLoad {
    // 代码1
    NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1 target:self selector:@selector(timerFire) userInfo:nil repeats:YES];
    // 代码2  上文中提到有些初始化方法会自动添加Runloop 这里是主动调用 效果都一样
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; 
    // 代码3
    self.timer = timer;
}

- (void)timerFire {
    NSLog(@"timer fire");
}

假设代码中self(viewController)UINavgationController管理 并且timer与self之间为强引用

timer和vc(self)Runloop之间的关系图
如图所示 分析一下4根线的由来

打破循环引用

解决循环引用,首先想到的方法就是让self对timer为弱引用weak或者time对target如self替换为weakSelf 然而这真的有用吗?

例:

@interface TimerViewController ()

@property (nonatomic, weak) NSTimer *timer;

@end

@implementation TimerViewController

- (void)dealloc {
    
    [self.timer invalidate];
     NSLog(@"dealloc");
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSTimer *timer = [NSTimer timerWithTimeInterval:1.0f target:self selector:@selector(count) userInfo:nil repeats:YES];

    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    
    self.timer = timer;
}

- (void)count {
     NSLog(@"count");
}
@end

从上一个vc push进来之后, 定时器开始启动 控制台打印count, 然后pop回去, TimerViewController并没有走dealloc方法 控制台依然打印count。

将self改成weakSelf效果依然一样

- (void)viewDidLoad {
    [super viewDidLoad];
    
    
    __weak typeof(self) weakSelf = self;
    NSTimer *timer = [NSTimer timerWithTimeInterval:1.0f target:weakSelf selector:@selector(count) userInfo:nil repeats:YES];

    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    
    self.timer = timer;
}

分析:

设置timer为weak

我们想通过self对timer的弱引用, 在self中的dealloc方法中让timer失效来达到相互释放的目的。但是, timer内部本身对于self有一个强引用。并且timer如果不调用invalidate方法,就会一直存在,所以就导致了self根本释放不了, 进而我们想通过在dealloc中设置timer失效来释放timer的方法也就行不通了。

设置self为weakSelf

__weak修饰self为weakSelf与普通的self的区别就在于, 这时候weakSelf作为一个参数传入block或者别的实例变量, block或实例变量都不会持有他, 也就是self的引用计数不会加1 在一般情况下 这时候就可以打破循环引用, 但是timer的内部机制决定了它必须通过设置invalidate来停止计时并释放, 在此之前, timer会强引用target, 所以也就不存在timer释放weakSelf, 即循环引用还是存在。

(别人对于weakstrong的解释 我觉得挺好的 多读几遍会有更多了解)

1.(weakstrong)不同的是:当一个对象不再有strong类型的指针指向它的时候,它就会被释放,即使该对象还有_weak类型的指针指向它;
2.一旦最后一个指向该对象的strong类型的指针离开,这个对象将被释放,如果这个时候还有weak指针指向该对象,则会清除掉所有剩余的weak指针

解决办法

总结: 要想解决循环引用的问题, 关键在于让timer失效即调用[timer invalidate]方法而不是各种weak。

- (void)stopTimer {
    [self.timer invalidate];
}
- (void)removeFromSuperview {
    [super removeFromSuperview];
    [self.timer invalidate];
}
@interface BreakTimeLoop ()

// 这里必须用weak 不然释放不了
@property (nonatomic, weak) id owner;

@end


@implementation BreakTimeLoop

- (void)dealloc {
     NSLog(@"BreakTimeLoop dealloc");
}

- (instancetype)initWithOwner:(id)owner {
    if (self = [super init]) {
        self.owner = owner;
    }
    return self;
}

- (void)doSomething:(NSTimer *)timer {
    // 需要参数可以通过 timer.userInfo 获取
    [self.owner performSelector:@selector(count)];
}

在vc中

@interface TimerViewController ()

/** timer在这种情况下也可用strong **/
@property (nonatomic, strong) NSTimer *timer;
/** 中间类 这里用strong和weak无所谓 主要是breaker中对owner的引用为weak就行 **/
@property (nonatomic, strong) BreakTimeLoop *breaker;

@end

@implementation TimerViewController

- (void)dealloc {
    
    [self.timer invalidate];
     NSLog(@"TimerViewController dealloc");
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.breaker = [[BreakTimeLoop alloc] initWithOwner:self];
    // 这里的doSomething: 如果在.h中没有声明会报警告 不过不影响实现
    self.timer = [NSTimer timerWithTimeInterval:1.0f target:self.breaker selector:@selector(doSomething:) userInfo:nil repeats:YES];

    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}

- (void)count {
     NSLog(@"count");
}
@end

控制台打印:


@interface NSTimer (SLBreakTimer)

+ (NSTimer *)sl_scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)())block;

@end

@implementation NSTimer (SLBreakTimer)

+ (NSTimer *)sl_scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)())block {
    
    // copy将block放入堆上防止提前释放
    return [NSTimer scheduledTimerWithTimeInterval:interval target:self selector:@selector(sl_block:) userInfo:[block copy] repeats:repeats];
}

+ (void)sl_block:(NSTimer *)timer {
    void (^block)() = timer.userInfo;
    if (block) {
        block();
    }
}
@end

在vc中

- (void)viewDidLoad {
    [super viewDidLoad];
    
    
    // 为weak防止循环引用
    __weak typeof(self) weakSelf = self;
    self.timer = [NSTimer sl_scheduledTimerWithTimeInterval:1.0f repeats:YES block:^{
        // 防止self提前释放
        __strong typeof(weakSelf) strongSelf = weakSelf;
        [strongSelf count];
    }];
    
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}

控制台打印为:

invalidate方法有啥用

在上文中, 我们所有做的一些都是围绕invalidate方法来做的。那么这个方法到底有什么用呢?

注意:

  1. 如果timer的引用为weak,在调用了invalidate之后,timer会被释放(ARC会置空)如果在这之后还想用timer必须重新创建,所以 我们添加timer进Runloop之前可以通过timer的isValid方法判断是否可用.
  2. 如果invalidate的调用不在添加Runloop的线程,那么timer虽然会释放他持有的资源,但是它本身不会被释放,他所在的Runloop也不会被释放,也会导致内存泄漏。
timer是否准时

不准时

  1. 线程在处理耗时的事情时会发生。
  2. 还有就是timer添加到的Runloop模式不是Runloop当前运行的模式,这种情况经常发生。

对于第一种情况我们不应该在timer上下功夫,而是应该避免这个耗时的工作。那么第二种情况,作为开发者这也是最应该去关注的地方,要留意,然后视情况而定是否将timer添加到Runloop多个模式。
虽然跳过去,但是,接下来的执行不会依据被延迟的时间加上间隔时间,而是根据之前的时间来执行。比如:
定时时间间隔为2秒,t1秒添加成功,那么会在t2、t4、t6、t8、t10秒注册好事件,并在这些时间触发。假设第3秒时,执行了一个超时操作耗费了5.5秒,则触发时间是:t2、t8.5、t10,第4和第6秒就被跳过去了,虽然在t8.5秒触发了一次,但是下一次触发时间是t10,而不是t10.5

比如上面说的t2、t4、t6、t8、t10,并不会在准确的时间触发,而是会延迟个很小的时间,原因也可以归结为2点:

  1. RunLoop为了节省资源,并不会在非常准确的时间点触发
  2. 线程有耗时操作,或者其它线程有耗时操作也会影响

iOS7以后,timer 有个属性叫做 Tolerance(时间宽容度,默认是0),标示了当时间点到后,容许有多少最大误差。
它只会在准确的触发时间到加上Tolerance时间内触发,而不会提前触发(是不是有点像我们的火车,只会晚点。。。)。另外可重复定时器的触发时间点不受Tolerance影响,即类似上面说的t8.5触发后,下一个点不会是t10.5,而是t10 + Tolerance

上一篇 下一篇

猜你喜欢

热点阅读