NSTimer用法与循环引用

首先介绍NSTimer的几种创建方式

- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(nullable id)ui repeats:(BOOL)rep
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;

常用方法

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

三种方法的区别是:

  • scheduledTimerWithTimeInterval方法不仅创建了NSTimer对象,还把该NSTimer对象加入到了当前的RunLoop(默认NSDefaultRunLoopModel模式)中。
  • 前两个方法需要使用addTimer:forMode:方法将NSTimer加入到RunLoop中。
- (void)addTimer:(NSTimer *)timer forMode:(NSRunLoopMode)mode;

与UIScrollView使用时注意事项

在当前线程为主线程时,某些UI事件,比如UIScrollView的拖动操作,会将RunLoop切换为NSEventTrackingRunLoopModel模式,在这个过程中,默认的NSDefaultRunLoopModel模式中注册的事件是不会被执行的。
这时可以将Timer按照NSRunLoopCommonModes模式加入到RunLoop中。
通常情况下NSDefaultRunLoopMode和UITrackingRunLoopMode都已经被加入到了common modes集合中, 所以不论runloop运行在哪种mode下, NSTimer都会被及时触发

如何销毁NSTimer

invalidate方法的官方介绍:

Stops the timer from ever firing again and requests its removal from its run loop.
This method is the only way to remove a timer from an NSRunLoopobject. The NSRunLoop
object removes its strong reference to the timer, either just before the invalidate method returns or at some later point.
If it was configured with target and user info objects, the receiver removes its strong references to those objects as well.

意思是:

  • invalidate方法会停止计时器的再次触发,并在RunLoop中将其移除。
  • invalidate方法是将NSTimer对象从RunLoop中移除的唯一方法。
  • 调用invalidate方法会删除RunLoop对NSTimer的强引用,以及NSTimer对target和userInfo的强引用!

那为什么RunLoop会对NSTimer强引用呢?

Timers work in conjunction with run loops. Run loops maintain strong references to their timers
( 计时器与运行循环一起工作。运行循环维护对计时器的强引用)

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.
(当计时器触发后,在调用invalidated之前会一直保持对target的强引用)

以上也解释了下面要说的NSTimer造成循环引用的原因

循环引用造成内存泄漏

循环引用示例.png

由上可见:NSTimer强引用了self,self也强引用了NSTimer,由此造成了循环引用,同时Runloop也强引用NSTimer。

  • 下面介绍两种情况下解决循环引用
  1. 一般情况下直接在vc的viewWillDisappear中调用以下方法即可解决
- (void)viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];
    [self.timer invalidate];
    self.timer = nil;
}
  1. 在A--push--B,B返回A

这种情况显然是在dealloc中调用invalidate方法,
有些人可能会想将NSTimer弱引用

@property (nonatomic, weak)NSTimer *timer;
  • 但是RunLoop强引用了timer ~,timer强引用了vc,所以dealloc不会被调用!
  • 或者target传入weakSelf,由于在invalidate方法调用之前,timer一直强引用target,而强引用了弱引用所引用的对象,等价于强引用!

下面介绍几种成熟的解决方案

一. 使用自定义Category用Block解决
NSTimer+ZHWeakTimer.h

@interface NSTimer (ZHWeakTimer)
+ (NSTimer *)zh_scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval block:(void (^)(void))eventBlock repeats:(BOOL)repeats;
@end
NSTimer+ZHWeakTimer.m

@implementation NSTimer (ZHWeakTimer)

+ (NSTimer *)zh_scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval block:(void (^)(void))eventBlock repeats:(BOOL)repeats
{
    NSTimer *timer = [self scheduledTimerWithTimeInterval:timeInterval target:self selector:@selector(zh_executeTimer:) userInfo:[eventBlock copy] repeats:repeats];
    
    return timer;
}

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

@end

定时器对象指定的target是NSTimer类对象是个单例,因此计时器是否会保留它都无所谓。这么做,循环引用依然存在,但是因为类对象无需回收,所以能解决问题。

