用Block解决NSTimer循环引用

作者: Levi_ | 来源:发表于2015-03-26 22:16 被阅读6804次

    由于计时器会保留其目标对象,使用计时器时很容易引起循环引用,如下代码所示:

    @interface XXClass : NSObject
    - (void)start;
    - (void)stop;
    @end
    
    @implementation XXClass {
        NSTimer *timer;
    }
    
    - (id)init {
        return [super init];
    }
    
    - (void)dealloc {
        [timer]
    }
    
    - (void)stop {
        [timer invalidate];
        timer = nil;
    }
    
    - (void)start {
        timer = [NSTimerscheduledTimerWithTimeInterval:5.0 
                                                target:self  
                                              selector:selector(doSomething) 
                                              userInfo:nil 
                                               repeats:YES];
    }
    
    - (void)doSomething {
        //doSomething
    }
    
    @end
    

    大多数开发者可能都会这样来实现定时器。创建定时器的时候,由于目标对象是self,所以要保留此实例。然而,因为定时器是用实例变量存放的,所以实例也保留了定时器,这就造成了循环引用。除非调用stop方法,或者系统回收实例,才能打破循环引用,如果无法确保stop一定被调用,就极易造成内存泄露。
    当指向XXClass实例的最后一个外部引用移走之后,该实例仍然会继续存活,因为定时器还保留着它。而定时器对象也不可能被系统释放,因为实例中还有一个强引用正在指向它。这种内存泄露是很严重的,如果定时器每次轮训都执行一些下载工作,常常会更容易导致其他内存泄露问题。
    这个问题可以通过Block来解决:

    @interface NSTimer (XXBlocksSupport)
    
    + (NSTimer *)xx_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                             block:(void(^)())block
                                           repeats:(BOOL)repeats;
    
    @end
    
    @implementation NSTimer (XXBlocksSupport)
    
    + (NSTimer *)xx_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                             block:(void(^)())block
                                           repeats:(BOOL)repeats
    {
        return [self scheduledTimerWithTimeInterval:interval
                                              target:self
                                            selector:@selector(xx_blockInvoke:)
                                            userInfo:[block copy]
                                             repeats:repeats];
    }
    
    + (void)xx_blockInvoke:(NSTimer *)timer {
        void (^block)() = timer.userinfo;
        if(block) {
            block();
        }
    }
    
    @end
    
    

    定时器现在的target是NSTimer类对象,这是个单例,此处依然有循环引用,然后类对象无需回收,所以不用担心。
    这套代码并不能解决问题,例如:

    - (void)start {
        timer = [NSTimer xx_scheduledTimerWithTimeInterval:.5
                                                     block:^{
                                                     [self doSomething];
                                                            }
                                                   repeats:YES];
    }
    

    这段代码里还是有循环引用,因为Block捕获了self变量。此处只要改用weak引用,即可打破循环引用。

    - (void)start {
        __weak XXClass *weakSelf = self;
        timer = [NSTimer xx_scheduledTimerWithTimeInterval:.5
                                                     block:^{
                                                     XXClass *strongSelf = weakSelf;
                                                     [strongSelf doSomething];
                                                            }
                                                   repeats:YES];
    }
    

    先定义了一个弱引用,令其指向self,然后使块捕获这个引用,而不直接去捕获普通的self变量。也就是说,self不会为计时器所保留。当块开始执行时,立刻生成strong引用,以保证实例在执行期间持续存活。
    采用这种写法之后,如果外界指向XXClass实例的最后一个引用将其释放,则该实例就可为系统所回收了。

    相关文章

      网友评论

      • 不必luo嗦:你好,请问这个方法+ (void)xx_blockInvoke:(NSTimer *)timer 为什么不是对象方法(-)
      • MrCoderLin:博主说NSTimer 会引用timer,请问这是怎么引用的?不理解,请解答。:pray:
      • 坤坤同学:这里真正创建timer实例的地方是在NSTimer的Category中,而且target也是NSTimer,NSTimer持有timer实例,timer实例持有NSTimer,还是有循环引用的,博主文章中也提到了。

        “定时器现在的target是NSTimer类对象,这是个单例,此处依然有循环引用,然后类对象无需回收,所以不用担心。” 至于这句我不太认同,虽然类对象无需回收,但是repeat为YES的timer并没有被回收,浪费内存,block被copy到timer的userInfo中,block也会一直被执行的。

        要想打破上述循环引用,需要在创建timer的类(非NSTimer)中对timer进行invalidate.
        举个例子:
        在controller中用category block方式创建个timer,controller并不持有timer,所以controller的dealloc方法会执行的,在controller的dealloc中对timer进行invalidate即可。

        以上只是对目前博主这种方式创建timer进行打破循环引用。在尝试使用其他方法。
        坤坤同学:@Vin周 NSTimer在创建的过程中会对target进行强引用直到invalidate,timer被加到Runloop后,Runloop对timer又进行了强引用,直到timer被invalidate后Runloop才释放对timer的强引用。就是这样一个过程
        VinZZZZ:没必要“在controller中用category block方式创建个timer”, 因为即使这个timer是controller的普通属性,dealloc仍会被执行。

        不过为啥 “NSTimer持有timer实例” ,这个还请进一步解释下?
        e763f1ec1cfe:block方式创建timer,在iOS10后才有。
      • Mr_Zander:虽然调用了dealloc但是timer还是会调用selector,请问楼主有什么好的方法吗?
      • 五月雪06:确实还是存在循环引用,调用不了dealloc方法
      • 叔丙仄:使用Block的方式,依然会有“类对象的循环引用”,虽然不会造成看起来的问题。
        更好的方式,个人觉得是使用weakProxy(继承自NSProxy,然后弱引用target),这样可以避免Catagory的使用(需要更改系统调用方法),而且使用起来更与系统本身方法更结合些,至少表面上看起来代码漂亮些。
        一剑寒潇:@叔丙仄 你的方案能发个demo吗?感觉很高深,我的邮箱:xinsiwei123@126.com,谢谢~
        Hi川:@叔丙仄 对的
        leftwater:恩 你这个也阔以实现
      • 大号鱼骨头:刚好遇到一个类似的问题

      本文标题:用Block解决NSTimer循环引用

      本文链接:https://www.haomeiwen.com/subject/bwqnxttx.html