iOS UIViewController 跟 NSTimer 无法释放真的是循环引用导致的吗?

2,131 阅读3分钟

废话开篇:在网上有这样的一个观点:当 UIViewController 里面有一个属性用 strong 修饰 NSTimer 对象,当 NSTimer 的 target 是当前控制器时,那么,在当前控制器的 dealloc 方法里进行定时器停止及置为nil是失效的,因为二者循环引用了。这样去理解无法释放的现象是有问题的

问题一、谁造成了 UIViewController 无法释放?

image.png

UIViewController 不强引用 NSTimer 对象,但 UIViewController 作为 NSTimer 的 target 依然是释放不了的。因为 NSTimer 对象已有持有了当前控制器,导致了当前控制器的引用计数 +1 ,而且定时器对tagert的应用应该是强引用 ,而 NSTimer 是依托runloop的,所以,定时器在没有特殊设定情况下执行结束操作,那么它就会一直定时执行任务,无法退出,所以,它相应的 target 也就会因为引用计数无法清 0 而不能释放。其实这是导致 UIViewController 无法释放的原因,而大家又习惯将最后的逻辑处理放在 UIViewController 的 dealloc 里,那么,才会有错觉,这两个循环引用了。

问题二、将 UIViewController 用 __weak 修饰作为 target 给 NSTimer 可以吗?

不行,因为 NSTimer 对 target 是强引用,所以,即便是用 __weak 修饰也会对 UIViewController 进行引用计数 +1 的操作。

这里可以看一下 NSTimer 绑定 target 前后 UIViewController 的引用计数个数。

image.png

这里可以看到打印结果,引用计数直接增加了 1,也就是说,__weak 修饰符是没有任何意义的。

image.png

问题三、将 UIViewController 用 weak 修饰 NSTimer 属性可以吗?

不行,其实用什么修饰其实不重要了,到这里就很清楚了,二者无法释放的原因并不是循环引用,而是 NSTimer 根本没有时机释放自身,导致 UIViewController 的引用计数不能清 0,而开发者有习惯把定时器的销毁放到 UIViewController 的 dealloc 里。

问题四、要去房顶拿梯子?

要去房顶拿梯子?那么,必须先有梯子上房,梯子在哪?在房上...。解决问题的关键就是把 “梯子” 换个位置。

在定时器里进行一些判断,让 NSTimer 有机会释放。 image.png

看,控制器销毁了。

image.png

步骤五、造个中间件,阻断二者联系

创建一个 GGTimer 类,实现阻断 UIViewController 与 NSTimer 的联系。

typedef void(^TimerCallBack)(void);

@interface GGTimer : NSObject

//这里必须用 weak 修饰,否则将毫无意义,强引用链依旧可以穿到 UIViewController
@property (nonatomic,weak) id target;

@property (nonatomic,assign) SEL targetSelector;

//尾随闭包定时器回调
@property (nonatomic,copy) TimerCallBack timerCallBack;


@end


@implementation GGTimer

- (NSTimer *)timerInterval:(NSInteger)timeInterval target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo timerCallBack:(TimerCallBack)timerCallBack{

    self.target = aTarget;

    self.targetSelector = aSelector;

    self.timerCallBack = timerCallBack;

    NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector: @selector(timerAction) userInfo:nil repeats:YES];

    [timer fire];

    return timer;

}


- (void)timerAction

{

    if (self.timerCallBack) {
        //定时器回调
        self.timerCallBack();

    } else if (self.target && self.targetSelector && [self.target respondsToSelector:self.targetSelector] ) {
        
        //target 执行指定方法
        IMP imp = [self.target methodForSelector:self.targetSelector];

        void (*func)(id, SEL) = (void *)imp;

        func(self.target, self.targetSelector);

    }

}


-(void)dealloc

{

    GGNSLog(@"定时器中间件销毁了");

}

直接调用 performSelector 方法会有警告,这里用的是写法二。

image.png

步骤六、在 UIViewController 的 dealloc 方法里执行销毁定制器的操作,

这里 NSTimer 属性依然用的是 strong 修饰的。

image.png

UIViewController 的引用计数并没有增加。

image.png

直接销毁定时器。

image.png

image.png

可以看到,定时器中间件 GGTimer 对象跟 UIViewController 都销毁了。

用 NSProxy 其实也是可以避免上述问题的,其实原理差不多。本身 NSProxy 也是利用了消息转发机制。代码拙劣,大神勿笑😊