优点:代码简洁,逻辑清晰
缺点:
1.需要使用weakSelf避免block循环引用
2.不再使用原生API
3.同时要为NSTimer何CADisplayLink分别引进一个Category

二. GCD自己实现Timer

直接用GCD自己实现一个定时器,YYKit直接有一个现成的类YYTimer这里不再赘述。
缺点:代价有点大,需要自己重新造一个定时器。

三. 代理NSProxy

使用工具类YYWeakProxy解决NSTimer/CADisplayLink循环引用问题!

YYWeakProxy.h
@interface YYWeakProxy : NSProxy
@property (nonatomic, weak, readonly) id target;
-(instancetype)initWithTarget:(id)target;
+(instancetype)proxyWithTarget:(id)target;
@end
YYWeakProxy.m
-(instancetype)initWithTarget:(id)target {
 _target = target;
 return self;
}
+(instancetype)proxyWithTarget:(id)target {
 return [[YYWeakProxy alloc] initWithTarget:target];
}
-(id)forwardingTargetForSelector:(SEL)selector {
 return _target;
}
-(void)forwardInvocation:(NSInvocation *)invocation {
 void *null = NULL;
 [invocation setReturnValue:&null];
}
-(NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
 return [NSObject instanceMethodSignatureForSelector:@selector(init)];
}
-(BOOL)respondsToSelector:(SEL)aSelector {
 return [_target respondsToSelector:aSelector];
}
-(BOOL)isEqual:(id)object {
 return [_target isEqual:object];
}
-(NSUInteger)hash {
 return [_target hash];
}
-(Class)superclass {
 return [_target superclass];
}
-(Class)class {
 return [_target class];
}
-(BOOL)isKindOfClass:(Class)aClass {
 return [_target isKindOfClass:aClass];
}
-(BOOL)isMemberOfClass:(Class)aClass {
 return [_target isMemberOfClass:aClass];
}
-(BOOL)conformsToProtocol:(Protocol *)aProtocol {
 return [_target conformsToProtocol:aProtocol];
}
-(BOOL)isProxy {
 return YES;
}
-(NSString *)description {
 return [_target description];
}
-(NSString *)debugDescription {
 return [_target debugDescription];
}
@end

该方法引入一个YYWeakProxy对象,在这个对象中弱引用真正的目标对象。通过YYWeakProxy对象,将NSTimer/CADisplayLink对象弱引用目标对象。
使用方法:

 self.timer = [NSTimer scheduledTimerWithTimeInterval:1
                                                  target:[YYWeakProxy proxyWithTarget:self]
                                                selector:@selector(timeEvent)
                                                userInfo:nil
                                                 repeats:YES];
- (void)timeEvent{
}
- (void)dealloc
{
    [self.timer invalidate];
    self.timer = nil;// 对象置nil是一种规范和习惯
}
为什么NSProxy的子类YYWeakProxy可以解决呢?
  • NSProxy本身是一个抽象类,它遵循NSObject协议,提供了消息转发的通用接口,NSProxy通常用来实现消息转发机制和惰性初始化资源。不能直接使用NSProxy。需要创建NSProxy的子类,并实现init以及消息转发的相关方法,才可以用。
  • YYWeakProxy继承了NSProxy,定义了一个弱引用的target对象,通过重写消息转发等关键方法,让target对象去处理接收到的消息。在整个引用链中,Controller对象强引用NSTimer/CADisplayLink对象,NSTimer/CADisplayLink对象强引用YYWeakProxy对象,而YYWeakProxy对象弱引用Controller对象,所以在YYWeakProxy对象的作用下,Controller对象和NSTimer/CADisplayLink对象之间并没有相互持有,完美解决循环引用的问题。

参考文档

1.iOS实录8:解决NSTimer/CADisplayLink的循环引用
2.NSTimer Class

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

推荐阅读更多精彩内容