iOS 定时中心及高精度定时

最近一个做电商类APP的朋友说,限时优惠页面 UITableView 上的 Cell 全部都需要倒计时,然后他的预想做法呢,就是每个一个 Timer 。。。听了好震惊呀,因为他们 APP 除了这里很多地方也需要倒计时,可是却每个地方都一个 Timer 。。。 遂建议封装个定时中心LYTimerHelper,主要的点其实就如下:

  1. 间隔Inteval一致的Timer同时间只存在一个(其实根据业务一般也是 1s 倒计时,所以大部分并不会创建太多个Timer
  2. 通过key将执行的操作通过block加入对应的Timer,也可通过key移除
  3. 每次Timer唤醒执行所有的block后,都进行判断是否还存在需要执行的block,若是不存在则这个Timer销毁,若是存在则继续等待下一次唤醒

所以限时优惠页面就很简单了,使用 MVVM+RAC 轻松的就能完成,只要将页面中的商品 model 倒计时操作加入定时中心,根据对应 model 的倒计时变化刷新对应 cell 的时间显示即可,别的地方亦是如此

很简单吧,那就顺便聊一聊 iOS 里的定时有哪些


  • NSTimer

NSTimer是最为常见的一种定时方式,根据文档可以知道它的使用方式也比较简单,文档也表明 NSTimer 是通过添加到RunLoop中被触发进行工作的,桥接 CFRunLoopTimerRef

Timers work in conjunction with run loops. Run loops maintain strong references to their timers

self.timer = [NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(timerRuning) userInfo:nil repeats:YES];
  // 或者
self.timer = [NSTimer timerWithTimeInterval:5 target:self selector:@selector(timerRuning) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
// 或者 延迟30s开始
NSTimeInterval timeInterval = [self timeIntervalSinceReferenceDate] + 30;
NSDate *newDate = [NSDate dateWithTimeIntervalSinceReferenceDate:timeInterval];
self.timer = [[NSTimer alloc] initWithFireDate:newDate interval:5 target:self selector:@selector(timerRuning) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];

酱紫其实就可以进行定时(iOS10 提供了更加便利的 block 方法)

RunLoop 影响

上面的代码在使用过程中,会发现当 UITableView 在滚动过程中,定时器到了时间并未触发

原因
RunLoop 的几种 Mode

A run loop mode is a collection of input sources and timers to be monitored and a collection of run loop observers to be notified. Each time you run your run loop, you specify (either explicitly or implicitly) a particular “mode” in which to run. During that pass of the run loop, only sources associated with that mode are monitored and allowed to deliver their events. (Similarly, only observers associated with that mode are notified of the run loop’s progress.) Sources associated with other modes hold on to any new events until subsequent passes through the loop in the appropriate mode

说的就是同一时候 RunLoop 只运行在一种 Mode 上,并且只有这个Mode相关联的源或定时器会被传递消息,更多内容可以查阅RunLoop
mainRunLoop一般处于 NSDefaultRunLoopMode,但是在滚动或者点击事件等触发时,mainRunLoop切换至 NSEventTrackingRunLoopMode,而上面 timer被加入的正是NSDefaultRunLoopMode(未指明也默认加入默认模式),所以滑动时未触发定时操作。

解决

添加timermainRunLoopNSRunLoopCommonMode中或者子线程中,而这里加入子线程中需要注意的手动开启并运行子线程的RunLoop

self.timer = [NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(timerRuning) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonMode];

NSRunLoopCommonMode这是一组可配置的常用模式。将输入源与此模式相关联也会将其与组中的每个模式相关联。对于Cocoa应用程序,此集合默认包括NSDefaultRunLoopModeNSPanelRunLoopModeNSEventTrackingRunLoopMode

其它需要注意的点
  1. Nonrepeating Timer运行完自行无效,以防再次触发,但 Repeating Timer需要手动调用
[self.timer invalidate];
self.timer = nil;
  1. NSTimer在运行期间会对target进行持有,所以切记在退出前invalidate,否则内存泄漏了(别再target的dealloc里调用,噗噗)

  2. NSTimer非实时性的
    a. 一个是可能因为当前RunLoop运行的Mode不监听导致未能触发
    b. 一个可能当前RunLoop做了耗时的工作,使得持续时间超过了一个或若干个NSTimer的触发时间,NSTimer不会进行补偿操作,只是此次循环检查定时器时触发NSTimer,但是不影响后面其它的触发时间,因为NSTimer根据一开始计划的时间来触发,并不根据每次被触发的实际时间点来计算下一次的触发时间
    c. fire方法立即触发时间,对于Nonrepeating Timer运行完自行无效,但是跟 b 点一样,不影响Repeating Timer的预定周期性触发

NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
     NSLog(@"timer fired ^_^");
}];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
     NSLog(@"a lot of work begin ^_^");
     NSMutableString *countStr = [[NSMutableString alloc] init];
     for (NSUInteger i = 0; i < 9999999; i++) {
           [countStr appendFormat:@"i_ = %ld ", i];
     }
     NSLog(@"a lot of work end ^_^");
});

