iOS面试总结——精确定时器

作者: 没了蜡笔de小新 | 来源:发表于2018-03-28 16:07 被阅读32次

    金三银四,祝大家能找到满意的工作~

    话不多说,进入正题

    定时器相信大家肯定不会陌生,iOS中常用的定时器有三种,分别是NSTimer,CADisplayLink和GCD。

    NSTimer

    两种方式创建

        NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(test) userInfo:nil repeats:YES];
        // 停止定时器
        [timer invalidate]; 
         timer == nil 
    

    创建方式2

        NSTimer *timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(test) userInfo:nil repeats:YES];
        // 将定时器添加到runloop中,否则定时器不会启动
        [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
        // 停止定时器
        [timer invalidate];
        timer == nil 
    

    方式1会自动将创建的定时器以默认方式添加到当前线程runloop中,而无需手动添加。但是在此种模式下,当滚动屏幕时runloop会进入另外一种模式,定时器会暂停,为了解决这种问题,可以像方式2那样把定时器添加到NSRunLoopCommonModes模式下。

    方式1和方式2在设置后都会在间隔设定的时间(本例中设置为2s)后执行test方法,如果需要立即执行可以使用下面的代码。
    [time fire];

    不过,NSTimer相对来说是不精确的,参考苹果官方文档介绍timer
    咳咳,鸟语,笔者大致翻译了一下重点内容😜

    1.NSTimer被添加到特定mode的runloop中;
    2.该mode型的runloop正在运行;
    3.到达激发时间。因为一个run loop需要管理大量的输入源,为了提NSTimer的效率,时间间隔限制为50-100毫秒比较合理。如果一个NSTimer的激发时间
    出现在一个耗时的方法中,或者当前run loop的mode没有监测该NSTimer,那么定时器就不会被激发,直到下一次run loop检测到该NSTimer时才会激发。
    因此,NSTimer的实际激发时间很有可能会比规划时间延后一段时间。
    

    哎,再来这样解释一下

    1.NSTimer加在main runloop中,模式是NSDefaultRunLoopMode,main负责所有主线程事件,例如UI界面的操作,复杂的运算,这样在同一个runloop中timer就会产生阻塞。
    2.模式的改变。主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。
    当你创建一个 Timer 并加到 DefaultMode 时,Timer 会得到重复回调,但此时滑动一个ScrollView时,RunLoop 会将 mode 切换为 TrackingRunLoopMode,这时 Timer 就不会被回调,并且也不会影响到滑动操作。所以就会影响到NSTimer不准的情况。
    PS:DefaultMode 是 App 平时所处的状态,rackingRunLoopMode 是追踪 ScrollView 滑动时的状态。
    

    解决办法

    方案1.在主线程中进行NSTimer操作,但是将NSTimer实例加到main runloop的特定mode(模式)中。避免被复杂运算操作或者UI界面刷新所干扰。
    NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(test) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    
    方案2.在子线程中进行NSTimer的操作,再在主线程中修改UI界面显示操作结果;
    - (void)timer2 {
        NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(newThread) object:nil];
        [thread start];
    }
    - (void)newThread {
        @autoreleasepool {
        [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(showTime) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] run];
          }
    }
    
    CADisplayLink
        CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(test:)];
        // 将创建的displaylink添加到runloop中,否则定时器不会执行
        [displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
    
        // 停止定时器
        [displayLink invalidate];
        displayLink = nil;  
    当把CADisplayLink对象add到runloop中后,selector就能被周期性调用,类似于重复的NSTimer被启动了;执行invalidate操作时,CADisplayLink对象就会从runloop中移除,selector调用也随即停止,类似于NSTimer的invalidate方法 
    
    需要注意的地方

    调用时机

    CADisplayLink是一个和屏幕刷新率同步的定时器类。CADisplayLink以特定模式注册到runloop后,每当屏幕显示内容刷新结束的时候,runloop就会向CADisplayLink指定的target发送一次指定的selector消息,CADisplayLink类对应的selector就会被调用一次,所以可以使用CADisplayLink做一些和屏幕操作相关的操作。
    

    重要属性

    1.frameInterval
      NSInteger类型的值,用来设置间隔多少帧调用一次selector方法,默认值是1,即每帧都调用一次。
    2.duration
      readOnly的CFTimeInterval值,表示两次屏幕刷新之间的时间间隔。需要注意的是,该属性在target的selector被首次调用以后才会被赋值。selector的调用间隔时间计算方式是:调用间隔时间 = duration × frameInterval。
    3.timestamp
    只读的CFTimeInterval值,表示屏幕显示的上一帧的时间戳,这个属性通常被target用来计算下一帧中应该显示的内容。
    
    CADisplayLink注意点总结

    注意点:
    iOS并不能保证能以每秒60次的频率调用回调方法,这取决于:
    1、CPU的空闲程度
    如果CPU忙于其它计算,就没法保证以60HZ执行屏幕的绘制动作,导致跳过若干次调用回调方法的机会,跳过次数取决CPU的忙碌程度。
    2、执行回调方法所用的时间
    如果执行回调时间大于重绘每帧的间隔时间,就会导致跳过若干次回调调用机会,这取决于执行时间长短。

    总结:
    从原理上不难看出,CADisplayLink使用场合相对专一,适合做界面的不停重绘,比如视频播放的时候需要不停地获取下一帧用于界面渲染。

    GCD定时器

    一次性定时
     dispatch_time_t timer = dispatch_time(DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC);
    
     dispatch_after(timer, dispatch_get_main_queue(), ^(void){
    
            NSLog(@"GCD-----%@",[NSThread currentThread]);
    
        }); 
    
    重复执行的定时器
    {
        //0.创建队列
        dispatch_queue_t queue = dispatch_get_main_queue();
        //1.创建GCD中的定时器
        /*
         第一个参数:创建source的类型 DISPATCH_SOURCE_TYPE_TIMER:定时器
         第二个参数:0
         第三个参数:0
         第四个参数:队列
         */
        dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    
        //2.设置时间等
        /*
         第一个参数:定时器对象
         第二个参数:DISPATCH_TIME_NOW 表示从现在开始计时
         第三个参数:间隔时间 GCD里面的时间最小单位为 纳秒
         第四个参数:精准度(表示允许的误差,0表示绝对精准)
         */
        dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
    
        //3.要调用的任务
        dispatch_source_set_event_handler(timer, ^{
            NSLog(@"GCD-----%@",[NSThread currentThread]);
        });
    
        //4.开始执行
        dispatch_resume(timer);
    
        //
        self.timer = timer;
    } 
    

    注意的地方: 此处注意一定要强引用定时器 ,否则定时器执行到 } 后将会被释放,无定时效果。GCD定时器时间非常精准,最小的定时时间可以达到1纳秒,所以用在非常精确的定时场合。

    补充一下

    NSObject的方法也有类似功能的方法

    - (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;
    

    敲黑板总结

    1.NSTimer和performSelector必须保证有一个活跃的runloop。

    performSelector和scheduledTimerWithTimeInterval方法都是基于runloop的。我们知道,当一个应用启动时,系统会开启一个主线程,并且把主线程的runloop激活,也就是run起来,并且主线程的runloop是不会停止的。所以,当这两个方法在主线程可以被正常调用。但情况往往不是这样的。实际编码中,我们更多的逻辑是放在子线程中执行的。而子线程的runloop是默认关闭的。这时如果不手动激活runloop,performSelector和scheduledTimerWithTimeInterval的调用将是无效的。
    

    2.NSTimer的创建与撤销必须在同一个线程操作、performSelector的创建与撤销必须在同一个线程操作。

    3.内存管理有潜在泄露的风险

    scheduledTimerWithTimeInterval方法将target设为A对象时,A对象会被这个timer所持有,也就是会被retain一次,timer会被当前的runloop所持有。performSelector:withObject:afterDelay:方法实际上是在当前线程的runloop里帮你创建的一个timer去执行任务,所以和scheduledTimerWithTimeInterval方法一样会retain其调用对象。但是,我们往往不希望因为这些延迟操作而影响对象的生命周期,更甚至是,导致对象无法释放。
    

    4.CADisplayLink

    iOS设备的屏幕刷新频率(FPS)是60Hz,因此CADisplayLink的selector默认调用周期是每秒60次,这个周期可以通过frameInterval属性设置,CADisplayLink的selector每秒调用次数=60/frameInterval。比如当frameInterval设为2,每秒调用就变成30次。因此,CADisplayLink周期的设置方式略显不便。
    
    NSTimer的selector调用周期可以在初始化时直接设定,相对就灵活的多。
    iOS设备的屏幕刷新频率是固定的,CADisplayLink在正常情况下会在每次刷新结束都被调用,精确度相当高。
    
    NSTimer的精确度就显得低了点,比如NSTimer的触发时间到的时候,runloop如果在忙于别的调用,触发时间就会推迟到下一个runloop周期。更有甚者,在OS X v10.9以后为了尽量避免在NSTimer触发时间到了而去中断当前处理的任务,NSTimer新增了tolerance属性,让用户可以设置可以容忍的触发的时间范围。
    

    5.GCD

    若使用dispatch_after,系统会帮我们处理线程级的逻辑,这样也我们更易于享受系统对线程所做的优化。除此之外,我们不用关心runloop的问题。并且调用的对象也不会被强行持有,这样上述的内存问题也不复存在。当然,需要注意block会持有其传入的对象,但这可以通过weakself解决。所以在这种延迟操作方案中,使用dispatch_after更佳。
    
    但是呢,dispatch_after有个致命的弱点:dispatch_after一旦执行后,就不能撤销了。而performSelector可以使用cancelPreviousPerformRequestsWithTarget方法撤销,NSTimer也可以调用invalidate进行撤销。(注意:撤销任务与创建timer任务必须在同一个线程,即同一个runloop)所以我们还是得用NSTimer或者performSelector吗?
    
    NO,其实GCD也有timer的功能。
    

    笔者自己封装了利用GCD实现的定时器,大家可以拿去直接使用,传送门

    end...

    相关文章

      网友评论

        本文标题:iOS面试总结——精确定时器

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