废话开篇:在网上有这样的一个观点:当 UIViewController 里面有一个属性用 strong 修饰 NSTimer 对象,当 NSTimer 的 target 是当前控制器时,那么,在当前控制器的 dealloc 方法里进行定时器停止及置为nil是失效的,因为二者循环引用了。这样去理解无法释放的现象是有问题的
问题一、谁造成了 UIViewController 无法释放?
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 的引用计数个数。
这里可以看到打印结果,引用计数直接增加了 1,也就是说,__weak 修饰符是没有任何意义的。
问题三、将 UIViewController 用 weak 修饰 NSTimer 属性可以吗?
不行,其实用什么修饰其实不重要了,到这里就很清楚了,二者无法释放的原因并不是循环引用,而是 NSTimer 根本没有时机释放自身,导致 UIViewController 的引用计数不能清 0,而开发者有习惯把定时器的销毁放到 UIViewController 的 dealloc 里。
问题四、要去房顶拿梯子?
要去房顶拿梯子?那么,必须先有梯子上房,梯子在哪?在房上...。解决问题的关键就是把 “梯子” 换个位置。
在定时器里进行一些判断,让 NSTimer 有机会释放。
看,控制器销毁了。
步骤五、造个中间件,阻断二者联系
创建一个 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 方法会有警告,这里用的是写法二。
步骤六、在 UIViewController 的 dealloc 方法里执行销毁定制器的操作,
这里 NSTimer 属性依然用的是 strong 修饰的。
UIViewController 的引用计数并没有增加。
直接销毁定时器。
可以看到,定时器中间件 GGTimer 对象跟 UIViewController 都销毁了。
用 NSProxy 其实也是可以避免上述问题的,其实原理差不多。本身 NSProxy 也是利用了消息转发机制。代码拙劣,大神勿笑😊