NSTimer 避坑指南

NSTimer 的创建

NSTimer的创建通常有两种方式,一种是以 scheduledTimerWithTimeInterval 为开头的类方法 。这些方法在创建了 NSTimer 之后会将这个 NSTimer 以 NSDefaultRunLoopMode 模式放入当前线程的 RunLoop。

+ ( NSTimer *) scheduledTimerWithTimeInterval:invocation:repeats: 
+ ( NSTimer *) scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:

另一种是以 timerWithTimeInterval 为开头的类方法。这些方法创建的 NSTimer 并不能马上使用,还需要调用 RunLoop 的 addTimer:forMode: 方法将 NSTimer 放入 RunLoop,这样 NSTimer 才能正常工作。

 + ( NSTimer *) timerWithTimeInterval:invocation:repeats:
 + ( NSTimer *) timerWithTimeInterval:target:selector:userInfo:repeats:

Timers work in conjunction with run loops. Run loops maintain strong references to their timers, so you don’t have to maintain your own strong reference to a timer after you have added it to a run loop.

从 NSTimer 的官方文档可以得知,RunLoop 对加入其中的 NSTimer 会添加一个强引用。这里需要注意一个细节问题,以 timerWithTimeInterval 为开头的类方法创建出来的 NSTimer 需要手动加入 RunLoop, 这样 RunLoop 才会对这个 NSTimer 有强引用。若是我们使用 weak 修饰 NSTimer 变量,在 NSTimer 创建之后加入 RunLoop 之前,将 NSTimer 对象赋值给 weak 修饰的变量,那么对导致 NSTimer 对象被释放。

#import "TimerViewController.h"
@interface TimerViewController ()
// 使用 weak
@property (nonatomic,weak) NSTimer *timer;

@end

@implementation TimerViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // NSTimer 创建之后没有被自动加入 RunLoop
    self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(outputLog:) userInfo:nil repeats:YES];
    
    if (self.timer == nil) {
        NSLog(@"timer 被释放了");
    }
}

- (void)outputLog:(NSTimer *)timer{
    NSLog(@"it is log!");
}

@end

代码运行之后,log 输出 “ timer 被释放了 ”,说明 self.timer 为 nil,刚刚创建的 NSTimer 对象被释放了。解决这个问题的方法也很简单, NSTimer 对象创建之后先加入 RunLoop 再赋值给变量。

// ...... 省略代码
- (void)viewDidLoad {
    [super viewDidLoad];
    // 创建 NSTimer
    NSTimer *doNotWorkTimer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(outputLog:) userInfo:nil repeats:YES];
    // NSTimer 加入 NSRunLoop
    [[NSRunLoop currentRunLoop] addTimer:doNotWorkTimer forMode:NSDefaultRunLoopMode];
    // 赋值给 weak 变量
    self.timer = doNotWorkTimer;
    
}
// ...... 省略代码

NSTimer 的循环引用

对于 NSTimer 来说,无论是重复执行的 NSTimer 还是一次性的 NSTimer 只要调用 invalidate 方法则会变得无效,NSTimer 就会释放资源。一次性的 NSTimer 执行完操作后会自动调用 invalidate 方法.

举个例子,TimerViewController 强引用一个 NSTimer,NSTimer 的 target 设置为 TimerViewController,在 TimerViewController 的 dealloc 方法里面调用 NSTimer 的 invalidate 方法。


@interface TimerViewController ()

@property (nonatomic,strong) NSTimer *timer;

@end

@implementation TimerViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(outputLog:) userInfo:nil repeats:YES];
}

-(void)outputLog:(NSTimer *)timer{
    NSLog(@"it is log!");
}

- (void)dealloc
{
    [self.timer invalidate];
    NSLog(@"TimerViewController dealloc!");
}

@end

上面这段代码中的 TimerViewController 和 NSTimer 构成了循环引用,退出 TimerViewController 页面,TimerViewController 和 NSTimer 都无法释放,TimerViewController 的 dealloc 方法没有被调用,NSTimer 就没有被 invalidate ,outputLog 方法会被一直触发。

