NSTimer

2018-03-12  本文已影响11人  Scott丶Wang

一、简单介绍

1.NSTimer是一个不准时的计时器,他会有一个tolerance,还有这与当前NSTimer所处的线程有很大的关系,如果NSTimer当前所处的线程正在进行耗时任务,NSTimer本次执行会等到这个耗时任务完毕之后才会继续执行。

2.这期间有可能会错过很多次NSTimer的循环周期,但是NSTimer并不会将前面错过的执行次数在后面都执行一遍,而是继续执行后面的循环,也就是在一个循环周期内只会执行一次循环。

3.在子线程中,使用NSTimer的时候一定要记住,需要将NSTimer加到当前线程的RunLoop中并开启RunLoop循环,否则NStimer将不会调起。ps:可以通过[NSRunLoop currentRunLoop]获取当前线程的RunLoop,懒加载来获取,有即用,无则创建。NSTimer本质上是在当前线程的Run Loop中循环执行的,因此Timer的回调方法不是在另一个线程的,如何在子线程中使用呢?

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    NSLog(@"主线程 %@", [NSThread currentThread]);
    
    //创建并执行新的线程
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(newThread) object:nil];
    [thread start];
}

- (void)newThread
{
    @autoreleasepool
    {
        //在当前Run Loop中添加timer,模式是默认的NSDefaultRunLoopMode
        [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(timer_callback) userInfo:nil repeats:YES];
        //开始执行新线程的Run Loop
        [[NSRunLoop currentRunLoop] run];
    }
}

//timer的回调方法
- (void)timer_callback
{
    NSLog(@"Timer %@", [NSThread currentThread]);
}
  1. 当使用NSTimer的scheduledTimerWithTimeInterval方法时。事实上此时Timer会被加入到当前线程的RunLoop中,且模式是默认的NSDefaultRunLoopMode。而如果当前线程就是主线程,也就是UI线程时,某些UI事件,比如UIScrollView的拖动操作,会将Run Loop切换成NSEventTrackingRunLoopMode模式,在这个过程中,默认的NSDefaultRunLoopMode模式中注册的事件是不会被执行的。也就是说,此时使用scheduledTimerWithTimeInterval添加到RunLoop中的Timer就不会执行。
- (void)viewDidLoad
{
    [super viewDidLoad];
    
    NSLog(@"线程: %@", [NSThread currentThread]);
    // 创建Timer
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(timer_callback) userInfo:nil repeats:YES];
    //使用NSRunLoopCommonModes模式,把timer加入到当前RunLoop中。
    // NSRunLoopCommonModes,这个模式等效于NSDefaultRunLoopMode和NSEventTrackingRunLoopMode的结合
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}

5.GCD中的Timer应该是最灵活的,而且是多线程的。GCD中的Timer是靠Dispatch Source来实现的

- (void)viewDidLoad {
    NSLog(@"线程 %@", [NSThread currentThread]);
    // 间隔还是2秒
    uint64_t interval = 2 * NSEC_PER_SEC;
    // 创建串行队列
    dispatch_queue_t queue = dispatch_queue_create("my queue", 0);
    // 创建Timer
    _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_source默认是Suspended状态,通过dispatch_resume函数开始它
    dispatch_resume(_timer);
}

Dispatch Source使用最多的就是用来实现定时器,source创建后默认是暂停状态,需要手动调用dispatch_resume启动定时器。dispatch_after只是封装调用了dispatch source定时器,然后在回调函数中执行定义的block。
Dispatch Source定时器使用时也有一些需要注意的地方,不然很可能会引起crash:
1、循环引用:因为dispatch_source_set_event_handler回调是个block,在添加到source的链表上时会执行copy并被source强引用,如果block里持有了self,self又持有了source的话,就会引起循环引用。正确的方法是使用weak+strong或者提前调用dispatch_source_cancel取消timer。
2、dispatch_resume和dispatch_suspend调用次数需要平衡,如果重复调用dispatch_resume则会崩溃,因为重复调用会让dispatch_resume代码里if分支不成立,从而执行了DISPATCH_CLIENT_CRASH(“Over-resume of an object”)导致崩溃。
3、source在suspend状态下,如果直接设置source = nil或者重新创建source都会造成crash。正确的方式是在resume状态下调用dispatch_source_cancel(source)后再重新创建。

参考:
NSTimer 避坑指南
iOS刨根问底-深入理解RunLoop
深入理解RunLoop

上一篇下一篇

猜你喜欢

热点阅读