原因
当你在ViewController(简称VC)中使用timer属性,由于VC强引用timer,timer的target又是VC造成循环引用。当你在VC的dealloc方法中销毁timer,发现VC被pop,VC的dealloc方法没走,VC在等timer释放才走dealloc,timer释放在dealloc中,所以引起循环引用。
解决方案
- 在ViewController执行dealloc前释放timer(不推荐)
- 苹果API接口解决方案(iOS 10.0以上可用)
- 使用block进行解决
- 使用NSProxy进行解决
一、在ViewController执行dealloc前释放timer(不推荐)
可以在viewWillAppear中创建timer
可以在viewWillDisappear中销毁timer
二、苹果系统API可以解决(iOS10以上)
在iOS 10.0以后,苹果官方新增了关于NSTimer的三个API:
+ (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));
这三个方法都有一个Block的回调方法。关于block参数,官方文档有说明:
the timer itself is passed as the parameter to this block when executed
to aid in avoiding cyclical references。
翻译过来就是说,定时器在执行时,将自身作为参数传递给block,来帮助避免循环引用。使用很简单,但是要注意两点:
1.避免block的循环引用,使用__weak和__strong来避免
2.在持用NSTimer对象的类的方法中-(void)dealloc调用NSTimer 的- (void)invalidate方法;
三、使用block来解决
通过创建一个NSTimer的category名字为PFSafeTimer,在NSTimer+PFSafeTimer.h代码如下:
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSTimer (PFSafeTimer)
+ (NSTimer *)PF_ScheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval block:
(void(^)(void))block repeats:(BOOL)repeats;
@end
NS_ASSUME_NONNULL_END
在NSTimer+PFSafeTimer.m中的代码如下:
#import "NSTimer+PFSafeTimer.h"
@implementation NSTimer (PFSafeTimer)
+ (NSTimer *)PF_ScheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval block:(void(^)(void))block repeats:(BOOL)repeats {
return [NSTimer scheduledTimerWithTimeInterval:timeInterval target:self selector:@selector(handle:) userInfo:[block copy] repeats:repeats];
}
+ (void)handle:(NSTimer *)timer {
void(^block)(void) = timer.userInfo;
if (block) {
block();
}
}
@end
该方案主要要点:
- 将计时器所应执行的任务封装成"Block",在调用计时器函数时,把block作为userInfo参数传进去。
- userInfo参数用来存放"不透明值",只要计时器有效,就会一直保留它。
- 在传入参数时要通过copy方法,将block拷贝到"堆区",否则等到稍后要执行它的时候,该blcok可能已经无效了。
- 计时器现在的target是NSTimer类对象,这是个单例,因此计时器是否会保留它,其实都无所谓。此处依然有保留环,然而因为类对象(class object)无需回收,所以不用担心。
三、使用NSProxy来解决循环引用
NSProxy:一个抽象超类,用于定义对象的API,这些对象充当其他对象或尚不存在的对象的替身。
使用NSProxy我们可以把任意的对象隐藏在后面,由这个抽象类在前面为我们真实的对象代理,当然,我们需要实现两个方法
- (void)forwardInvocation:(NSInvocation *)invocation;
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel;
第一个是方法决议,我们可以在这里改变方法的指针,更换方法, 第二个是方法签名,用来提供相应的函数返回类型和参数,
接下来我们新建 TimerProxy 类 继承 NSProxy:
//TimerProxy.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface TimerProxy : NSProxy
+ (instancetype)proxyWithObject:(id)obj;
@end
NS_ASSUME_NONNULL_END
#import "TimerProxy.h"
@interface TimerProxy ()
@property (nonatomic, weak) id object;
@end
@implementation TimerProxy
- (instancetype)withProxy:(id)obj {
_object = obj;
return self;
}
+ (instancetype)proxyWithObject:(id)obj {
return [[self alloc] withProxy:obj];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
SEL selector = invocation.selector;
if ([_object respondsToSelector:selector]) {
[invocation invokeWithTarget:_object];
}
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [_object methodSignatureForSelector:sel];
}
@end