问题
在使用NSTimer
的时候,我们会遇到按理说控制器会调用dealloc
的情况下并没有调用,这就是因为在初始化NSTimer
的时候,传入的target
会被NSTimer
强引用,并且控制器强引用NSTimer
,所以产生循环引用。
使用如下代码,就可以看到在TwoViewController
退出的时候,dealloc
并没有调用
@interface TwoViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation TwoViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}
- (void)timerAction
{
NSLog(@"%s", __func__);
}
- (void)dealloc
{
[self.timer invalidate];
NSLog(@"%s", __func__);
}
上面看到的图片都是靠项目运行期间产生的问题推测出来的,有什么方法可以判断猜测的正确性呢?下面我会介绍源代码方式和解决循环引用的方式。
源代码验证循环引用
因为iOS Foundation框架是闭源的,所以并没有直接的代码供用户查看源码。但是我们可以通过 GNUstep 开源项目进行查看,它将Cocoa的OC库重新开源实现了一遍,因此对我们软件开发具有一定的参考价值。
+ (NSTimer*) timerWithTimeInterval: (NSTimeInterval)ti
target: (id)object
selector: (SEL)selector
userInfo: (id)info
repeats: (BOOL)f
{
return AUTORELEASE([[self alloc] initWithFireDate: nil
interval: ti
target: object
selector: selector
userInfo: info
repeats: f]);
}
.
.
.
- (id) initWithFireDate: (NSDate*)fd
interval: (NSTimeInterval)ti
target: (id)object
selector: (SEL)selector
userInfo: (id)info
repeats: (BOOL)f
{
if (ti <= 0.0)
{
ti = 0.0001;
}
if (fd == nil)
{
_date = [[NSDate_class allocWithZone: NSDefaultMallocZone()]
initWithTimeIntervalSinceNow: ti];
}
else
{
_date = [fd copyWithZone: NSDefaultMallocZone()];
}
_target = RETAIN(object);
_selector = selector;
.
.
.
@interface NSTimer : NSObject
{
#if GS_EXPOSE(NSTimer)
@public
NSDate *_date; /* Must be first - for NSRunLoop optimisation */
BOOL _invalidated; /* Must be 2nd - for NSRunLoop optimisation */
BOOL _repeats;
NSTimeInterval _interval;
id _target;
SEL _selector;
id _info;
查看上述三个代码片段,通过+ timerWithTimeInterval:target:selector:userInfo:repeats:
定位到_target
可以看到,项目是通过强引用,引用这个_target
。因此,产生循环引用也就不奇怪了。
代码解决方法
首先我们可以先看一下如下图,我们可以添加一个中间类,将TimerProxy
的target
设为弱引用并指向当前控制器就不会产生循环引用了
代码实现如下:
- (void)viewDidLoad {
[super viewDidLoad];
self.timer = [NSTimer timerWithTimeInterval:1.0 target:[TimerProxy timerProxyWithTarget:self] selector:@selector(timerAction) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}
@interface TimerProxy : NSObject
@property (nonatomic, weak) id target;
+ (instancetype)timerProxyWithTarget:(id)target;
@end
@implementation TimerProxy
+ (instancetype)timerProxyWithTarget:(id)target
{
TimerProxy *instance = [TimerProxy new];
instance.target = target;
return instance;
}
- (id)forwardingTargetForSelector:(SEL)aSelector
{
return self.target;
}
@end
这里使用到了runtime
消息转发机制,将当前原本发送到target(TimerProxy类)
的selector
转发到当前控制器了,避免方法找不到错误,这样就解决循环引用。
当然了,也可以使用YYkit那套分类NSTimer+YYAdd
,他将target
指向了NSTimer类对象,并且通过block传递selector
,iOS10之后,也提供了类似YYkit的做法。他们相同的做法就是避免target
指向当前view或者控制器。