iOS定时器-NSTimer&GCD定时器

在iOS开发中定时器是我们经常遇到的需求,常用到的定时器表示方式有NSTimer、GCD,那么它们之间有什么样的区别呢?本文将从两者的基本使用开始剖析它们之间的区别。

1、NSTimer

1.1、NSTimer简介

NSTimer是iOS中最基本的定时器。NSTimer是通过RunLoop来实现的,在一般的情况下NSTimer作为定时器是比较准确的,但是如果当前的耗时操作较多时,可能出现延时问题。同时,因为受到RunLoop的支配,NSTimer会受到RunLoopMode的影响。在创建NSTimer的时候默认是被加到defaultMode的,但是如果在一个滑动的视图中如tableview,当RunLoop的mode发生变化时,当前的NSTimer就不会工作了,这就是我们在开发中遇到的NSTimer用在tableview中,当tableview滚动的时候NSTimer停止工作的原因,所以我们在创建NSTimer的时候将其加到RunLoop指定mode为NSRunLoopCommonModes

1.2、NSTimer基本使用

NSTimer的初始化方式有两种,分别是invocationselector两种调用方式,这两种方式区别不大,但是selector的方式更加简便。

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;

下面我们来看下这两种方式的使用。

1.2.1、selector方式

使用selector方式初始化NSTimer比较简单,只需要指定执行的方法和是否循环就可以了。

- (void)selectorType {
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];

    // NSDefaultRunLoopMode模式,切换RunLoop模式,定时器停止工作.
    // [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    
    // UITrackingRunLoopMode模式,切换RunLoop模式,定时器停止工作.
    // [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
    
    // common modes的模式,以下三种模式的组合模式 NSDefaultRunLoopMode & NSModalPanelRunLoopMode & NSEventTrackingRunLoopMode
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
- (void)timerTest {
    NSLog(@"hello");
}

在上一个小节讲过,NSTimer依赖于RunLoop,需要把初始化好的timer添加到RunLoop中,对于RunLoop的几种模式在上面的代码注释中有说明。
这段代码的运行结果就是每隔两秒钟就会打印一次“hello”

打印结果:
2020-03-16 17:55:24.123435+0800 ThreadDemo[3845:9977585] hello
2020-03-16 17:55:26.122417+0800 ThreadDemo[3845:9977585] hello
2020-03-16 17:55:28.123599+0800 ThreadDemo[3845:9977585] hello
2020-03-16 17:55:30.122504+0800 ThreadDemo[3845:9977585] hello

1.2.1、invocation方式

通过invocation方式初始化timer相对于来说会稍微复杂一些,最主要的是invocation参数。同样的也需要手动将timer加入到RunLoop中。

- (void)invocationType {
    // 获取到方法的签名
    NSMethodSignature *signature = [[self class]instanceMethodSignatureForSelector:@selector(timerTest)];
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
    invocation.target = self;
    invocation.selector = @selector(timerTest);

    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 invocation:invocation repeats:YES];
    [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
}

- (void)timerTest {
    NSLog(@"hello");
}

这段代码的运行结果就是每隔两秒钟就会打印一次“hello”

打印结果:
2020-03-16 22:54:48.964318+0800 ThreadDemo[6400:10171057] hello
2020-03-16 22:54:50.964530+0800 ThreadDemo[6400:10171057] hello
2020-03-16 22:54:52.964403+0800 ThreadDemo[6400:10171057] hello
2020-03-16 22:54:54.964780+0800 ThreadDemo[6400:10171057] hello

1.2.3、scheduledTimerWithTimeInterval方法

在上面列举的API中其实有scheduledTimerWithTimeInterval方法可以创建timer,这个方法和timerWithTimeInterval的区别就在于前者会默认的将timer添加到了RunLoop,并且currentRunLoop是NSDefaultRunLoopMode,而后者是需要开发者手动的将timer添加到RunLoop中。

- (void)scheduledTimer {
//    NSTimer *timer1 = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];

    NSMethodSignature *signature = [[self class] instanceMethodSignatureForSelector:@selector(timerTest)];
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
    invocation.target = self;
    invocation.selector = @selector(timerTest);

    NSTimer *timer2 = [NSTimer scheduledTimerWithTimeInterval:2.0 invocation:invocation repeats:YES];
}
- (void)timerTest {
    NSLog(@"hello");
}

这段代码的运行结果就是每隔两秒钟就会打印一次“hello”

