NSTimer那些事

定时器:

  • 需要被添加到Runloop,否则不会运行,当然添加的Runloop不存在也不会运行
  • 还要指定添加到的Runloop的哪个模式,而且还可以指定添加到Runloop的多个模式,模式不对也是不会运行的
  • Runloop会对timer有强引用,timer会对目标对象进行强引用(是否隐约的感觉到坑了。。。)
  • timer的执行时间并不准确,系统繁忙的话,还会被跳过去
  • invalidate调用后,timer停止运行后,就一定能从Runloop中消除吗?

定时器的一般用法

- (void)viewDidLoad {
    NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1 target:self selector:@selector(timerFire) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    self.timer = timer;
}

- (void)timerFire {
    NSLog(@"timer fire");
}

上面的代码就是我们使用定时器最常用的方式,可以总结为2个步骤:创建,添加到runloop

  • scheduled开头的方法会自动将timer添加到当前Runloopdefault mode并马上启动
  • timer开头的方法需要自己添加到Runloop并手动开启

方法参数:

  • ti(interval):定时器触发间隔时间,单位为秒,可以是小数。如果值小于等于0.0的话,系统会默认赋值0.1毫秒
  • invocation:这种形式用的比较少,大部分都是block和aSelector的形式
  • yesOrNo(rep):是否重复,如果是YES则重复触发,直到调用invalidate方法;如果是NO,则只触发一次就自动调用invalidate方法
  • aTarget(t):发送消息的目标,timer会强引用aTarget,直到调用invalidate方法
  • aSelector(s):将要发送给aTarget的消息,如果带有参数则应:- (void)timerFireMethod:(NSTimer *)timer声明
  • userInfo(ui):传递的用户信息。使用的话,首先aSelector须带有参数的声明,然后可以通过[timer userInfo]获取,也可以为nil,那么[timer userInfo]就为空
  • date:触发的时间,一般情况下我们都写[NSDate date],这样的话定时器会立马触发一次,并且以此时间为基准。如果没有此参数的方法,则都是以当前时间为基准,第一次触发时间是当前时间加上时间间隔ti
  • block:timer触发的时候会执行这个操作,带有一个参数,无返回值

添加到runloop,参数timer是不能为空的,否则抛出异常
- (void)addTimer:(NSTimer *)timer forMode:(NSRunLoopMode)mode
另外,系统提供了一个- (void)fire方法,调用它可以触发一次:

NSTimer添加到NSRunLoop

timer必须添加到Runloop才有效,很明显要保证两件事情,一是Runloop存在(运行),另一个才是添加。确保这两个前提后,还有Runloop模式的问题。

一个timer可以被添加到Runloop的多个模式,比如在主线程中runloop一般处于NSDefaultRunLoopMode,而当滑动屏幕的时候,比如UIScrollView或者它的子类UITableView、UICollectionView等滑动时Runloop处于UITrackingRunLoopMode模式下,因此如果你想让timer在滑动的时候也能够触发,就可以分别添加到这两个模式下。或者直接用NSRunLoopCommonModes

但是一个timer只能添加到一个Runloop(Runloop与线程一一对应关系,也就是说一个timer只能添加到一个线程)。如果你非要添加到多个Runloop,则只有一个有效

关于强引用的问题

一般我们使用NSTimer如下图所示

- (void)viewDidLoad {
    // 代码1
    NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1 target:self selector:@selector(timerFire) userInfo:nil repeats:YES];
    // 代码2  上文中提到有些初始化方法会自动添加Runloop 这里是主动调用 效果都一样
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; 
    // 代码3
    self.timer = timer;
}

- (void)timerFire {
    NSLog(@"timer fire");
}

假设代码中self(viewController)UINavgationController管理 并且timer与self之间为强引用

timer和vc(self)Runloop之间的关系图

如图所示 分析一下4根线的由来

  • L1:nav push 控制器的时候会强引用,即在push的时候产生;
  • L2:是在代码3的位置产生
  • L3:是在代码1的位置产生,至此L2与L3已经产生了循环引用,虽然timer还没有添加到Runloop
  • L4:是在代码2的位置产生
    我们常说的NSTimer会产生循环引用 其实就是由于timer会对self进行强引用造成的。

打破循环引用

