iOS之NSTimer坑点总结

前言

我负责努力,其余交给运气。

对于NSTimer,大家应该很熟悉,因为NSTimer的坑比较多,所以面试中也是常问问题,工作中就更不用说了。那么今天写这篇文章的起因,就是因为自己写代码的时候习惯把黄色警告解了,所以解完自己的,就看了一下小伙伴的代码,结果发现:

不知不觉走向坑

这个警告呢,大意是:说好的不能为空,结果你给我传空值?其实产生这个问题的原因,也是self.timer_timer的区别,self.的话会走set/get方法。有兴趣的可以自己查一下。

由此,引起了思考:因为我们可以在很多地方,看别人用的时候,都会告诉你,释放的时候先invalidate再附值nil。可是为什么呢?然后自己查了一下,发现好多坑点,一想,算了,干脆自己总结一下吧....

一、NSTimer的循环引用
1.1 NSTimer的销毁

首先,在demo中我push进了一个控制器,再这个控制器中实现如下代码:

#import "TimerViewController.h"

@interface TimerViewController ()

@property (nonatomic, weak) NSTimer* timer;

@property (nonatomic, assign) NSInteger number;

@end

@implementation TimerViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    self.number = 0;
    // 创建定时器
    NSTimer* timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(showTimer:) userInfo:nil repeats:YES];
    // 将定时器添加到runloop中,否则定时器不会启动
    [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    self.timer = timer;
}

- (void)showTimer: (NSTimer*)sender {
    _number ++;
    NSLog(@"timer run %ld",_number);
}

- (void)viewDidDisappear:(BOOL)animated{
    [super viewDidDisappear:animated];
    NSLog(@"viewDidDisappear run");
}

- (void)dealloc{
    NSLog(@"dealloc run");
    if(self.timer){
        [_timer invalidate];
        _timer = nil;
    }
}

@end

运行结果如下:

执行结果

当我pop退出当前控制器的时候,其实根本没有走dealloc方法,所以根本没有释放成功。(不得不深思,有多少人是这样写的?之前接手过一个项目,里面有一个NSTimer实现的弹幕,结果就是控制器退出的时候,NSTimer并没有释放成功,当时自己也是很懵逼...)这个问题,原因是因为强引用,官方文档中有这么一段描述:

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.

大概意思是:定时器运行时会与runloop一起。runloop会对定时器强引用,因此在将定时器添加到runloop之后,不必使自己对定时器强引用。(所以其实我们可以不用strong来修饰NSTimer

所以问题来了:runloop会自动强引用NSTimer,然而这不是结束,这仅仅只是开始,除了强引用NSTimer之外,还会对target产生强引用( [NSTimer timerWithTimeInterval: target: selector: userInfo: repeats: ]),一般target我们传的都是控制器(所以后面就默认是控制器了哈),所以系统会额外对控制器产生强引用(好奇的小伙伴可以自己打印一下NSTimer添加前后控制器的retainCount),所以只要NSTimer还在被强引用,那么控制器也跑不了...

1.2 invalidate

既然上面说到,NSTimerrunloop强引用,那么我们用的invalidate呢?官方文档

Stops the timer from ever firing again and requests its removal from its run loop.

个人理解是:停止定时器的运行,并从runloop中删除。
所以其实invalidate并不是释放了NSTimer,它只是停止了NSTimer的运行,然后解除了runloopNSTimer的强引用关系罢了,然后解除对控制器的强引用,使得控制器可以正常释放NSTimer

那么上面代码的运行结果就显而易见了:dealloc要等控制器销毁的时候才会调用,而NSTimer还没有invalidate,那么控制器一直被强引用,所以并不会走dealloc,结果不走dealloc就不会invalidate... (循环引用就出现了)

所以我们可以把invalidate放在viewDidDisappear中,或者pop的时候调用invalidate,而NSTimer置空,实际上只是有的人认为这样更保险一些,因为invalidate毕竟不是释放对象;所以其实只要你能了解并控制好NSTimer的释放,invalidate后置空并不是一定要使用,只是因为大家现在已经把它用成了规范和习惯,但是不用也不代表错。

需要注意的是:

  • You must send this message from the thread on which the timer was installed. If you send this message from another thread, the input source associated with the timer may not be removed from its run loop, which could prevent the thread from exiting properly.
    必须从添加定时器的线程中调用invalidate,否则可能不能正常从runloop中删除对象,从而阻塞线程正常退出。
  • Do not subclass NSTimer.
    不要将NSTimer子类化。(所以一些办法是建立一个桥接类,个人觉得比较麻烦)
二、子线程启动定时器问题

我们知道NSTimer的运行一定要有runloop的存在。我们也都知道iOS是通过runloop作为消息循环机制,主线程默认启动了runloop,可是子线程没有默认的runloop,因此,我们在子线程启动定时器是不生效的。

解决办法也简单,在子线程启动一下runloop就可以了。

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSTimer* timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(showTimer:) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
        [[NSRunLoop currentRunLoop] run];
    });
三、runloop的mode问题

runloop有五种mode:

1. kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
2. UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
3. UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用,会切换到kCFRunLoopDefaultMode
4. GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
5. kCFRunLoopCommonModes: 这是一个占位用的Mode,作为标记kCFRunLoopDefaultMode和UITrackingRunLoopMode用,并不是一种真正的Mode 

如上,滑动ScrollView时,runloop会切换mode,然而我们创建NSTimer的时候,如果不手动添加mode,那么默认是KCFRunLoopDefaultMode,所以当滑动ScrollView时切换到UITrackingRunLoopModeNSTimer就失效了,要等到滑动结束,mode切换回来的时候才会继续执行。

解决办法是手动设置modekCFRunLoopCommonModes
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

总结

文字描述很多(语文又不好),希望耐心看下来的小伙伴会有一些收货,哈哈。

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