NSTimer 常见疑问

这篇文章不科普原理,原理网上随便搜,这里只聊常见的疑问。

  • runloop与内存泄漏
  • target方式循环引用
  • block方式循环引用
  • 其它定时器
run loop与内存泄漏

1、子线程使用NSTimer需要注意什么?

子线程需要创建run loop,model设为default,NSTimer添加到run loop中,否则不会运行。NSTimer的Action更新UI注意回到主线程中。

2、NSTimer不添加到run loop可以运行吗?

可以,但是没有实际意义。

dispatch_async(dispatch_get_global_queue(0, 0), ^{
     NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
            NSLog(@"run without run loop");
        }];
     [timer fire];
});

通过fire方法可以手动触发NSTimer的任务,但是TimeInterval和repeats的设置都没作用,相当于直接写这样的代码:

NSLog(@"run without run loop");

在实际应用场景中意义不大。

3、NStimer的TimeInterval为什么不精准?

一方面是因为线程阻塞的时候run loop会在下一次run loop循环中fire;一方面是因为NSTimer本身有Tolerance,允许出现容忍误差,即使Tolerance设置为0,系统还是会给NSTimer设置Tolerance不为0的值。

4、invalidate的实际作用是什么?

  • 作用是移除run loop对NSTimer对象的强引用,移除NSTimer对target和userinfo的强引用。所以invalidate避免了run loop不释放NSTimer对象的问题从而避免内存泄漏,同时打破了和self可能存在的循环引用关系从而避免内存泄漏。
  • invalidate必须调用,这是唯一在run loop中移除NStimer对象的方法。repeats为NO会自动调用,为YES需要自己手动调用。

5、invalidate不在NSTimer对象注册的线程执行会怎样?

移除NSTimer对target和userinfo的强引用,可能不会移除run loop对NSTimer对象的强引用从而导致内存泄漏,注意是可能

target方式循环引用

1、target设置为weak引用是否可以打破循环引用?

不可以,target不支持weak引用。

C++代码可以看到这一点:

/var/folders/27/wz97zmvn6fx_pw_kzbbrrm580000gn/T/BViewController-32f70f.mi:60696:20: error: 
cannot create __weak reference because the current deployment target does not support weak references
__attribute__((objc_ownership(weak))) typeof(self) weakself = self;

OC代码没报错是因为weak指针指向对象指针,顺着这条路,target依然可以强引用NSTimer对象。

2、怎么打破target方式产生的循环引用?

  • dealloc之前在适当的地方invalidate
  • 继承NSProxy,在NSProxy子类中weak引用self,并实现消息转发self,NSTimer的target设置为NSProxy子类的对象,最后别忘了dealloc中invalidate。不然虽然解决了循环引用,但是run loop不释放NSTimer对象还是会内存泄漏。

3、UIControl和NSTimer的Target-Action有什么不同?

  • UIControl的target在内部使用了weak引用,所以不会有循环引用问题。
  • NStimer的target在内部如果也使用weak引用会造成比循环引用更难以发现的内存泄漏问题。NSTimer的触发是通过run loop向target发消息,如果target是weak引用,那么target释放后,weak 指针对自动置为nil,向nil发消息不会报错,但也不会执行action,这就给你造成了一个错觉,NSTimer对象似乎随着target的释放也释放了,然而NSTimer对象其实还被run loop持有者永远不会释放,也就造成了内存泄漏。所以我们可以理解为NSTimer的循环引用问题是苹果故意给我们设计的,苹果希望我们可以通过invalidate方法显示的打破循环引用,同时让run loop释放NSTimer对象。
  • iOS 10之后,NSTimer给我们提供了Block的方法,这个方法不存在target-action的循环引用问题,block实现不故意设计成循环引用的原因在于Block的持有者是NSTimer对象本身,只要run loop没释放NSTimer对象,block就可以一直执行。所以即使NSTimer的持有者被释放了,我们依然可以显示的看到定时器还在运行着,从而知道NSTimer对象还没有释放,发现内存泄漏问题。
block方式循环引用

在block中weak引用self。注意invalidate,invalidate在block中或者在dealloc中都可以,否则run loop不释放NSTimer对象还是会内存泄漏。

1、低于iOS 10.0的版本,NSTimer没有block的创建方法怎么办?

添加NSTimer的分类,分类中定义block方式的创建方法(类方法),创建方法内部实现用target方式,target设置为NSTimer类对象。
block内部weak引用self:
self->timer->NSTimer类对象->weakself

  • 如果不weak引用self,即使在dealloc之前invalidate打破timer->NSTimer类对象进而打破循环引用,但是由于NSTimer类对象不会释放,进而导致self也不会释放。
  • weak引用self,不会循环引用,invalidate在block中或者在dealloc中都可以。如果不调用invalidate,run loop不释放NSTimer对象还是会内存泄漏。
其它定时器

1、GCD定时器
2、CADisplayLink
这两个以后有时间详细写一下。

最后提供一个OC版的定时器库吧。
https://github.com/clarkIsMe/CYTimer.git

提供了6个类方法,包含了NSTimer、CADisplayLink、GCD定时器的Target-Action调用方式和Block调用方式。
内部解决了内存泄漏的问题,使用这个6个类方法去创建定时器,可以完全忽略定时器给我们带来的坑,让我们更加专注在业务开发上。
同时提供了APP进入后台,进入前台,以及当前控制器生命周期的AOP回调,让我们在写相关场景的业务时代码不再到处飞了。

