解决NSTimer循环应用(完美版), 不需要再 viewDidDisappear 中释放

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

     

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值