NSTimer解决循环引用

问题

在使用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。因此,产生循环引用也就不奇怪了。

代码解决方法

首先我们可以先看一下如下图,我们可以添加一个中间类,将TimerProxytarget设为弱引用并指向当前控制器就不会产生循环引用了

解决逻辑图

代码实现如下:

- (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或者控制器。

你可能感兴趣的:(NSTimer解决循环引用)