后期会更新哦,有兴趣的小伙伴可以关注下,准备加上一些自带定时器的控件,定时器与控件的生命周期绑定,完全不再操心定时器的任何问题。

/// NSTimer的Target-Action实现方式,自动检测runloop并添加,内部解决了循环引用问题,内部自动调用了invalidate,避免内存泄漏
/// @param ti 调用间隔,单位 s
/// @param aTarget 目标对象
/// @param aSelector 回调方法
/// @param userInfo 传参
/// @param yesOrNo 是否重复执行

+ (CYTimer *)scheduledNormalTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo API_AVAILABLE(ios(8.0));


/// NSTimer的Block实现方式,自动检测runloop并添加,内部解决了循环引用问题,内部自动调用了invalidate,避免内存泄漏
/// @param interval 调用间隔,单位 s
/// @param aTarget CYTimer的生命周期与aTarget绑定,aTarget不建议使用weak 引用,虽然weak引用不会导致任何问题。
/// @param repeats 是否重复执行
/// @param block 回调

+ (CYTimer *)scheduledNormalTimerWithTimeInterval:(NSTimeInterval)interval bindTo:(id)aTarget repeats:(BOOL)repeats block:(void (^)(CYTimer *timer))block API_AVAILABLE(ios(8.0));


/// CADisplayLink的Target-Action实现方式,自动检测runloop并添加,内部解决了循环引用问题,内部自动调用了invalidate,避免内存泄漏
/// @param ti 调用间隔,单位 s
/// @param aTarget 目标对象
/// @param aSelector 回调方法

+ (CYTimer *)scheduledFPSTimerWithFrameInterval:(NSUInteger)ti target:(id)aTarget selector:(SEL)aSelector API_AVAILABLE(ios(8.0));


/// CADisplayLink的Block实现方式,自动检测runloop并添加,内部解决了循环引用问题,内部自动调用了invalidate,避免内存泄漏
/// @param interval  iOS 10以后该参数代表每秒执行的次数,0 为代表每一帧都调用;iOS 10以前每 frameInterval  帧的调用一次,1 为每一帧都调用,不可以小于1
/// @param aTarget CYTimer的生命周期与aTarget绑定,aTarget不建议使用weak 引用,虽然weak引用不会导致任何问题。
/// @param block 回调

+ (CYTimer *)scheduledFPSTimerWithFrameInterval:(NSUInteger)interval bindTo:(id)aTarget block:(void (^)(CYTimer *timer))block API_AVAILABLE(ios(8.0));


/// GCD定时器的Target-Action实现方式,自动检测runloop并添加,内部解决了循环引用问题,内部自动调用了invalidate,避免内存泄漏
/// @param ti  iOS 10以后该参数代表每秒执行的次数,0 为代表每一帧都调用;iOS 10以前每 frameInterval  帧的调用一次,1 为每一帧都调用,不可以小于1
/// @param aTarget 目标对象
/// @param aSelector 回调方法

+ (CYTimer *)scheduledGCDTimerWithTimeInterval:(NSUInteger)ti target:(id)aTarget selector:(SEL)aSelector API_AVAILABLE(ios(8.0));


/// GCD定时器的Block实现方式,自动检测runloop并添加,内部解决了循环引用问题,内部自动调用了invalidate,避免内存泄漏
/// @param interval 调用间隔,单位 s
/// @param aTarget CYTimer的生命周期与aTarget绑定,aTarget不建议使用weak 引用,虽然weak引用不会导致任何问题。
/// @param block 回调

+ (CYTimer *)scheduledGCDTimerWithTimeInterval:(NSUInteger)interval bindTo:(id)aTarget block:(void (^)(CYTimer *timer))block API_AVAILABLE(ios(8.0));

做个这个组件的初衷:
1、提供干净的定时器调用方式,不用考虑循环引用、内存泄漏等等问题,不用时刻想着销毁定时器,让我们更加专注在业务上。
2、提供不同原理实现的定时器来更好的适应业务场景。
3、提供适当的AOP。

需要知道的地方:
1、如果你声明了CYTimer类型的成员变量,然后直接调用CYTimer的类方法去执行任务,没有用 = 给成员变量赋值,那么这个赋值过程会自动发生;如果CYTimer类型的成员变量个数超过一个,这个自动赋值的过程就不会发生了。
2、如果你采用CYTimer的Block方式调用,那么你仍然要注意Block内部弱引用self,这个组件是解决定时器的问题,不是block。
3、除了block内部你自己写的代码里注意循环引用,其它地方你将不再需要关心self是否需要弱引用,怎么样都可以
4、dealloc里不要求调用 invalidate 方法,当然你要调用也可以。
5、使用normal定时器,你不用关心runloop是否会释放NSTimer,这个释放过程是自动发生的。
6、normal和FPS的定时器都是在当前线程的runloop中,模式是 NSRunLoopCommonModes,如果你需要自己灵活设置模式,请告诉我。
7、block回调已经自动切回了主线程,你没必要在自己的block代码再切一次。

使用建议:
1、非动画类推荐使用用GCD的方法。
2、动画类的推荐使用FPS的方法。
3、如果你偏爱用NSTimer,那你也可以选择normal的方法,而且让你使用中不再有坑。但是它不准呀大兄弟,为啥你非得用。

如果有大神对我这篇文章的某些地方有不同看法,请一定指出,最后感谢您花费宝贵的时间看完这篇文章。

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