美文网首页
NSTimer解决循环引用

NSTimer解决循环引用

作者: hj的简书 | 来源:发表于2019-08-05 19:57 被阅读0次

    问题

    在使用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解决循环引用

          本文链接:https://www.haomeiwen.com/subject/zhkvdctx.html