原来 TimerViewController 强引用一个 NSTimer,NSTimer 使用TimerViewController 为 target, 这样会构成循环引用,那如果 TimerViewController 弱引用一个 NSTimer,是不是能够解决这个问题呢?

@interface TimerViewController ()
// 使用 weak
@property (nonatomic,weak) NSTimer *timer;

@end
// ...... 省略代码

运行结果和上面使用强引用的案例没有什么差别,究竟是什么原因呢?

引用图示

如上图所示 TimerViewController 弱引用 NSTimer, NSTimer 强引用 TimerViewController。

同生共死

TimerViewController 需要 NSTimer 同生共死。NSTimer 需要在 TimerViewController 的 dealloc 方法被 invalidate 。NSTimer 被 invalidate 的前提是 TimerViewController 被 dealloc。而 NSTimer 一直强引用着 TimerViewController 导致 TimerViewController 无法调用 dealloc 方法。
从 NSTimer 的角度来看解决方案,如果 NSTimer 不持有 TimerViewController 的引用,那么 TimerViewController 就可以正常销毁,dealloc 方法可以正常调用 NSTimer 的 invalidate 方法,那么 NSTimer 和 TimerViewController 都可以销毁,完美!

NSTimer 的销毁

在 NSTimer 的使用过程,要避免循环引用问题。解决方案是 NSTimer 不持有 TimerViewController 的引用,也就是说 NSTimer 的 target 对象不要是 TimerViewController。 这里有 2 个方案可以来处理这个问题。

第一个方案:将 target 分离出来独立成一个 WeakProxy 代理对象, NSTimer 的 target 设置为 WeakProxy 代理对象,WeakProxy 是 TimerViewController 的代理对象,所有发送到 WeakProxy
的消息都会被转发到 TimerViewController 对象。使用代理对象可以达到 NSTimer 不直接持有 TimerViewController 的目的。


#import "TimerViewController.h"
#import "YYWeakProxy.h"
@interface TimerViewController ()

@property (nonatomic,weak) NSTimer *timer;

@end

@implementation TimerViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[YYWeakProxy proxyWithTarget:self] selector:@selector(outputLog:) userInfo:nil repeats:YES];
}

- (void)outputLog:(NSTimer *)timer{
    NSLog(@"it is log!");
}

- (void)dealloc
{
    [self.timer invalidate];
    NSLog(@"TimerViewController dealloc!");
}


@end

YYWeakProxy 来自 YYKit 开源项目 ,是一个代理类的实现。

第二个方案是通过 category 把 NSTimer 的 target 设置为 NSTimer 类,让 NSTimer 自身做为target, 把 selector 通过 block 传入给 NSTimer,在 NSTimer 的 category 里面触发 selector 。这样也可以达到 NSTimer 不直接持有 TimerViewController 的目的,实现更优雅 ( 如果是直接支持 iOS 10 以上的系统版本,那可以使用 iOS 10新增的系统级 block 方案 )。

// NSTimer+BlocksSupport.h
#import <Foundation/Foundation.h>

@interface NSTimer (BlocksSupport)
+ (NSTimer *)xx_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                       repeats:(BOOL)repeats
                                         block:(void(^)())block;
@end

// NSTimer+BlocksSupport.m
#import "NSTimer+BlocksSupport.h"

@implementation NSTimer (BlocksSupport)
+ (NSTimer *)xx_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                       repeats:(BOOL)repeats
                                         block:(void(^)())block;
{
    return [self scheduledTimerWithTimeInterval:interval
                                         target:self
                                       selector:@selector(xx_blockInvoke:)
                                       userInfo:[block copy]
                                        repeats:repeats];
}
+ (void)xx_blockInvoke:(NSTimer *)timer {
    void (^block)() = timer.userInfo;
    if(block) {
        block();
    }
}
@end


