NSTimer

定时器的用法

        系统提供了8个创建方法,6个类创建方法,2个实例初始化方法。有三个方法直接将timer添加到了当前runloop的NSDefaultRunLoopMode中,而不需要我们自己添加,当然这样的代价是runloop只能是当前runloop,模式是NSDefaultRunLoopMode。

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

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

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

        下面五种创建方法,不会自动添加到runloop,还需手动调用addTimer:forMode:

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

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

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

- (instancetype)initWithFireDate:(NSDate*)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(id)ui repeats:(BOOL)rep;

- (instancetype)initWithFireDate:(NSDate*)date interval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void(^)(NSTimer*timer))block;

NSTimer添加到NSRunLoop

        timer其实也是一种资源,所有的资源如果要起作用,就得加到runloop中去。同理timer这种资源要想起作用,那肯定也需要加到runloop中才会有效。如果一个runloop里面不包含任何资源的话,运行该runloop时会立马退出。

        我们都知道iOS是通过runloop作为消息循环机制,主线程默认启动了runloop,可是子线程没有默认的runloop,因此,我们在子线程启动定时器是不生效的。解决的方式也简单,在子线程启动一下runloop就可以了。

dispatch_async(dispatch_get_global_queue(0,0), ^{

NSTimer* timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(Timered:) userInfo:nil repeats:YES];       

[[NSRunLoopcurrentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];      

[[NSRunLoopcurrentRunLoop] run];  

  });

        timer是runloop的一个触发源,由于timer是添加到runloop中使用的,一个timer可以被添加到runloop的多个模式,比如在主线程中runloop一般处于NSDefaultRunLoopMode,但是,比如UIScrollView或者它的子类UITableView、UICollectionView等滑动时runloop处于UITrackingRunLoopMode模式下,因此如果你想让timer在滑动的时候也能够触发,就可以分别添加到这两个模式下。这时候就应该使用runloop的NSRunLoopCommonModes(等效于NSDefaultRunLoopMode和NSEventTrackingRunLoopMode的结合)模式,能够让定时器在runloop两种模式切换时也不受影响。

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; 

关于强引用的问题

        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.

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.

1. invalidate方法是唯一能从runloop中移除timer的方式,调用invalidate方法后,runloop会移除对timer的强引用。

2.timer的添加和timer的移除(invalidate)需要在同一个线程中,否则timer可能不能正确的移除,线程不能正确退出。

        在这里首先声明一下:不是所有的timer都会造成循环引用。就像不是所有的block都会造成循环引用一样。以下两种timer不会有循环引用:

1. 非repeat类型的。非repeat类型的timer在执行完后,会自动调用invalidate方法,因此不会出现循环引用。

2.block类型的,iOS 10之后才支持,因此对于还要支持老版本的app来说,这个API还不能满足所有需求。当然block内部的循环引用也要避免。

         再次声明:不是解决了循环引用,target就可以释放了,别忘了在持有timer的类dealloc的时候执行invalidate。

       在除了iOS10新增的三个block方法外,其他方法方法都会有target(一般为self)。

        因为runloop强引用了timer,

The receiver retains aTimer. To remove a timer from all run loop modes on which it is installed, send an invalidate message to the timer.

       而且timer有强引用了target(self)。

The object to which to send the message specified by aSelector when the timer fires. The timer maintains a strong reference to target until it (the timer) is invalidated.

        如果不释放timer,造成self也释放不了,导致内存泄漏。

        我们常说的timer会产生循环引用 其实就是由于timer会对self进行强引用造成的。由于timer对target强引用的特性,如果要避免控制器不释放的问题,需要在特定的时机调用timer 的 invalidate方法,也就是提前结束timer。在通常情况下,这种方式是可以解决问题的,虽然需要警惕页面退出之前有没有结束timer,但毕竟解决了问题不是。但是,日常项目中通常是多人协作,如果该timer是一个view的属性,而这个view又需要让别人使用,那timer什么时候结束呢?让调用者来管理timer的结束显然是不合理的。更好的方式还是应该在dealloc 方法中结束timer,这样调用者根本无须关注timer。  

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

@interface TimerViewController()

@property(nonatomic,weak)NSTimer*timer;

@end

@implementationTimerViewController

- (void)dealloc {      

  [self.timer invalidate];

  NSLog(@"dealloc");

}