如 b 点所说,大量工作耗费较长时间,这中间并未触发定时,同时工作完成在此次循环检查了定时器触发了定时 D,而 E 的时间点也依然是按一开始的时间之后 N * 1s的预计触发,而不是在 D 之后 1s


  • CADisplayLink

CADisplayLink是基于屏幕刷新周期的定时器,一般 60 frames/s,同样也是基于RunLoop,因此也会碰到因为运行在非定时触发的Mode或者工作耗时导致的延迟(这点跟NSTimer一样),这里需要注意的是回调工作若是当前线程大量计算,也会导致下一次的延迟,掉帧卡顿发生
使用也比较简单,文档说的比较清楚

// 1.初始化
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLink:)];
// 2. 设置 - 2桢回调一次,这里非时间,而是以桢为单位
self.displayLink.frameInterval = 2; //iOS10之前
self.displayLink.preferredFramesPerSecond = 30; //iOS10及之后

// 3.加入RunLoop
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
//
// 4.callback
- (void)displayLink:(CADisplayLink *)displayLink {
    ... ...
// 5.时间累计:每次回调的间隔时间
    self.accumulator += displayLink.duration * displayLink.frameInterval; //粗略计算,因为可能碰到大量工作等导致间隔时间bu zhu
// 或者🔥
    if (self.timestamp == 0) {
        self.timestamp = displayLink.timestamp;
    }
    CFTimeInterval now = displayLink.timestamp; // 获取当前的时间戳
    self.accumulator += now - self.timestamp;
    self.timestamp = now;
// 6.预计下一次时间
    NSTimeInterval next = displayLink.targetTimestamp; //iOS10及之后
}
... ...
// 7.暂停
self.displayLink.paused = YES;
// 8.销毁
[self.displayLink invalidate];
self.displayLink = nil;

CADisplayLink因为同步屏幕刷新频率,屏幕刷新后立即回调,因此很适合跟 UI 相关的定时绘制操作,像进度条、FPS等等,这样就无须进行多余运算
同样 CADisplayLink 会对 target 持有,所以记得进行释放,以免造成内存泄露


  • GCD

Grand Central Dispatch 简称 GCD,一套低层API,提供了简单易用并无需直接操作线程居于队列的并发编程,详情查阅
GCD 功能非常强大,今天只涉及 Dispatch Sources 中的定时器

Dispatch Sources 替换了处理系统相关事件的异步回调函数。配置一个dispatch source,需要指定要监测的事件(DISPATCH_SOURCE_TYPE_TIMER等)、dispatch queue、以及处理事件的代码(blockC函数)。当事件发生时,dispatch source会提交对应的blockC函数到指定的dispatch queue执行

Dispatch Sources监听系统内核对象并处理,通过系统级调用,会比NSTimer更精准一些

// 1. 创建 dispatch source,并指定检测事件为定时
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue("LY_Timer_Queue", 0));
// 2. 设置定时器启动时间,间隔,容差
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 0.5 * NSEC_PER_SEC,  0 * NSEC_PER_SEC); 
// 3. 设置callback
dispatch_source_set_event_handler(timer, ^{
        NSLog(@"timer fired ^_^");
    });
dispatch_source_set_event_handler(timer, ^{
       //取消定时器时一些操作
    });
// 4.启动定时器-刚创建的source处于被挂起状态
dispatch_resume(timer);
// 5.暂停定时器
dispatch_suspend(timer);
// 6.取消定时器
dispatch_source_cancel(timer);
timer = nil;

觉得每次创建代码太多,封装下即可

需要注意的点
  1. 与其他 dispatch objects 一样,dispatch sources 也是引用计数数据类型,在 ARC 中无需手动调用 dispatch_release(timer)

  2. dispatch_suspend(timer)timer 挂起,若是此时已经在执行 block,继续完成此次 block,并不会立即停止

  3. dispatch source在挂起时,直接设置为 nil 或者 其它新源都会造成crash,需要在activate的状态下调用dispatch_source_cancel(timer)后再重新创建。

  4. dispatch_source_set_timer中设置启动时间,dispatch_time_t可通过两个方法生成:dispatch_timedispatch_walltime
    a. dispatch_time创建相对时间,基于mach_absolute_time,CPU的时钟周期数ticks,这个tricks在每次手机重启之后,会重新开始计数,而且iPhone锁屏进入休眠之后tick也会暂停计数,mach_absolute_time()不会受系统时间影响,只受设备重启和休眠行为影响。

    b. dispatch_walltime类似创建绝对时间,当设备休眠时,时间依然再走

    所以这两者的区别就是,当设备休眠时,dispatch_time停止运行,dispatch_walltime继续运行,所以如果一个事件处理是在30分钟之后,运行5分钟后,设备休眠20分钟,两个时间对应的事件触发点如下:

  1. 若是想像 NSTimer 实现 Nonrepeating Timer,则使用 dispatch_after
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void) {
});
  1. dispatch_source 可内部持有也可外部持有,内部持有可在事件处理block中进行有条件取消
