iOS的几种定时器及区别

来自我的个人博客Minecode.link

在开发中我们经常用到定时器,iOS为我们提供了多种定时器,包括NSTimer、CADisplayLink、GCD、NSThread(performSelector:afterDelay:),其本质都是通过RunLoop来实现,但GCD通过其调度机制大大提高了性能。定时器的使用中容易存在一些误区,故写本文总结。

本文将介绍iOS的几种定时器、定时器的立即执行方法、内存泄露问题、不准时问题

NSTimer

iOS中最基本的定时器,在Swift中称为Timer。其通过RunLoop来实现,一般情况下较为准确,但当当前循环耗时操作较多时,会出现延迟问题。同时,也受所加入的RunLoop的RunLoopMode影响,具体可以参考RunLoop的特性。

创建

构造方法主要分为自动启动和手动启动,手动启动的构造方法需要我们在创建NSTimer后手动启动它:

/// 构造并开启(启动NSTimer本质上是将其加入RunLoop中)
// "scheduledTimer"前缀的为自动启动NSTimer的,如:
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block

/// 构造但不开启
// "timer"前缀的为只构造不启用的,如:
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block

前面说到,定时器的本质是加入到了RunLoop的Timer列表中,从而随着运行循环来实现定时器的功能。所以NSTimer除了构造,还需要加入RunLoop。关于RunLoop简单实用可以见文末。

释放

定时器的释放一定要先将其终止,而后才能销毁对象。具体原因下文会说。

- (void)invalidate;

立即执行(fire)

我们对定时器设置了延时之后,有时需要让它立刻执行,可以使用fire方法:

- (void)fire;

但是该方法的使用需要注意: fire方法不会改变预定周期性调度。什么意思呢?就是说,如果我们把Timer设置为循环调用,那么我们任何时候调用fire方法,下一次调度的时间仍旧是按照预定时间,而非基于本次执行的时间计算而得。这里需要特别注意,我们可以参考下面的🌰:

// Declare a timer with 10s interval
self.timer1 = [NSTimer timerWithTimeInterval:10.0 target:self selector:@selector(timerMethod1) userInfo:nil repeats:NO];
[[NSRunLoop currentRunLoop] addTimer:self.timer1 forMode:NSRunLoopCommonModes];

self.timer2 = [NSTimer timerWithTimeInterval:10.0 target:self selector:@selector(timerMethod2) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer2 forMode:NSRunLoopCommonModes];

NSLog(@"Launch %@", [NSDate date]);

// Fire at 8.0s
__weak typeof(self) weakSelf = self;
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (ino64_t)(8.0 * NSEC_PER_SEC));
dispatch_after(time, dispatch_get_main_queue(), ^{
    __strong typeof(self) strongSelf = weakSelf;
    [strongSelf.timer1 fire];
    [strongSelf.timer2 fire];
});

// Log
- (void)timerMethod1 {
    static int timerIdx1 = 0;
    NSLog(@"Timer Method1: %@ %d", [NSDate date], timerIdx1++);
}
- (void)timerMethod2 {
    static int timerIdx2 = 0;
    NSLog(@"Timer Method2: %@ %d", [NSDate date], timerIdx2++);
}

我们定义了两个NSTimer并加入到RUnLoop中,其目标方法和其他属性均相同,唯一区别是前者只运行一次。

我们在第8秒时调用fire方法,结果如何呢? timer1立即执行,并且由于仅执行一次,其任务结束。而timer2在第8秒执行后,仍旧在第10秒执行,这样的结果说明了fire方法不会改变预定周期性调度

CADisplayLink

CADisplayLink是基于屏幕刷新的周期,所以其一般很准时,每秒刷新60次。其本质也是通过RunLoop,所以不难看出,当RunLoop选择其他模式或被耗时操作过多时,仍旧会造成延迟。

其使用步骤为 创建CADisplayLink->添加至RunLoop中->终止->销毁。代码如下:

