0%

iOS定时器杂记

NSTimer

NSTimer是使用最多的一种定时器,虽然但是NSTimer还是有很多的注意事项:

  1. 不注意使用的话有循环引用的隐患。因为NSTimer会强引用target,如果target再强引用NSTimer那么就会发生循环引用。比如:

    1
    2
    3
    4
    5
    - (void)test_normal_timer1 {
    self.normalStrongTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(doSome) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.normalStrongTimer forMode:NSRunLoopCommonModes];
    [self.normalStrongTimer fire];
    }

    上面的代码就会发生循环引用。此时即使页面返回,但是因为循环引用,ViewController的dealloc将不会被执行,因此invalidate不能放在dealloc里,只能在其他时机比如viewDidDisappear里调用。所以一般使用weak来弱引用定时器,这样就可以在dealloc里调用invalidate。

  2. 需要在合适的地方invalid定时器,否则定时器会一直强引用target从而延长target的生命周期。

    在开发中很容易忘记invalid定时器,一旦忘记invalid定时器,定时器就会延长target的生命周期,比如页面返回了但实际还没有被销毁,从而产生一些诡异问题。这也是使用NSTimer必须注意的地方。

  3. 使用时必须保证有一个活跃的runloop,并且需要指定mode。因此在子线程中使用不是很方便。

  4. 精度可能不够。

  5. 网上的一个说法:创建和撤销必须在同一个线程上,在多线程环境下使用不便。(这一条存疑,经过验证在子线程创建一个定时器,在另一个子线程invalidate并没有发现有什么问题)

  6. iOS10开始支持block使用,同样在使用block时一定要注意循环引用。

为了从根本上避免上述问题,一个弱引用target的、能够在自身销毁时自动invalid的定时器想必是极好的。解决办法就是封装GCD定时器。

GCD定时器

基本使用:

1.创建定时器源

1
2
3
4
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,
0,
0,
self.privateSerialQueue);

2.设置定时器

1
2
3
4
5
6
7
8
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
);

3.设置事件回调

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

4.启动定时器

1
dispatch_resume(self.timer);

5.撤销定时器

1
dispatch_source_cancel(timer);

问答

1. 在子线程中使用定时器

最佳实践就是使用GCD定时器,因为GCD定时器不需要添加在runloop中,并且本身也支持指定事件回调的派发队列。

如果你非得使用NSTimer+NSThread,那么必须注意runloop的操作。runloop有下面三种开启方式:

1
2
3
- (void)run; 
- (void)runUntilDate:(NSDate *)limitDate;
- (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;

run:启动前如果没有添加任何源那么runloop一启动就会马上退出,方法也会马上返回(三种方式都遵守)。而如果有添加源则runloop将一直运行,不会退出。即使手动移除了源,也不会使得runloop会退出。

runUntilDate:和run差不多,只是多了一个截止日期。截止日期到了,则退出runloop,方法返回。

runMode:运行一次runloop后会退出。比如runloop期间处理了一个输入源事件,处理后会退出runloop,方法也会返回。注意处理定时器事件不会使得runloop退出,但如果调用invalidate则会退出。所以如果你想让runloop能够退出的话最好使用该方法。

前面两种方法即使调用 [self.timer invalidate]; ,runloop也不会退出(少量情况下也会退出),这样线程就会一直存在,

1
2
3
//线程也是会强引用target的,所以这里线程和self循环引用了。
self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(createTimer1) object:nil];
[self.thread start];

线程一直存在就会一直持有self,导致页面即使返回了也不会销毁。而如果在当前线程中强行调用 + (void)exit; 退出线程,会因为一些资源没有正常释放而造成内存泄露,经过验证self依然是没有被释放的。所以只能使用第三种启动方式。

因此,在子线程中使用定时器,最佳实践就是使用GCD定时器。

2.为什么MSWeakTimer的invalidate方法里必须是异步取消,同步取消不行吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)invalidate
{
// We check with an atomic operation if it has already been invalidated. Ideally we would synchronize this on the private queue,
// but since we can't know the context from which this method will be called, dispatch_sync might cause a deadlock.
if (!OSAtomicTestAndSetBarrier(7, &_timerFlags.timerIsInvalidated))
{
dispatch_source_t timer = self.timer;
dispatch_async(self.privateSerialQueue, ^{
dispatch_source_cancel(timer);
ms_release_gcd_object(timer);
});
}
}

正如注释写的,如果我们在回调的方法里调用invalidate方法,那么将导致队列死锁。所以这里必须采用异步invalidate。

参考

Dispatch Source学习

更可靠和高精度的 iOS 定时器

dispatch source理解

NSRunLoop的退出方式 挺不错的

关于 performSelector 的一些小探讨

觉得文章有帮助可以打赏一下哦!