前言
我们在开发中,经常会用到NSTimer
这个类的+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo
方法,但是NSTimer会对传入的target
对象进行强引用,如果target
又对timer
进行了强引用,那么就会出现循环引用,今天我们就研究一下如何解决这个问题。
例如:
@interface ViewController ()
@property (strong, nonatomic) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
}
- (void)timerTest {
NSLog(@"%s", __func__);
}
- (void)dealloc {
NSLog(@"%s", __func__);
//类销毁的时候调用`timer`的`invalidate`方法进行停止
[_timer invalidate];
}
@end
当控制器pop
出去的时候,会执行dealloc
方法,看上去好像没什么问题,但是我们实际的运行结果是这样的:
2019-08-14 20:26:39.636581+0800 定时器-01[16487:12091593] -[ViewController timerTest]
2019-08-14 20:26:40.636660+0800 定时器-01[16487:12091593] -[ViewController timerTest]
2019-08-14 20:26:41.636513+0800 定时器-01[16487:12091593] -[ViewController timerTest]
2019-08-14 20:26:42.636270+0800 定时器-01[16487:12091593] -[ViewController timerTest]
当我们的控制器pop
消失以后控制台还在一直打印timerTest
的方法,dealloc
方法也没有执行。这是为什么?原因就是控制器
对timer
进行了强引用,而timer
又会对传入的target
也就是控制器
进行了强引用,这就造成了循环引用。
这时候我们就会想,将self
通过__weak
修饰一下不就可以了,那么我们来试一下,代码经过改造后如下:
@interface ViewController ()
@property (strong, nonatomic) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:weakSelf selector:@selector(timerTest) userInfo:nil repeats:YES];
}
- (void)timerTest {
NSLog(@"%s", __func__);
}
- (void)dealloc {
NSLog(@"%s", __func__);
//类销毁的时候调用`timer`的`invalidate`方法进行停止
[_timer invalidate];
}
@end
这时候我们再次运行代码,结果还是一样的,控制器消失了,但是控制台还在一直打印timerTest
方法,这是为什么呢?
其实原因还是上面所说的,timer
对传入的terget
也就是self
进行了强引用,虽然我们在外部传入的是一个弱引用的weakSelf
,但是timer
内部还是有一个强指针指向了self
的内存地址。
那么我们应该怎么解决这个问题呢?在这里我提供了三种解决方式:
-
方案1:在
- (void)viewWillDisappear:(BOOL)animated
方法中调执行[_timer invalidate]
方法
改造后的代码如下:
@interface ViewController ()
@property (strong, nonatomic) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
}
- (void)timerTest {
NSLog(@"%s", __func__);
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
//在此处调用,可以避免循环引用导致的内存问题
[_timer invalidate];
}
- (void)dealloc {
NSLog(@"%s", __func__);
}
@end
运行结果如下:
2019-08-14 20:54:36.662354+0800 定时器-01[16508:12095299] -[ViewController timerTest]
2019-08-14 20:54:37.662384+0800 定时器-01[16508:12095299] -[ViewController timerTest]
2019-08-14 20:54:38.662300+0800 定时器-01[16508:12095299] -[ViewController timerTest]
2019-08-14 20:54:39.662305+0800 定时器-01[16508:12095299] -[ViewController timerTest]
2019-08-14 20:54:40.210602+0800 定时器-01[16508:12095299] -[ViewController dealloc]
我们看到,当 控制器
消失的时候,执行了dealloc
方法,为什么呢?原因是在[_timer invalidate]
执行以后,释放了对target
也就是self
的强引用,循环引用被破坏,因此控制器
可以被释放。
这种方式仅限于控制器中,当我们的传入的target
不是一个控制器,而是一个普通的继承自NSObject
的对象的时候,就不能用这种方式,因此我们来看下面的方法
-
方案2:使用
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block
方法
改造后的代码如下:
@interface ViewController ()
@property (strong, nonatomic) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
if (@available(iOS 10.0, *)) {
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0f repeats:YES block:^(NSTimer * _Nonnull timer) {
[weakSelf timerTest];
}];
} else {
// Fallback on earlier versions
}
}
- (void)timerTest {
NSLog(@"%s", __func__);
}
- (void)dealloc {
NSLog(@"%s", __func__);
//类销毁的时候调用`timer`的`invalidate`方法进行停止
[_timer invalidate];
}
执行结果如下:
2019-08-14 20:44:26.588273+0800 定时器-01[16505:12094230] -[ViewController timerTest]
2019-08-14 20:44:27.588241+0800 定时器-01[16505:12094230] -[ViewController timerTest]
2019-08-14 20:44:28.588314+0800 定时器-01[16505:12094230] -[ViewController timerTest]
2019-08-14 20:44:29.588270+0800 定时器-01[16505:12094230] -[ViewController timerTest]
2019-08-14 20:44:29.611601+0800 定时器-01[16505:12094230] -[ViewController dealloc]
这时候,当控制器
消失的时候,执行了dealloc
方法,原因是虽然timer
对block
是强引用,但是block
内部对于self
是弱引用的,这个时候,破坏了循环引用,控制器
没有被timer
强引用,所以可以被释放。但是我们发现,这个方法只有在iOS10
以后才有的,所以,如果我们的app
的兼容版本是iOS 10
以后的,那么我们可以这么写,如果要兼容更低版本,那么我们只能载想其他的办法。
-
方案3:使用
代理对象
首先我们创建一个代理对象
,代码如下:
#import <Foundation/Foundation.h>
@interface TimerProxy : NSObject
@property (weak, nonatomic) id target;
+ (instancetype)proxyWithTarget:(id)target;
@end
#import "TimerProxy.h"
@implementation TimerProxy
+ (instancetype)proxyWithTarget:(id)target {
TimerProxy *proxy = [[TimerProxy alloc] init];
proxy.target = target;
return proxy;
}
//利用消息转发机制,将方法转发给target处理
- (id)forwardingTargetForSelector:(SEL)aSelector {
return self.target;
}
@end
然后改造原有的代码:
#import "TimerProxy.h"
@interface ViewController ()
@property (strong, nonatomic) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//此时,传入的target就不是self,而是TimerProxy对象
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:[TimerProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
}
- (void)timerTest {
NSLog(@"%s", __func__);
}
- (void)dealloc {
NSLog(@"%s", __func__);
//类销毁的时候调用`timer`的`invalidate`方法进行停止
[_timer invalidate];
}
执行结果如下:
2019-08-14 21:17:47.476647+0800 定时器-01[16527:12097738] -[ViewController timerTest]
2019-08-14 21:17:48.475937+0800 定时器-01[16527:12097738] -[ViewController timerTest]
2019-08-14 21:17:49.476636+0800 定时器-01[16527:12097738] -[ViewController timerTest]
2019-08-14 21:17:50.476643+0800 定时器-01[16527:12097738] -[ViewController timerTest]
2019-08-14 21:17:51.120013+0800 定时器-01[16527:12097738] -[ViewController dealloc]
这种方案,我们采用的是代理对象
的方式,控制器
对于timer
是强引用,timer
对于代理对象
是强引用,而代理对象
对于控制器
是弱引用,这样我们就破坏了循环引用,最终调用了dealloc
方,从而解决问题。
-
方案4:使用
NSProxy
(推荐方案)
首先,我们自定义一个继承自NSProxy
的类
#import <Foundation/Foundation.h>
@interface TimerProxy2 : NSProxy
@property (weak, nonatomic) id target;
+ (instancetype)proxyWithTarget:(id)target;
@end
#import "TimerProxy2.h"
@implementation TimerProxy2
+ (instancetype)proxyWithTarget:(id)target {
// 这句代码会报错,因为NSProxy无init方法,直接alloc即可
// TimerProxy2 *proxy = [[TimerProxy2 alloc] init];
TimerProxy2 *proxy = [TimerProxy2 alloc];
proxy.target = target;
return proxy;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.target methodSignatureForSelector:sel];
}
//消息转发
- (void)forwardInvocation:(NSInvocation *)invocation {
invocation.target = self.target;
[invocation invokeWithTarget:self.target];
}
@end
改造原有代码
#import "TimerProxy2.h"
@interface ViewController ()
@property (strong, nonatomic) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//此时,传入的target就不是self,而是TimerProxy对象
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:[TimerProxy2 proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
}
- (void)timerTest {
NSLog(@"%s", __func__);
}
- (void)dealloc {
NSLog(@"%s", __func__);
//类销毁的时候调用`timer`的`invalidate`方法进行停止
[_timer invalidate];
}
执行结果如下:
2019-08-14 21:17:46.476786+0800 定时器-01[16527:12097738] -[ViewController timerTest]
2019-08-14 21:17:47.476647+0800 定时器-01[16527:12097738] -[ViewController timerTest]
2019-08-14 21:17:48.475937+0800 定时器-01[16527:12097738] -[ViewController timerTest]
2019-08-14 21:17:49.476636+0800 定时器-01[16527:12097738] -[ViewController timerTest]
2019-08-14 21:17:50.476643+0800 定时器-01[16527:12097738] -[ViewController timerTest]
2019-08-14 21:17:51.120013+0800 定时器-01[16527:12097738] -[ViewController dealloc]
这种方案也可以解决循环引用的问题,但是大家可能觉得,这种写法相比方案3更麻烦一些,代码写的要更多,但是为什么更推荐这种实现方式呢?原因还要从oc
方法调用分析。
oc
的方法调用最终会转换为objc_msgSend
函数进行调用,objc_msgSend
的执行流程分为三大阶段
- 消息发送
- 动态解析
- 消息转发
其中消息发送
阶段,就会进行多次方法查找。首先从自身的方法缓存中进行查找,找不到会在本类的方法列表进行查找,找不到再去父类的缓存中进行查找,找不到再去父类的方法列表中进行查找...一直超找到根类,如果还没有找到的话,就会进行动态方法解析
,没有实现动态方法解析
的话,最后就会进入消息转发
阶段。
而使用NSProxy
会先从自身方法列表进行查找,找不到的话,会直接进入消息转发
阶段,省去了去父类一层层查找和动态方法解析
这些操作,因此效率更高。
自此,我们关于NSTimer
引起的循环引用问题就算是彻底解决了,其实方法4也是给我们提供了一种思路,如果以后开发中遇到了类似于NSTimer
这种的循环引用问题,我们完全可以通过代理对象
这种方式来解决这种问题。