NSTimer在项目中很常用, 但是使用不当会产生循环引用. 所以就想办法优化一下.
在 Controller B 中有一个 NSTimer
@property (strong, nonatomic) NSTimer *timer;
你创建了它,并挂载到 main runloop
self.timer = [NSTimer scheduledTimerWithTimeInterval:1
target:self selector:@selector(timerAction:) userInfo:nil repeats:true];
然后退出 Controller B 的时候,忘记关掉 timer 了, 这时候就产生了内存泄漏, Controller B 将不会释放,B 与 timer 循环引用。因为创建 timer 的时候把 self 直接写进去了。
要解决这个问题也很简单,当类的使用者能够确定不需要使用这个计时器时,比如-viewDidDisappear时,就调用
[_timer invalidate];
_timer = nil;
这样就打破了循环引用,Controller B也可以正确释放。
但是这不是本文的目的啊, 想办法试着解决这个问题, 即使使用者疏忽了, 也能正常释放timer和Controller B.
尝试一
既然不能直接传 self,那传 weakSelf 试试
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1
target:weakSelf selector:@selector(timerAction:) userInfo:nil repeats:true];
测试结果还是发生了循环引用,B 没有释放,timer 对 weakSelf 指向的变量是强引用的,主线程的RunLoop -> timer -> weakSelf指向的B对象 -> timer,三者之间形成循环引用。weakSelf只是一个小小的指针而已, 传入到NSTimer中时weakSelf指向的B对象还是被NSTimer强引用了, 这种尝试失败了.
尝试二
设置一个包装类,包着 Controller B 放进 timer 中,像这样
我认为 Controller B 有几 MB 那么大,泄露了很浪费内存。WeakWrap 只有几百个字节那么小,泄露了也没关系。
WeakWrap 中对 Controller B 弱引用,WeakWrap 包着 Controller B,传进 timer 中,就算忘记关 timer,也只是泄露了 WeakWrap 和 timer。
这种方式可以在Controller B的dealloc中写[_timer invalidate], 这样就不会有内存泄漏了, 如果不写的话,只不过是Controller B不泄露了, WeakWrap还是泄漏的,只是影响比较小罢了, 如果一个 Controller 是频繁进出的,进出一次,泄漏一个,如果有几十个泄露的 timer 挂在 main runloop 上会影响性能和流畅性,你想几十个 timer 一起 fire,又调用了 timer 事件响应方法,开销还是挺大的。
尝试三
NSTimer 已知是会强引用参数 target:self 的了,如果忘记关 timer 的话,传什么进去都会被强引用。那干脆模仿系统的实现自己做一个 timer 算了,timer 的功能就是定时调某个方法,我们自己开辟一个新线程, 让他做这个事情也可以啊!
后续琢磨一下, 这样的代价有点大啊, 虽然不需要考虑循环引用了, 但是一个timer就需要开一个线程, 而且线程的转换也需要耗费性能, 为了使用方便, 这么做的代价有点大. 虽然得不偿失, 但是也算是一种尝试, 一种思路吧.
尝试四(最终方案)
在ios10之后, 苹果出了几个新的NSTimer方法. 这几个方法的入参都是block, 不需要在传target+sel, 使用起来确实方便的很多, 但是 还是需要在dealloc中写 [_timer invalidate], 不然循环引用避免不了.
而且这个api是ios10之后才能用的, 很多需要从ios9开始兼容, 众多网友尝到了block的甜头, 就试着用类别封装一个block回调的NSTimer.
但是还是解决不了根源问题, 还是需要dealloc中的[_timer invalidate].
然后, 重点来了, 发现了吗, VC竟然可以走进dealloc, 也就是说,如果有个weakVC的指针, 那么这个weakVC那时候一定会变成nil, 可不可以在timer中判断下weakVC是否为nil, 如果为nil就把timer销毁掉. 调用的时候变成了这样, 在VC销毁后, weakSelf为nil, 此时把[_timer invalidate], 至少比在dealloc中要好的多.
等等, 虽然可以, 但是看着怎么这么怪呢! 要是能不写[_timer invalidate]就好了.
好吧, 我们在优化下, 还记得兼容ios9的那个类别吗? 我们拿过来改改, 还需要知道timer的任务执行者, 当任务执行者为nil时, 调用[_timer invalidate], 所以传入一个参数targetObject, 这个参数的生命周期和NSTimer是一起的, 要销毁同时销毁.
怎么做到同时销毁呢?
1. targetObject的引用计数不能增加, 需要使用weak保存这个对象, 当VC销毁时, targetObject的指针也会变成nil
2. targetObject为nil时, 调用[_timer invalidate]
由于执行事件需要2个参数, 一个是事件的block, 一个是targetObject判断Timer是否该销毁, 所以timer.userInfo得是一个集合类型, 而且由于不能给targetObject的引用计数增加, 而block恰恰需要引用计数+1, 所以还需要使用NSHashTable包裹一层targetObject, 最后就是这样了.
调用的时候就变成这样了, 设置VC都不需要一个属性来持有timer了.
为了确定这个timer真的没了, 检查3个东西, 所以这就是最终方案了. 完美
1. 对应的事件log消失了;
2. timer类别中的repeatBlock: 也不再执行了;
3. 打印加入timer之前和之后的[NSRunLoop currentRunLoop], 发现那个新生成timer确实没了;
最后贴上这个类别的代码, 还有demo地址 https://github.com/guochaoshun/timer
#import <Foundation/Foundation.h>
typedef void(^timerActionBlock)(NSTimer * _Nonnull blockTimer);
@interface NSTimer (Category)
// 这个方法是10.0以上系统才有的,按照这样的思路自己写一个,10.0以下的系统就能用了.
+ (NSTimer *)gcs_addTimerInRunLoopWithTimeInterval:(NSTimeInterval)interval targetObject:(id)target repeats:(BOOL)repeats block:(timerActionBlock)block ;
@end
------------
#import "NSTimer+Category.h"
@implementation NSTimer (Category)
+ (NSTimer *)gcs_addTimerInRunLoopWithTimeInterval:(NSTimeInterval)interval targetObject:(id)target repeats:(BOOL)repeats block:(timerActionBlock)block {
NSMutableDictionary * dic = [NSMutableDictionary dictionaryWithCapacity:2];
NSHashTable * table = [NSHashTable weakObjectsHashTable];
[table addObject:target];
[dic setObject:table forKey:@"target"];
[dic setObject:block forKey:@"block"];
NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:interval target:self selector:@selector(repeatBlock:) userInfo:dic repeats:repeats] ;
return timer ;
}
+ (void)repeatBlock:(NSTimer *)timer {
NSDictionary * dic = timer.userInfo ;
NSAssert([dic isKindOfClass:[NSDictionary class]], @"数据类型不对");
timerActionBlock block = [dic objectForKey:@"block"];
NSHashTable * table = dic[@"target"];
id target = table.anyObject;
if (target && block) {
block(timer) ;
} else {
[timer invalidate];
timer = nil;
}
}
@end