打印结果:
2020-03-16 23:05:30.717027+0800 ThreadDemo[6581:10181270] hello
2020-03-16 23:05:32.715849+0800 ThreadDemo[6581:10181270] hello

2020-03-16 23:05:34.716522+0800 ThreadDemo[6581:10181270] hello

如上代码所示,并没有将timer添加到RunLoop,timer照样可以正常运行。

1.2.4 NSTimer在线程中使用

上面所列举的例子都是在主线程中运行的,那是因为主线程默认是启动RunLoop的,但是在线程是没有默认开启RunLoop的,所以当在子线程中使用NSTimer的时候就需要手动开启RunLoop了。

- (void)timerInThread {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];

        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
        [[NSRunLoop currentRunLoop]run];
    });
}
- (void)timerTest {
    NSLog(@"hello");
}

1.3、NSTimer中存在的问题

1.3.1、RunLoop的mode问题

如果在一个滚动的视图(如tableview)使用NSTimer,在视图滚动的时候,timer会停止计时,那是因为当视图滚动的时候RunLoop的mode是UITrackingRunLoopMode模式。解决方式就是把timer 添加到RunLoop的NSRunLoopCommonModes,那么UITrackingRunLoopModekCFRunLoopDefaultMode都被标记为了common模式,就可以在默认模式和追踪模式都能够运行。

1.3.2、NSTimer的循环引用

当NSTimer的target被强引用了,而target又强引用的timer,这样就造成了循环引用,导致timer无法释放产生内存泄露的问题。这也是在开发中经常遇到的问题。当然不是所有的NSTimer都会产生循环引用。

    1. repeats参数为NO的情况下,不会产生循环引用。
    1. ios10后的新的API方法timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block也不会产生循环引用,但是不要忘记了在合适的地方调用invalidate方法停止定时器的运行。

要解决NSTimer的循环引用问题就需要打破NSTimer和target之间的循环条件,有如下几种方式。

1.3.2.1、NSProxy的方式

创建一个中间类DSProxy继承自NSProxy,这个类中对timer的target进行弱引用,再把需要执行的方法都转发给timer的target。

@interface DSProxy : NSProxy

@property (weak, nonatomic) id target;
+ (instancetype)proxyWithTarget:(id)target;
@end

@implementation DSProxy

