美文网首页
NSTimer那些事

NSTimer那些事

作者: Sunli_ | 来源:发表于2017-03-17 18:20 被阅读448次

定时器:

  • 它会被添加到Runloop,否则不会运行,当然添加的Runloop不存在也不会运行
  • 还要指定添加到的Runloop的哪个模式,而且还可以指定添加到Runloop的多个模式,模式不对也是不会运行的
  • Runloop会对timer有强引用,timer会对目标对象进行强引用(是否隐约的感觉到坑了。。。)
  • timer的执行时间并不准确,系统繁忙的话,还会被跳过去
  • invalidate调用后,timer停止运行后,就一定能从Runloop中消除吗?

定时器的一般用法

- (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个实例初始化方法。

  • 有三个方法直接将timer添加到了当前Runloop default mode,而不需要我们自己操作,当然这样的代价是Runloop只能是当前Runloop,模式是Default mode
+ (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;
  • 下面五种创建,不会自动添加到Runloop,还需调用addTimer:forMode:
+ (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;

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

  • ti(interval):定时器触发间隔时间,单位为秒,可以是小数。如果值小于等于0.0的话,系统会默认赋值0.1毫秒
  • invocation:这种形式用的比较少,大部分都是block和aSelector的形式
  • yesOrNo(rep):是否重复,如果是YES则重复触发,直到调用invalidate方法;如果是NO,则只触发一次就自动调用invalidate方法
  • aTarget(t):发送消息的目标,timer会强引用aTarget,直到调用invalidate方法
  • aSelector(s):将要发送给aTarget的消息,如果带有参数则应:- (void)timerFireMethod:(NSTimer *)timer声明
  • userInfo(ui):传递的用户信息。使用的话,首先aSelector须带有参数的声明,然后可以通过[timer userInfo]获取,也可以为nil,那么[timer userInfo]就为空
  • date:触发的时间,一般情况下我们都写[NSDate date],这样的话定时器会立马触发一次,并且以此时间为基准。如果没有此参数的方法,则都是以当前时间为基准,第一次触发时间是当前时间加上时间间隔ti
  • block:timer触发的时候会执行这个操作,带有一个参数,无返回值

添加到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根线的由来
  • L1:nav push 控制器的时候会强引用,即在push的时候产生;
  • L2:是在代码3的位置产生
  • L3:是在代码1的位置产生,至此L2与L3已经产生了循环引用,虽然timer还没有添加到Runloop
  • L4:是在代码2的位置产生
    我们常说的NSTimer会产生循环引用 其实就是由于timer会对self进行强引用造成的。

打破循环引用

解决循环引用,首先想到的方法就是让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。

  • 外部调用方法触发invalidate
- (void)stopTimer {
    [self.timer invalidate];
}
  • 如果在是在view中的定时器, 可以重写removeFromSuperview
- (void)removeFromSuperview {
    [super removeFromSuperview];
    [self.timer invalidate];
}
  • 将timer的target设为一个中间类
@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

控制台打印:


  • NSTimer写一个分类
@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方法来做的。那么这个方法到底有什么用呢?

  • 将timer从Runloop中移除
  • 释放他自身持有的资源 比如target userInfo block

注意:

  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

相关文章

  • NSTimer那些事

    定时器: 它会被添加到Runloop,否则不会运行,当然添加的Runloop不存在也不会运行 还要指定添加到的Ru...

  • NSTimer/CADisplayLink那些事

    NSTimer计时器对象,就是一个能在从现在开始的后面的某一个时刻或者周期性的执行我们指定的方法的对象。CADis...

  • iOS中的计时器

    一、NSTimer 创建方法 1 NSTimer *timer = [NSTimer scheduledTimer...

  • NSTimer

    创建NSTimer 创建NSTimer的常用方法是: + (NSTimer *)scheduledTimerWit...

  • iOS 延时

    1 NSTimer //1秒后执行 NSTimer *timer = [NSTimer timerWithTim...

  • iOS 获取网络流量

    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:...

  • 时间倒计时

    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:...

  • 1.4NSTimer

    NSTimer NSTimer.png

  • NSRunloop跟NSTimer

    NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval...

  • iOS-NSTimer 使用

    1.NSTimer的创建方法 2. NStimer的开启 3. NStimer的停止 4. NStimer的其他属...

网友评论

      本文标题:NSTimer那些事

      本文链接:https://www.haomeiwen.com/subject/hwtenttx.html