NSTimer会保留目标对象


标签: objectvie-c 计时器

前言

在编写业务逻辑的时候,用到NSTimer的地方很多。比如,定期的更新窗口。对于它开发者文档中有如下描述:

You use the NSTimer class to create timer objects or, more simply, timers. A timer waits until a certain time interval has elapsed and then fires, sending a specified message to a target object. For example, you could create an NSTimer object that sends a message to a window, telling it to update itself after a certain time interval.

这是官方对NSTimer的介绍,就是用来在一个指定的时间间隔内,发送个一消息给一个指定的对象做指定你想要做的事。提到它就顺带提一下CFRunLoopTimerRef,这两个其实是一个东西,只不过是不同框架里的。前者是属于Foundation里的,属于ARC的管理范围,后者是属于Core Foundation里的,基于C语言实现的一个框架,不属于ARC的管理范围。但是这两个框架里的很多东西是是等价的,只是名字不同而已。

PS:在ARC下编程,Cocoa框架下的类都在ARC的管理范围之下包括以NS开头的类。Cocoa是最重要的两个框架为:Foundation和UIKit。使用其他框架的时候要小心了,对象的内存管理需要我们自己来哦!这其中就包括Core Foundation。

Timers work in conjunction with run loops.Note in particular that 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.A timer is not a real-time mechanism;it fires only when one of the run loop modes to which the timer has been added is running and able to check if the timer’s firing time has passed.

上面是节选的一段,个人认为比较重要的。主要是说NSTimer是和RunLoop一起工作的,而且RunLoop会对加入其中的NSTimer保持一个强引用。对NSTimer来说,一旦添加进RunLoop中,我们就不需要在外部去维护一个NSTimer的强引用。NSTimer的时间间隔也不是实时,可能比实际的时候多一点,也可能少一点,而且它只在指定的mode中运行。关于RunLoop,这是之前写的。

正文

以前用计时器的时候,只顾着用了,也没想太多,在后来的学习当中,发现计时器也会持有目标对象从而产生保留环,而且对重复执行的计时器,这种后果不仅是漏了内存还可能有更加严重。以下面的代码为例:

//Customer.h
#import <Foundation/Foundation.h>

@interface Customer : NSObject
@property(nonatomic, strong) NSTimer *timer;
- (void)start;
- (void)stop;
@end

//Customer.m
#import "MyTimer.h"
@implementation MyTimer
- (instancetype)init
{
    return [super init];
}
- (void)stop
{
    [self.timer invalidate];
    self.timer = nil;
}
- (void)start
{
    __weak MyTimer *timer = self;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:@selector(dosomething) userInfo:nil repeats:YES];
}
- (void)dosomething
{
    NSLog(@"dosomething");
}
- (void)dealloc
{
    [self.timer invalidate];
}
@end

//某个作用域
{
    Customer *cus = [[Customer alloc] init];
    [cus start];
}

如果一切正常情况,当出了作用域之后cus,cus便会被释放,计时器也会失效。但是因为计时器持有了self,而计时器作为属性又被self持有了,这样就出现了保留环,所以就算出了作用域cus其实也没有被释放即dealloc方法没有执行,计时器也没有失效,依然做着事,这样很可能会出现严重后果,这取决于所做的事。这里除非手动调用stop方法使计时器失效。那有没有更好的办法呢?答案是肯定的,那就是通过块来解决。可以为NSTimer增加一个分类:

//NSTimer+blockSupport.h
#import <Foundation/Foundation.h>

@interface NSTimer (blockSupport)
+ (NSTimer*)block_scheduledTimerWithTimerInterval:(NSTimeInterval)interval
                                           block:(void(^)())block
                                         repeats:(BOOL)repeats;

@end

//NSTimer+blockSupport.m
#import "NSTimer+blockSupport.h"

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

通过上面这个类方法创建的计时器,利用了userInfo参数将计时器要执行的任务封装成了块,该参数可以存放一个不透明值即万能值,只要计时器有效,该参数就会一直被保留。仅依靠这个分类还不行,还要对start方法修改如下:

- (void)start
{
    __weak MyTimer *timer = self;
    self.timer = [NSTimer block_scheduledTimerWithTimerInterval:1.0f block:^{
        [timer dosomething];
    } repeats:YES];
}

这样把计时器所做的事封装进了block,只要目标对象存在计时器就一直有效,而计时器捕获的只是self的一个弱引用,不会造成保留环。一旦目标对象被释放了,计时器立刻失效。以上内容参考了《Effectvie Objective-c》

–EOF–

由于disqus被墙,评论需要VPN哦。