+ (instancetype)proxyWithTarget:(id)target {
    DSProxy* proxy = [[self class] alloc];
    proxy.target = target;
    return proxy;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
    return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation{
    SEL sel = [invocation selector];
    if ([self.target respondsToSelector:sel]) {
        [invocation invokeWithTarget:self.target];
    }
}

@interface ProxyTimer : NSObject

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval target:(id)target selector:(SEL)selector userInfo:(nullable id)userInfo repeats:(BOOL)repeats;

@end

@implementation ProxyTimer

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval target:(id)target selector:(SEL)selector userInfo:(nullable id)userInfo repeats:(BOOL)repeats{
    NSTimer* timer = [NSTimer scheduledTimerWithTimeInterval:timeInterval target:[DSProxy proxyWithTarget: target] selector:selector userInfo:userInfo repeats:repeats];
    return timer;
}

@end
1.3.2.2、NSTimer封装

这种方式其实和NSProxy的方式很类似,创建一个类对NSTimer进行封装,将taget弱引用,

@interface DSTimer : NSObject

@property (nonatomic, weak) id target;
@property (nonatomic) SEL selector;

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval target:(id)target selector:(SEL)selector userInfo:(nullable id)userInfo repeats:(BOOL)repeats;

@end

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval target:(id)target selector:(SEL)selector userInfo:(nullable id)userInfo repeats:(BOOL)repeats {
    DSTimer *dsTimer = [[DSTimer alloc] init];
    dsTimer.target = target;
    dsTimer.selector = selector;
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:timeInterval target:dsTimer selector:@selector(timered:) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
    return timer;
}

- (void)timered:(NSTimer *)timer {
    if ([self.target respondsToSelector:self.selector]) {
        #pragma clang diagnostic push
        #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        [self.target performSelector:self.selector withObject:timer];
        #pragma clang diagnostic pop
    }
}
1.3.2.2、block实现
@interface NSTimer (DSTimer)

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval repeats:(BOOL)repeats blockTimer:(void (^)(NSTimer *))block;

@end

@implementation NSTimer (DSTimer)

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval repeats:(BOOL)repeats blockTimer:(void (^)(NSTimer *))block {
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:timeInterval target:self selector:@selector(timered:) userInfo:[block copy] repeats:repeats];
    [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
    return timer;
}

+ (void)timered:(NSTimer *)timer {
    void (^ block)(NSTimer *timer)  = timer.userInfo;
    block(timer);
}

@end

2、GCD

2.1、GCD简介

GCD实现定时器功能,是利用GCD中的Dispatch Source中的一种类型DISPATCH_SOURCE_TYPE_TIMER来实现的。dispatch源(Dispatch Source)监听系统内核对象并处理,更加的精准。和NSTimer依赖于RunLoop不一样,GCD并不依赖于RunLoop,所以即使是在滚动视图中也不会出现视图滚动时定时器不起效果的情况。同时GCD定时器提供了定时器的启动、暂停、回复、取消等功能,相对而言更加的贴近开发需求。

2.2、GCD基本使用

GCD定时器调用 dispatch_source_create方法创建一个source源,然后通过dispatch_source_set_timer方法设置定时器,dispatch_source_set_event_handler设置定时器任务,初创建的定时器是暂停的,需要调用dispatch_resume方法启动定时器,当然也可以调用dispatch_suspend或者dispatch_source_cancel停止定时器。

下面是对于GCD的简单封装。

typedef enum : NSUInteger {
    Status_Running,
    Status_Pause,
    Status_Cancle,
} TimerStatus;

@interface GCDTimer ()

@property (nonatomic, strong) dispatch_source_t gcdTimer;
@property (nonatomic, assign) TimerStatus currentStatus;

@end

@implementation GCDTimer

- (void)scheduledTimerWithTimeInterval:(NSTimeInterval)interval runNow:(BOOL)runNow afterTime:(NSTimeInterval)afterTime repeats:(BOOL)repeats queue:(dispatch_queue_t)queue block:(void (^)(void))block  {
    /** 创建定时器对象
    * para1: DISPATCH_SOURCE_TYPE_TIMER 为定时器类型
    * para2-3: 中间两个参数对定时器无用
    * para4: 最后为在什么调度队列中使用
    */
    self.gcdTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    /** 设置定时器
    * para2: 任务开始时间
    * para3: 任务的间隔
    * para4: 可接受的误差时间,设置0即不允许出现误差
    * Tips: 单位均为纳秒
    */
    dispatch_time_t when;
    if (runNow) {
        when = DISPATCH_TIME_NOW;
    } else {
        when = dispatch_walltime(NULL, (int64_t)(afterTime * NSEC_PER_SEC));
    }
    dispatch_source_set_timer(self.gcdTimer, dispatch_time(when, interval * NSEC_PER_SEC), interval * NSEC_PER_SEC, 0);
    dispatch_source_set_event_handler(self.gcdTimer, ^{
        if (!repeats) {
            dispatch_source_cancel(self.gcdTimer);
        }
        block();
    });
    dispatch_resume(self.gcdTimer);
    self.currentStatus = Status_Running;
}

- (void)pauseTimer {
    if (self.currentStatus == Status_Running && self.gcdTimer) {
        dispatch_suspend(self.gcdTimer);
        self.currentStatus = Status_Pause;
    }
}

- (void)resumeTimer {
    if (self.currentStatus == Status_Pause && self.gcdTimer) {
        dispatch_resume(self.gcdTimer);
        self.currentStatus = Status_Running;
    }
}

- (void)stopTimer {
    if (self.gcdTimer) {
        dispatch_source_cancel(self.gcdTimer);
        self.currentStatus = Status_Cancle;
        self.gcdTimer = nil;
    }
}


@end

2.3、GCD定时器的注意事项

1、dispatch_resumedispatch_suspend调用要成对出现。dispatch_suspend 严格上只是把timer暂时挂起,dispatch_resumedispatch_suspend分别会减少和增加 dispatch 对象的挂起计数。当这个计数大于 0 的时候,timer就会执行。但是Dispatch Source并没有提供用于检测 source 本身的挂起计数的 API,也就是说外部不能得知一个 source 当前是不是挂起状态,那么在两者之间需要设计一个标记变量。
2、source在suspend状态下,如果直接设置source = nil或者重新创建source都会造成crash。正确的方式是在resume状态下调用dispatch_source_cancel(source)释放当前的source。
3、dispatch_source_set_event_handler回调是一个block,在添加到source中后会被source强引用,所以在这里需要注意循环引用的问题。正确的方法是使用weak+strong或者提前调用dispatch_source_cancel取消timer。

3、NSTimer和GCD定时器的比较

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

推荐阅读更多精彩内容