解决循环引用,首先想到的方法就是让self对timer为弱引用weak或者time对target如self替换为weakSelf 然而这真的有用吗?

例:

@interface TimerViewController ()

@property (nonatomic, weak) NSTimer *timer;

@end

@implementation TimerViewController

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

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSTimer *timer = [NSTimer timerWithTimeInterval:1.0f target:self selector:@selector(count) userInfo:nil repeats:YES];

    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    
    self.timer = timer;
}

- (void)count {
     NSLog(@"count");
}
@end

从上一个vc push进来之后, 定时器开始启动 控制台打印count, 然后pop回去, TimerViewController并没有走dealloc方法 控制台依然打印count。

将self改成weakSelf效果依然一样

- (void)viewDidLoad {
    [super viewDidLoad];
    
    
    __weak typeof(self) weakSelf = self;
    NSTimer *timer = [NSTimer timerWithTimeInterval:1.0f target:weakSelf selector:@selector(count) userInfo:nil repeats:YES];

    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    
    self.timer = timer;
}

分析:

设置timer为weak

我们想通过self对timer的弱引用, 在self中的dealloc方法中让timer失效来达到相互释放的目的。但是, timer内部本身对于self有一个强引用。并且timer如果不调用invalidate方法,就会一直存在,所以就导致了self根本释放不了, 进而我们想通过在dealloc中设置timer失效来释放timer的方法也就行不通了。

设置self为weakSelf

__weak修饰self为weakSelf, weakSelf作为一个局部变量在一般情况下,这时候就可以打破循环引用。 但是timer还是对weakSelf有强引用,所以weakSelf一直都在,即还是释放不了。self和weakSelf在这里的区别仅仅在于,如果self释放了,weakSelf会置空。

(别人对于weakstrong的解释 我觉得挺好的 多读几遍会有更多了解)

1.(weakstrong)不同的是:当一个对象不再有strong类型的指针指向它的时候,它就会被释放,即使该对象还有_weak类型的指针指向它;
2.一旦最后一个指向该对象的strong类型的指针离开,这个对象将被释放,如果这个时候还有weak指针指向该对象,则会清除掉所有剩余的weak指针

解决办法

总结: 要想解决循环引用的问题, 关键在于让timer失效即调用[timer invalidate]方法而不是各种weak。

  • 外部调用方法触发invalidate
- (void)stopTimer {
    [self.timer invalidate];
}
  • 如果在是在view中的定时器, 可以重写removeFromSuperview
- (void)removeFromSuperview {
    [super removeFromSuperview];
    [self.timer invalidate];
}
  • 将timer的target设为一个中间类
@interface BreakTimeLoop ()

// 这里必须用weak 不然释放不了
@property (nonatomic, weak) id owner;

@end


@implementation BreakTimeLoop

- (void)dealloc {
     NSLog(@"BreakTimeLoop dealloc");
}

- (instancetype)initWithOwner:(id)owner {
    if (self = [super init]) {
        self.owner = owner;
    }
    return self;
}

- (void)doSomething:(NSTimer *)timer {
    // 需要参数可以通过 timer.userInfo 获取
    [self.owner performSelector:@selector(count)];
}

在vc中

@interface TimerViewController ()

/** timer在这种情况下也可用strong **/
@property (nonatomic, strong) NSTimer *timer;
/** 中间类 这里用strong和weak无所谓 主要是breaker中对owner的引用为weak就行 **/
@property (nonatomic, strong) BreakTimeLoop *breaker;

@end

@implementation TimerViewController

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

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.breaker = [[BreakTimeLoop alloc] initWithOwner:self];
    // 这里的doSomething: 如果在.h中没有声明会报警告 不过不影响实现
    self.timer = [NSTimer timerWithTimeInterval:1.0f target:self.breaker selector:@selector(doSomething:) userInfo:nil repeats:YES];

    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}

- (void)count {
     NSLog(@"count");
}
@end

控制台打印:


  • NSTimer写一个分类
@interface NSTimer (SLBreakTimer)

+ (NSTimer *)sl_scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)())block;

@end

@implementation NSTimer (SLBreakTimer)

+ (NSTimer *)sl_scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)())block {
    
    // copy将block放入堆上防止提前释放
    return [NSTimer scheduledTimerWithTimeInterval:interval target:self selector:@selector(sl_block:) userInfo:[block copy] repeats:repeats];
}