dispatch_source_set_event_handler(timer, ^{
        NSLog(@"dis timer fired ^_^");
        if (条件满足) {
            dispatch_source_cancel(timer);
        }
    });
  1. 可以通过dispatch_set_target_queue(timer, queue)更改事件处理的所在队列,修改 dispatch source 是异步操作,所以不会更改已经在执行的事件

  2. dispatch_resumedispatch_suspend调用需一一对应,重复调用dispatch_resume会crash


  • 高精度定时

以上的几种定时都会受限于苹果为了保护电池和提高性能采用的策略而导致有延迟,像NSTimer会有50-100毫秒的误差,若的确需要使用更高精度的定时器(误差小于0.5毫秒),一般在多媒体操作方面有所需要,苹果官方同样也提供了方法,阅读高精度定时文档
高精度定时用到的比较少,一般视频或者音频相关数据流操作中需要

其实现原理就是使定时器的线程优先于系统上的其他线程,在无多线程冲突的情况下,这定时器的请求会被优先处理,所以不要创建大量的实时线程,一旦某个线程都要被优先处理,结果就是实时线程都失败了

所以现在实现高精度定时有两种方法:
1.使用Mach Thread API把定时器所在的线程,移到高优先级的线程调度类即the real time scheduling class

#include <mach/mach.h>
#include <mach/mach_time.h>
#include <pthread.h>
 
void move_pthread_to_realtime_scheduling_class(pthread_t pthread)
{
    mach_timebase_info_data_t timebase_info;
    mach_timebase_info(&timebase_info);
 
    const uint64_t NANOS_PER_MSEC = 1000000ULL;
    double clock2abs = ((double)timebase_info.denom / (double)timebase_info.numer) * NANOS_PER_MSEC;
 
    thread_time_constraint_policy_data_t policy;
    policy.period      = 0;
    policy.computation = (uint32_t)(5 * clock2abs); // 5 ms of work
    policy.constraint  = (uint32_t)(10 * clock2abs);
    policy.preemptible = FALSE;
 
    int kr = thread_policy_set(pthread_mach_thread_np(pthread_self()),
                   THREAD_TIME_CONSTRAINT_POLICY,
                   (thread_policy_t)&policy,
                   THREAD_TIME_CONSTRAINT_POLICY_COUNT);
    if (kr != KERN_SUCCESS) {
        mach_error("thread_policy_set:", kr);
        exit(1);
    }
}

2.使用更精确的计时API mach_wait_until(),如下代码使用mach_wait_until()等待10秒

#include <mach/mach.h>
#include <mach/mach_time.h>
 
static const uint64_t NANOS_PER_USEC = 1000ULL;
static const uint64_t NANOS_PER_MILLISEC = 1000ULL * NANOS_PER_USEC;
static const uint64_t NANOS_PER_SEC = 1000ULL * NANOS_PER_MILLISEC;
 
static mach_timebase_info_data_t timebase_info;
 
static uint64_t abs_to_nanos(uint64_t abs) {
    return abs * timebase_info.numer  / timebase_info.denom;
}
 
static uint64_t nanos_to_abs(uint64_t nanos) {
    return nanos * timebase_info.denom / timebase_info.numer;
}
 
void example_mach_wait_until(int argc, const char * argv[])
{
    mach_timebase_info(&timebase_info);
    uint64_t time_to_wait = nanos_to_abs(10ULL * NANOS_PER_SEC);
    uint64_t now = mach_absolute_time();
    mach_wait_until(now + time_to_wait);
}

以上的所有定时器,虽然提供方法各不相同,但它们的内核代码是相同的

  1. NSTimer平时用的比较多,一般也足够,需要注意的就是加入的RunLoopMode 或者若是子线程,需要手动启动并运行RunLoop;同时注意使用invalidate手动停止定时,否则引起内存泄漏;
  2. 需与显示更新同步的定时,建议CADisplayLink,可以省去多余计算
  3. GCD定时精度较NSTimer高,使用时只需将任务提交给相应队列即可,对文件资源等定期读写操作很方便,使用时需要⚠️dispatch_resumedispatch_suspend配套,同时要给 dispatch source设置新值或者置nil,需先dispatch_source_cancel(timer)
  4. 高精度定时,使用较少,一般多媒体视频流/音频流处理相关需要

好了,就写到这里吧,反正吃饭时间到了。。。就酱紫

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容