关于NSTimer的循环引用问题

2020-04-20  本文已影响0人  Z_Lukas

关于NSTimer的介绍

1、最近在总结知识、好久没记录了,今天抽空记录下NSTimer的使用,首先我们先看下App官方API文档中解释的NSTimer

### Summary
A timer that fires after a certain time interval has elapsed, sending a specified message to a target object.
### Discussion
第一段:
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. 

首先看这段的解释,NSTimer的工作依赖NSRunLoop,并且RunLoop保持强持有当前的Timer, 这个时候当你把Timer添加到当前的RunLoop后,你当前的Timer就被Run Loop强持有了

To use a timer effectively, you should be aware of how run loops operate. See [Threading Programming Guide](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/Introduction/Introduction.html#//apple_ref/doc/uid/10000057i) for more information.

A timer is not a real-time mechanism. If a timer’s firing time occurs during a long run loop callout or while the run loop is in a mode that isn't monitoring the timer, the timer doesn't fire until the next time the run loop checks the timer. Therefore, the actual time at which a timer fires can be significantly later. See also [Timer Tolerance](applewebdata://43EE1961-4719-4FAB-9500-13E376222651#1667624).

[NSTimer](apple-reference-documentation://hcaODPrPmP) is toll-free bridged with its Core Foundation counterpart, [CFRunLoopTimerRef](apple-reference-documentation://hcPs68Qze5).

这段讲述了Timer并不是十分精确的,他需要依赖runloop的处理事件循环周期,所以有一个容忍的公差度

NSTimer是如何造成的当前VC无法释放的

1587377415988.jpg

如上图所示,当你使用下列方式Target-Action初始话的时候,造成的当前self无法释放,原因当前的NSRunLoop强持有的Timer,而timer又被self强持有,所以Timer无法释放,self也无法释放,

self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(selector) userInfo:id repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];

如何解决上边这种方法定义的Timer呢?就是我需要调用的【self.timer invalidate】方法,下面我们看下官方文档给出的解释


1587377506737.jpg

特别是图中的标出来的这句,意思是当你调用invalidate的时候,会打破runLoop强引用Timer,也同时打破Taget对Timer的强持有,这个时候 在看(1587377415988.jpg)此图剩余self对timer单向强持有了,所谓的循环引用也就被打破了,所以打破的重重之重是需要调用invalidate方法。

总结:

当我们使用初始化方式是Target-Action这种方式的初始化确实是存在循环引用的也就是下列的方法


需要代码添加到当前的RunLoop中
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
系统自己给你添加到当前的RunLoop中并且是Defalut Mode模式 参考API的注释
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
需要代码添加到当前的RunLoop中
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
系统自己给你添加到当前的RunLoop中并且是Defalut Mode模式 参考API的注释
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
第二种初始化 使用了带有Block的事件回调函数进行的处理,初始化函数,可能苹果意思到定时器存在的循环引用的缺陷,在iOS 10系统以后使用了这种Block回调的 就不存在循环引用了,此时也可以再dealloc函数中调用 invalidate 函数就不会存Target-Action 这种形式初始化的定时器的无法调用的问题了
+ (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));

/// Initializes a new NSTimer object using the block as the main body of execution for the timer. This timer needs to be scheduled on a run loop (via -[NSRunLoop addTimer:]) before it will fire.
/// - parameter:  fireDate   The time at which the timer should first fire.
/// - parameter:  interval  The number of seconds between firings of the timer. If seconds is less than or equal to 0.0, this method chooses the nonnegative value of 0.1 milliseconds instead
/// - parameter:  repeats  If YES, the timer will repeatedly reschedule itself until invalidated. If NO, the timer will be invalidated after it fires.
/// - parameter:  block  The execution body of the timer; the timer itself is passed as the parameter to this block when executed to aid in avoiding cyclical references
- (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));

介绍完循环引用的问题了,我们看下使用Taregt-Action 这种形式,我们一般是需要选择合适的时机去调用invalidate解除下循环引用让定时器无效,但是这个时机的选择比较不确定,需要根据具体的业务需求如pop出页面的时候,dismiss调页面时候。主动调用invalidate函数
所以我们不想管他什么时机调用,我们不想手动调用了,我们只关注当前VC释放的时候定时器就无效掉,于是我们需要借助一个中间类去帮我们自动实现这种需求

类方法:

创建一个继承NSObject的子类TempTarget,并创建开启计时器的方法。

TempTarget .h文件

@property (nonatomic, assign) SEL selector;
@property (nonatomic, weak) NSTimer *tempTimer;
@property (nonatomic, weak) id tempTarget;

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval target:(id)tempTarget selector:(SEL)selector userInfo:(id)userInfo repeats:(BOOL)repeats;

TempTarget .m文件

@implementation TempTarget

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval target:(id)tempTarget selector:(SEL)selector userInfo:(id)userInfo repeats:(BOOL)repeats {
    TempTarget *target = [[TempTarget alloc] init];
    target.tempTarget = tempTarget;
    target.selector = selector;
    target.tempTimer = [NSTimer scheduledTimerWithTimeInterval:interval target:target selector:@selector(timerSelector:) userInfo:userInfo repeats:repeats];
    return target.tempTimer;
}

- (void)timerSelector:(NSTimer *)tempTimer {
    if (self.tempTarget && [self.tempTarget respondsToSelector:self.selector]) {
        [self.tempTarget performSelector:self.selector withObject:tempTimer.userInfo];
    }else {
        [self.tempTimer invalidate];
    }
}

@end

VC调用

self.timer = [TempTarget scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerStart:) userInfo:nil repeats:YES];
注意

VC强引用timer,因为timer的target是TempTarget实例,所以timer强引用TempTarget实例,而TempTarget实例弱引用VC,解除循环引用。看TempTarget.h 文件两个所引用属性,其实这种方法本质还是存在循环医用的问题的,根本解决循环引用还是主动调用invalidate方法

1587379684885.jpg
上一篇下一篇

猜你喜欢

热点阅读