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

百家 作者:iOS开发 2018-08-11 12:14:28

点击上方“iOS开发”,选择“置顶公众号”

关键时刻,第一时间送达!


作者:没了蜡笔de小新

链接:https://www.jianshu.com/p/202365e78ecc

iOS开发整理发布,转载请联系作者获得授权

话不多说,进入正题

定时器相信大家肯定不会陌生,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实现的定时器,大家可以拿去直接使用,GitHub:

https://github.com/aiOSFuture/YQ_SafeGCDTimer

【点击成为源码大神】

关注公众号:拾黑(shiheibook)了解更多

[广告]赞助链接:

四季很好,只要有你,文娱排行榜:https://www.yaopaiming.com/
让资讯触达的更精准有趣:https://www.0xu.cn/

公众号 关注网络尖刀微信公众号
随时掌握互联网精彩
赞助链接