前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >深入学习iOS定时器

深入学习iOS定时器

作者头像
sweet说好的幸福
发布2020-12-23 10:12:32
9220
发布2020-12-23 10:12:32
举报
文章被收录于专栏:sweet_iOSsweet_iOS

定时器,用来延迟或重复执行某些方法,例如:网络定时刷新,UI间隔刷新,动画效果......iOS中的定时器大致分为这几类: <pre> NSObject GCD定时器 NSTimer CADisplayLink </pre>

RunLoop

在讲解定时器之前,先普及下RunLoop的基本知识。传送门: iOS - RunLoop 深入理解感谢ibireme整理了一份完整讲解,从 CFRunLoop 的源码入手,介绍 RunLoop 的概念以及底层实现原理。在此,总结性的介绍下。

RunLoop概念

一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线程能随时处理事件但并不退出,通常的代码逻辑是这样的:

代码语言:javascript
复制
function loop() {
    initialize();
    do {
        var message = get_next_message();
        process_message(message);
    } while (message != quit);
}

这种模型通常被称作 Event Loop。 Event Loop 在很多系统和框架里都有实现,比如 Node.js 的事件处理,比如 Windows 程序的消息循环,再比如 OSX/iOS 里的 RunLoop。实现这种模型的关键点在于:如何管理事件/消息,如何让线程在没有处理消息时休眠以避免资源占用、在有消息到来时立刻被唤醒。所以,RunLoop 实际上就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行上面 Event Loop 的逻辑。线程执行了这个函数后,就会一直处于这个函数内部 "接受消息->等待->处理" 的循环中,直到这个循环结束(比如传入 quit 的消息),函数返回。 苹果不允许直接创建 RunLoop,需要类似像主线程调用一样调用,即[NSRunLoop mainRunLoop],在 CoreFoundation 里面关于 RunLoop 有5个类:

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef

其中CFRunLoopTimerRef 是基于时间的触发器,其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。后面要讲的NSTimer 其实就是 CFRunLoopTimerRef。

NSObject

iOS框架图 在object-c中,绝大部分类的基类都是NSObject,使用NSObject延迟执行也被用于网络定时刷新,配套使用代码cancelPreviousPerformRequestsWithTarget与performSelector,相对而言比较简洁。当然NSObject与RunLoop之间的联系远不只于此,例如事件响应和手势识别,就不做展开。

代码语言:javascript
复制
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget selector:(SEL)aSelector object:(nullable id)anArgument;

当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。 当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。 当调用 cancelPreviousPerformRequestsWithTarget时,实际上就是讲Timer 从RunLoop中移除。

GCD

GCD定时器其实是一种特殊的分派源,它是基于分派队列的,而NSTimer是基于运行循环的,所以,尤其是在多线程中,GCD定时器要比NSTimer好用的多。另外,GCD定时器使用dispatch_block_t,而不是方法选择器,也就是说GCD实现的定时器是不受RunLoop约束。 实际上 RunLoop 底层也会用到 GCD 的东西,在<pre>CFRrunLoop.c</pre>中我们能发现引用了<pre>#include <dispatch/dispatch.h></pre>,例如RunLoop 的超时时间就是使用 GCD 中的 dispatch_source_t来实现的,摘自 __CFRunLoopRun中的源码:

代码语言:javascript
复制
 dispatch_source_t timeout_timer = NULL;
    struct __timeout_context *timeout_context = (struct __timeout_context *)malloc(sizeof(*timeout_context));
    if (seconds <= 0.0) { // instant timeout
        seconds = 0.0;
        timeout_context->termTSR = 0ULL;
    } else if (seconds <= TIMER_INTERVAL_LIMIT) { //超时时间在最大限制内,才创建timeout_timer
        dispatch_queue_t queue = pthread_main_np() ? __CFDispatchQueueGetGenericMatchingMain() : __CFDispatchQueueGetGenericBackground();
        timeout_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
            dispatch_retain(timeout_timer);
        timeout_context->ds = timeout_timer;
        timeout_context->rl = (CFRunLoopRef)CFRetain(rl);
        timeout_context->termTSR = startTSR + __CFTimeIntervalToTSR(seconds);
        dispatch_set_context(timeout_timer, timeout_context); // source gets ownership of context
        dispatch_source_set_event_handler_f(timeout_timer, __CFRunLoopTimeout);
        dispatch_source_set_cancel_handler_f(timeout_timer, __CFRunLoopTimeoutCancel);
        uint64_t ns_at = (uint64_t)((__CFTSRToTimeInterval(startTSR) + seconds) * 1000000000ULL);
        dispatch_source_set_timer(timeout_timer, dispatch_time(1, ns_at), DISPATCH_TIME_FOREVER, 1000ULL);
        dispatch_resume(timeout_timer);
    } else { // infinite timeout
        seconds = 9999999999.0;
        timeout_context->termTSR = UINT64_MAX;
    }

