NSTimer

作者: Scott丶Wang | 来源:发表于2018-03-12 16:44 被阅读11次

    一、简单介绍

    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

    相关文章

      网友评论

          本文标题:NSTimer

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