// TimerViewController.m
#import "TimerViewController.h"
#import "NSTimer+BlocksSupport.h"

@interface TimerViewController ()

@property (nonatomic,weak) NSTimer *timer;

@end

@implementation TimerViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.timer =[NSTimer xx_scheduledTimerWithTimeInterval:1.0 repeats:YES block:^{
            NSLog(@"it is log!");
    }];
}

- (void)dealloc
{
    [self.timer invalidate];
    NSLog(@"TimerViewController dealloc!");
}

@end

以上 2 个方案都可以达到目的,推荐使用第二个 NSTimer 的 category 方案。

NSTimer 触发时间的准确性问题

RunLoop 机制 -来自 sunnyxx

从 RunLoop 的机制图中可以看到 CFRunLoopTimer 存在,CFRunLoopTimer 作为 RunLoop 的事件源之一,它的上层对应就是 NSTimer,NSTimer 的触发正是基于 RunLoop, 使用 NSTimer 之前必须注册到 RunLoop。一个NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 00:00, 00:02, 00:04,00:06 这几个时间点。RunLoop 为了节省资源,并不会在非常准确的时间点回调这个 NSTimer,NSTimer 有个属性叫做 Tolerance 表示回调 NSTimer 的时间点容许多少最大误差。

tolerance : The amount of time after the scheduled fire date that the timer may fire.

如果 RunLoop 执行了一个很长时间的任务,错过了某个时间点,则那个时间点的回调也会跳过去,不会延后执行。比如 00:02 这个时间点被错过了,RunLoop 不会 那么就只能等待下一个时间点 00:04 。

RunLoop 的触发时间准确性也与 RunLoop 的 mode 相关。

主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。这两个 Mode 都已经被标记为”Common”属性。DefaultMode 是 App 平时所处的状态,TrackingRunLoopMode 是追踪 ScrollView 滑动时的状态。当你创建一个 Timer 并加到 DefaultMode 时,Timer 会得到重复回调,但此时滑动一个TableView时,RunLoop 会将 mode 切换为 TrackingRunLoopMode,这时 Timer 就不会被回调,并且也不会影响到滑动操作。
有时你需要一个 Timer,在两个 Mode 中都能得到回调,一种办法就是将这个 Timer 分别加入这两个 Mode。还有一种方式,就是将 Timer 加入到顶层的 RunLoop 的 “commonModeItems” 中。”commonModeItems” 被 RunLoop 自动更新到所有具有”Common”属性的 Mode 里去。

// ......省略代码
- (void)viewDidLoad {
    [super viewDidLoad];
    self.timer =[NSTimer xx_scheduledTimerWithTimeInterval:1.0 repeats:YES block:^{
            NSLog(@"it is log!");
    }];
   
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}

- (void)dealloc
{
    [self.timer invalidate];
    NSLog(@"TimerViewController dealloc!");
}
// ......省略代码

参考

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

推荐阅读更多精彩内容

  • 之前要做一个发送短信验证码的倒计时功能,打算用NSTimer来实现,做的过程中发现坑还是有不少的。 基本使用 NS...
    WeiHing阅读 4,380评论 1 8
  • NSTimer使用方法 初始化+ (NSTimer)timerWithTimeInterval:(NSTimeIn...
    Gary_fei阅读 236评论 0 1
  • 记得自己刚接触nstimer时,以为就是个定时循环执行某方法的计时器,然而之后遇到过各种问题,最近发现问的最多的就...
    超_iOS阅读 5,165评论 5 5
  • NSTimer是iOS最常用的定时器工具之一,在使用的时候常常会遇到各种各样的问题,最常见的是内存泄漏,通常我们使...
    bomo阅读 1,203评论 0 7
  • 相同点:1.重视农民,重视教育;2.强调农民组织;3.重视知识分子的作用 不同点:对农村根本问题的认识上:梁认为是...
    边南阅读 3,128评论 2 4