GCD API 记录 (三)中的 dispatch_source中的timer中有详细讲解。 但同时 GCD 提供的某些接口也用到了 RunLoop, 例如 dispatch_async()。当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里执行这个 block。

GCD定时器实现:
  • 执行一次
代码语言:javascript
复制
double delayTimer = 1.0;    
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayTimer * NSEC_PER_SEC);   
 dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ 
          //do
});
  • 重复执行
代码语言:javascript
复制
NSTimeInterval delayTimer = 1.0;     
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);    
dispatch_source_t _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);    dispatch_source_set_timer(_timer, dispatch_walltime(NULL, 0), delayTimer * NSEC_PER_SEC, 0);     
dispatch_source_set_event_handler(_timer, ^{   
       //do    
});
 dispatch_resume(_timer);

NSTimer

在介绍RunLoop时已经提到过:NSTimer 其实就是 CFRunLoopTimerRef。他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。 如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。

使用方法:
  • 创建方法
代码语言:javascript
复制
  NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(action:) userInfo:nil repeats:NO];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

其中, TimerInterval : 执行之前等待的时间。比如设置成1.0,就代表1秒后执行方法 target : 需要执行方法的对象。 selector : 需要执行的方法 repeats : 是否需要循环

  • 结束方法
代码语言:javascript
复制
[timer invalidate];

CADisplayLink

简单地说,CADisplayLink就是一个定时器,保持跟屏幕刷新率相同的频率刷新。 虽然CADisplayLink使用场合相对专一,只适合做UI的不停重绘,但并不妨碍他成为很多高手热爱的技巧之一。在做精细的动画效果时,CADisplayLink将是一个很好的助手,例如自定义动画引擎或者视频播放的渲染;类似于siri语音输入效果就用到了CADisplayLink;很多模仿wave效果也多采用CADisplayLink刷新界面。 iOS设备的屏幕刷新频率是固定的,我们在使用时不用关心屏幕的刷新频率,因为它本身就是跟屏幕刷新同步的。CADisplayLink在正常情况下会在每次刷新结束都被调用,精确度相当高。

属性
  • timestamp:只读,屏幕显示的上一帧的时间戳,timestamp = duration * frameInterval。
  • duration:只读,系统屏幕每次刷新的时间戳,在target的selector被首次调用以后被系统赋值。
  • targetTimestamp:只读
  • paused:读写,控制CADisplayLink的运行,即暂停操作。结束一个CADisplayLink实例需要调用invalidate从runloop删除
  • frameInterval:读写,标识间隔多少帧调用一次selector 方法,默认为1,即每帧都调用一次。对于iOS设备来说那刷新频率就是60HZ也就是每秒60次,如果将 frameInterval 设为2 那么就会两帧调用一次,也就是变成了每秒刷新30次。
  • preferredFramesPerSecond:
使用方法:
  • 创建方法
代码语言:javascript
复制
  CADisplayLink *timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(wave)];
    [timer addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
  • 结束方法
代码语言:javascript
复制
[timer invalidate];
衍生:CADisplayLink与UIBezierPath、CAShapeLayer的激情碰撞

如果说CADisplayLink是控制动画流畅度的尚方宝剑,那UIBezierPath与CAShapeLayer就是实现动画效果的倚天屠龙。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • RunLoop
    • RunLoop概念
    • NSObject
    • GCD
      • GCD定时器实现:
      • NSTimer
        • 使用方法:
        • CADisplayLink
          • 属性
            • 使用方法:
              • 衍生:CADisplayLink与UIBezierPath、CAShapeLayer的激情碰撞
              相关产品与服务
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档