// 创建CADisplayLink
CADisplayLink *disLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkMethod)];
// 添加至RunLoop中
[disLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
// 终止定时器
[disLink invalidate];
// 销毁对象
disLink = nil;

由于其并非NSTimer的子类,直接使用NSRunLoop的添加Timer方法无法加入,应使用CADisplayLink自己的addToRunLoop:forMode:方法。

同时,由于其是基于屏幕刷新的,所以也度量单位是每帧,其提供了根据屏幕刷新来设置间隔的frameInterval属性,其决定于屏幕刷新多少帧时调用一次该方法,默认为1,即1/60秒调用一次。

如果我们想要计算出每次调用的时间间隔,可以通过frameInterval * duration求出,后者为屏幕每帧间隔的只读属性。

在日常开发中,适当使用CADisplayLink甚至有优化作用。比如对于需要动态计算进度的进度条,由于起进度反馈主要是为了UI更新,那么当计算进度的频率超过帧数时,就造成了很多无谓的计算。如果将计算进度的方法绑定到CADisplayLink上来调用,则只在每次屏幕刷新时计算进度,优化了性能。MBProcessHUB则是利用了这一特性。

GCD

GCD定时器是dispatch_source_t类型的变量,其可以实现更加精准的定时效果。我们来看看如何使用:

/** 创建定时器对象
 * para1: DISPATCH_SOURCE_TYPE_TIMER 为定时器类型
 * para2-3: 中间两个参数对定时器无用
 * para4: 最后为在什么调度队列中使用
 */
_gcdTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
/** 设置定时器
 * para2: 任务开始时间
 * para3: 任务的间隔
 * para4: 可接受的误差时间,设置0即不允许出现误差
 * Tips: 单位均为纳秒
 */
dispatch_source_set_timer(_gcdTimer, DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC, 0.0 * NSEC_PER_SEC);
/** 设置定时器任务
 * 可以通过block方式
 * 也可以通过C函数方式
 */
dispatch_source_set_event_handler(_gcdTimer, ^{
    static int gcdIdx = 0;
    NSLog(@"GCD Method: %d", gcdIdx++);
    NSLog(@"%@", [NSThread currentThread]);
    
    if(gcdIdx == 5) {
        // 终止定时器
        dispatch_suspend(_gcdTimer);
    }
});
// 启动任务,GCD计时器创建后需要手动启动
dispatch_resume(_gcdTimer);

GCD更准时的原因

通过观察代码,我们可以发现GCD定时器实际上是使用了dispatch源(dispatch source),dispatch源监听系统内核对象并处理。dispatch类似生产者消费者模式,通过监听系统内核对象,在生产者生产数据后自动通知相应的dispatch队列执行,后者充当消费者。通过系统级调用,更加精准。

定时器不准时的问题及解决

通过上文的叙述,我们大致了解了定时器不准时的原因,总结一下主要是

  • 当前RunLoop过于繁忙
  • RunLoop模式与定时器所在模式不同

上面解释了GCD更加准时的原因,所以解决方案也不难得出:

  • 避免过多耗时操作并发
  • 采用GCD定时器
  • 创建新线程并开启RunLoop,将定时器加入其中(适度使用)
  • 将定时器添加到NSRunLoopCommonModes(使用不当会阻塞UI响应)

其中后两者在使用前应确保合理使用,否则会产生负面影响。

定时器的内存泄露问题

定时器在使用时应格外注意内存管理,常见情况时定时器对象无法释放造成内存泄露,而严重情况会造成控制器也无法释放,危害更大。其内存泄露有两部分问题,我们先来看第一部分:

问题1: NSTimer无法释放

我们知道,NSTimer实际上是加入到RunLoop中的,那么在其启动时其被RunLoop强引用,那么即使我们在后面将定时器设为nil,也只是引用计数减少了1,其仍因为被RunLoop引用而无法释放,造成内存泄露。

问题2: 控制器无法释放

这是NSTimer无法释放所造成的更严重问题,由于为定时器设置了target,控制器就会得到一个来自定时器的引用。我们来分析一下这个情况,首先定时器必须被强引用,否则将在autoreleasepool之后被释放掉造成野指针。而定时器通过target属性对控制器产生一个强引用,造成了循环引用。

那么如何解决这两个问题呢?答案就是使用invalidate方法。

苹果文档介绍如下:

This method is the only way to remove a timer from an NSRunLoop object. The NSRunLoop object removes its strong reference to the timer, either just before the invalidate method returns or at some later point.
If it was configured with target and user info objects, the receiver removes its strong references to those objects as well.

即,invalidate方法会将定时器从RunLoop中移除,同时解除对target等对象的强引用。

CADisplayLink同理,而GCD定时器则使用dispatch_suspend()

更多技术文章,欢迎访问
本人博客 - Minecode's Blog
Github - Minecodecraft

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

推荐阅读更多精彩内容