项目中有个地方用到了计时器,最近偶然发现timer没有得到释放,问题代码如下:
#import "NextViewController.h"
@interface NextViewController ()
@property (nonatomic, weak) NSTimer *timer;
@end
@implementation NextViewController
- (void)viewDidLoad {
[super viewDidLoad];
_timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(countDown:) userInfo:nil repeats:YES];
}
- (void)countDown:(NSTimer *)timer {
NSLog(@"计时中");
}
- (void)dealloc {
[_timer invalidate];
_timer = nil;
}
常规操作,打眼一看没什么问题,可当我pop回上个界面,发现timer仍然在工作,timer没有被销毁?于是我在dealloc方法处打了断点,发现没有走断点,那说明页面在返回的时候没有被销毁,按理来说页面出栈,应该被销毁掉了,页面没被销毁的原因可能有以下几点:
1.timer计时器没有被销毁
2.block循环引用:简单来说就是在堆上的对象与堆上的对象互相引用造成的环,就会引起循环引用。
3.delegate循环引用
所以问题很显然,timer没有释放
那为什么timer没有被释放:
emmmm……,timer需要在dealloc中进行释放,也就是需要控制器先进行释放,而timer又强引用了target(self即就是控制器),要释放控制器,又得需要timer进行释放。举个暗黑点的例子,就是A,B两人约好自我销毁,两人互不信任,A说:你先死,你死了我才能死,B说:不,你先死,你死了我才能死。最后导致两个人都不能自我销毁成功。
当方法中的参数repeats
设置为YES
的时候,runloop强引用timer,timer强引用target(self),关系:runloop->timer->target(self)。这里附上官文中的一段话:
Timers work in conjunction with run loops. Run loops maintain strong references to their timers, so you don’t have to maintain your own strong reference to a timer after you have added it to a run loop.
大概意思就是:timer是依赖runloop工作的。runloop强引用计时器,因此在将计时器添加到runloop中后,就不必再强引用timer了。
那么我们如何解决这个问题:
1.在viewDisAppear相关方法中对timer进行销毁
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
[_timer invalidate];
_timer = nil;
}
问题是如果我在进入下个页面的时候我不想对timer进行销毁,那这个就不是很合适了。
2.既然问题是出在timer对target(self)的强引用,那我是否可以把这个target进行转移,不让这个target指向控制器。答案是可以的,我们可以写个分类,将target封装在内部,通过block将事件暴露出来,如下
#import "NSTimer+PassEvent.h"
@implementation NSTimer (PassEvent)
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti event:(void(^)(NSTimer *timer))event {
return [NSTimer scheduledTimerWithTimeInterval:ti target:self selector:@selector(trigger:) userInfo:[event copy] repeats:YES];
}
+ (void)trigger:(NSTimer *)timer {
void(^block)(NSTimer *timer) = timer.userInfo;
if (block) {
block(timer);
}
}
@end
注意:
这里用了一个非常巧妙的传参步骤,将block作为参数进行传递,然后通过timer的userInfo传递给事件。对于block copy,我自己测试了一下,其实这里不用copy也是可以的,在ARC下,只要将block赋值给一个变量,那么这个block就将被拷贝到堆上,这是编译器的优化。
在iOS10.0的时候官方其实已经给我们增加了一个这样的方法,内部实现应该和这个差不多。
/// - parameter: block The execution body of the timer; the timer itself is passed as the parameter to this block when executed to aid in avoiding cyclical references
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
注意:
这个block参数的解释,翻译过来是:block是计时器的执行主体;执行计时器时,计时器本身作为参数传递给该块,以帮助避免循环引用。
所以timer不能销毁的原因是循环引用,runloop->timer->target(self)-->timer,关于这里为啥对timer用了weak属性修饰符还是没能打破循环引用,这是因为还有runloop持有着timer,timer的引用计数不为0就不能自动置为nil,只能手动置为nil。那么循环引用中的一条关系链破坏掉没用,那我们就破坏掉另一条, 所以将timer->target(self)他们的关系进行破坏,将target指向其他对象。
所以兼容10.0之前的版本最后的代码是这样:
#import "NSTimer+PassEvent.h"
#import <UIKit/UIKit.h>
@implementation NSTimer (PassEvent)
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti event:(void(^)(NSTimer *timer))event {
NSString *version= [UIDevice currentDevice].systemVersion;
if (version.doubleValue >= 10.0) {
return [NSTimer scheduledTimerWithTimeInterval:ti repeats:YES block:event];
} else {
return [NSTimer scheduledTimerWithTimeInterval:ti target:self selector:@selector(trigger:) userInfo:[event copy] repeats:YES];
}
}
+ (void)trigger:(NSTimer *)timer {
void(^block)(NSTimer *timer) = timer.userInfo;
if (block) {
block(timer);
}
}
@end
文章结束