- (void)viewDidLoad {   

 [superviewDidLoad];

NSTimer*timer = [NSTimer timerWithTimeInterval:1.0f target:selfselector:@selector(count) userInfo:nilrepeats:YES];    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

self.timer = timer

;}

- (void)count { 

NSLog(@"count"); 

}

@end

将self改成weakSelf

- (void)viewDidLoad { 

   [superviewDidLoad];          

  __weaktypeof(self) weakSelf =self;

NSTimer*timer = [NSTimer timerWithTimeInterval:1.0f target:weakSelf selector:@selector(count) userInfo:nilrepeats: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的方法也就行不通了。而且weak的timer,如果写成这样,还会导致崩溃,要手动加入runloop的。

 self.timer  = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(nslog:) userInfo:nil repeats:YES];

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

设置self为weakSelf

        用__weak修饰self为weakSelf与普通的self的区别就在于, 这时候weakSelf作为一个参数传入block或者别的实例变量, block或实例变量都不会持有他, 也就是self的引用计数不会加1 ,在一般情况下 这时候就可以打破循环引用, 但是timer的内部机制决定了它必须通过设置invalidate来停止计时并释放, 在此之前, timer会强引用target, 所以也就不存在timer释放weakSelf, 即循环引用还是存在。

方法:block方法,来自《Effective Objective-C》第52条:别忘了NSTimer会保留其目标对象,其实iOS10开始系统也提供了该方法,但是的兼容更低的版本。

- (void)viewDidLoad {   

 [superviewDidLoad];  

  __weakidweakSelf =self;

NSTimer* timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer*timer) {

NSLog(@"block %@",weakSelf);   

 }];

}

@implementation NSTimer(BlockTimer)

+ (NSTimer*)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats blockTimer:(void(^)(NSTimer*))block{

NSTimer* timer = [NSTimer scheduledTimerWithTimeInterval:interval target:self selector:@selector(timered:) userInfo:[blockcopy] repeats:repeats];

returntimer;

}

+ (void)timered:(NSTimer*)timer {

void(^block)(NSTimer*timer)  = timer.userInfo;    block(timer);

}

@end

解释:将强引用的target变成了NSTimer的类对象。类对象本身是单例的,是不会释放的,所以强引用也无所谓。执行的block通过userInfo传递给定时器的响应函数timered:。循环引用被打破的结果是:timer的使用者强引用timer。timer强引用NSTimer的类对象。timer的使用者在block中通过weak的形式使用,因此是被timer弱引用。

如果在是在view中的定时器, 可以重写removeFromSuperview

- (void)removeFromSuperview {   

  [super removeFromSuperview]; 

   [self.timer invalidate];

}

NSTimer的实时性

        NSTimer不是一个实时系统,因此不管是一次性的还是周期性的timer的实际触发事件的时间可能都会跟我们预想的会有出入。差距的大小跟当前我们程序的执行情况有关系,比如可能程序是多线程的,而你的timer只是添加在某一个线程的runloop的某一种指定的runloop mode中,由于多线程通常都是分时执行的,而且每次执行的mode也可能随着实际情况发生变化。如果timer当前所处的线程正在进行大数据处理(假设为一个大循环),timer本次执行会等到这个大数据处理完毕之后才会继续执行。

       这期间有可能会错过很多次timer的循环周期,但是timer并不会将前面错过的执行次数在后面都执行一遍,而是继续执行后面的循环,也就是在一个循环周期内只会执行一次循环。 无论循环延迟的多离谱,循环间隔都不会发生变化,在进行完大数据处理之后,有可能会立即执行一次timer循环,但是后面的循环间隔始终和第一次添加循环时的间隔相同。

后记

重复初始化造成的问题。

如果

- (void)viewDidLoad {

    [super viewDidLoad];

    _timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(nslog:) userInfo:nil repeats:YES];

    [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSDefaultRunLoopMode];

    _timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {

        NSLog(@"timer");

    }];

   [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSDefaultRunLoopMode];

}

- (void)nslog:(NSTimer*)timer{

    NSLog(@"timer");

}

或者

- (void)viewDidLoad {

    [super viewDidLoad];

   _timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {

        NSLog(@"timer");

  }];

  _timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(nslog:) userInfo:nil repeats:YES];

  }

- (void)nslog:(NSTimer*)timer{

    NSLog(@"timer");

}

        多次把_timer加入runloop,即使调用了invalidate,_timer释放了,self释放了,还是会输出,造成内存泄漏。

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

推荐阅读更多精彩内容