+ (void)sl_block:(NSTimer *)timer {
    void (^block)() = timer.userInfo;
    if (block) {
        block();
    }
}
@end

在vc中

- (void)viewDidLoad {
    [super viewDidLoad];
    
    
    // 为weak防止循环引用
    __weak typeof(self) weakSelf = self;
    self.timer = [NSTimer sl_scheduledTimerWithTimeInterval:1.0f repeats:YES block:^{
        // 防止self提前释放
        __strong typeof(weakSelf) strongSelf = weakSelf;
        [strongSelf count];
    }];
   
}

控制台打印为:

  • 采用block方式(iOS10之后才有)
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
invalidate方法有啥用

在上文中, 我们所有做的一些都是围绕invalidate方法来做的。那么这个方法到底有什么用呢?

  • 将timer从Runloop中移除
  • 释放他自身持有的资源 比如target userInfo block

注意:

  1. 如果timer的引用为weak,在调用了invalidate之后,timer会被释放(ARC会置空)如果在这之后还想用timer必须重新创建,所以 我们添加timer进Runloop之前可以通过timer的isValid方法判断是否可用.
  2. 如果invalidate的调用不在添加Runloop的线程,那么timer虽然会释放他持有的资源,但是它本身不会被释放,他所在的Runloop也不会被释放,也会导致内存泄漏。
timer是否准时

不准时

  • 第一种不准时:有可能跳过去
  1. 线程在处理耗时的事情时会发生。
  2. 还有就是timer添加到的Runloop模式不是Runloop当前运行的模式,这种情况经常发生。

对于第一种情况我们不应该在timer上下功夫,而是应该避免这个耗时的工作。那么第二种情况,作为开发者这也是最应该去关注的地方,要留意,然后视情况而定是否将timer添加到Runloop多个模式。
虽然跳过去,但是,接下来的执行不会依据被延迟的时间加上间隔时间,而是根据之前的时间来执行。比如:
定时时间间隔为2秒,t1秒添加成功,那么会在t2、t4、t6、t8、t10秒注册好事件,并在这些时间触发。假设第3秒时,执行了一个超时操作耗费了5.5秒,则触发时间是:t2、t8.5、t10,第4和第6秒就被跳过去了,虽然在t8.5秒触发了一次,但是下一次触发时间是t10,而不是t10.5

  • 第二种不准时:不准点

比如上面说的t2、t4、t6、t8、t10,并不会在准确的时间触发,而是会延迟个很小的时间,原因也可以归结为2点:

  1. RunLoop为了节省资源,并不会在非常准确的时间点触发
  2. 线程有耗时操作,或者其它线程有耗时操作也会影响

iOS7以后,timer 有个属性叫做 Tolerance(时间宽容度,默认是0),标示了当时间点到后,容许有多少最大误差。
它只会在准确的触发时间到加上Tolerance时间内触发,而不会提前触发(是不是有点像我们的火车,只会晚点。。。)。另外可重复定时器的触发时间点不受Tolerance影响,即类似上面说的t8.5触发后,下一个点不会是t10.5,而是t10 + Tolerance

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

推荐阅读更多精彩内容

  • 引言 定时器:A timer waits until a certain time interval has el...
    时间已静止阅读 2,794评论 6 34
  • NSTimer是iOS最常用的定时器工具之一,在使用的时候常常会遇到各种各样的问题,最常见的是内存泄漏,通常我们使...
    bomo阅读 1,177评论 0 7
  • 之前要做一个发送短信验证码的倒计时功能,打算用NSTimer来实现,做的过程中发现坑还是有不少的。 基本使用 NS...
    WeiHing阅读 4,377评论 1 8
  • 洛带着稚在租屋周围逛了一大圈,洛走路时有点颠儿颠儿的,看上去是在想什么高兴的事情,她平时穿高跟鞋走路可不是这样。 ...
    江户川糯米乱步阅读 151评论 0 0
  • 今天,我想聊聊最近办号遇到的事儿。感觉自己做的挺带劲的。任何事情你不做,你永远不知道你会遇到些什么事。做公众号最开...
    thanksgi阅读 318评论 0 2