应该取消分配NSInvocation中的目标,但它不是

时间:2015-05-29 08:11:53

标签: ios objective-c

我创建了一个弱目标计时器类,当目标被释放时,计时器不会自动触发并使其自身无效。代码是这样的:

// ViewController.m

@interface TestObj : NSObject
@end

@implementation TestObj
- (id)init
{
    self = [super init] ;
    if (self) {
        NSLog(@"%@ %@", self, NSStringFromSelector(_cmd)) ;
    }
    return self ;
}
- (void)dealloc
{
    NSLog(@"%@ %@", self, NSStringFromSelector(_cmd)) ;
}
- (void)timerFiredForInvocation:(id)obj
{
    NSLog(@"%@, %@", obj, NSStringFromSelector(_cmd)) ;
}
@end

@interface ViewController ()
@property (nonatomic, strong) WTTimer *timer1 ;
@property (nonatomic, strong) WTTimer *timer2 ;
@property (nonatomic, strong) TestObj *obj ;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    _obj = [[TestObj alloc] init] ;
    NSMethodSignature *methodSig = [_obj methodSignatureForSelector:@selector(timerFiredForInvocation:)] ;
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig] ;
    invocation.target = _obj ;
    invocation.selector = @selector(timerFiredForInvocation:) ;
    id objArgument = [[TestObj alloc] init] ;
    [invocation setArgument:&objArgument atIndex:2] ;
    _timer2 = [WTTimer scheduledTimerWithTimeInterval:2.0 invocation:invocation repeats:YES] ;
    NSLog(@"timer is scheduled") ;
    // delay to set self.obj to nil and make it be deallocated
    [self performSelector:@selector(delay) withObject:nil afterDelay:3.0] ;
}
- (void)delay
{
    NSLog(@"%@ %@", self, NSStringFromSelector(_cmd)) ;    
    self.obj = nil ;
}
@end

// WTTimer.m

@class TimerDelegateObject ;
@protocol WTTimerDelegate <NSObject>
- (void)wtTimerFired:(TimerDelegateObject *)obj ;
@end

@interface TimerDelegateObject : NSObject
@property (nonatomic, weak) id<WTTimerDelegate> delegate ;
- (void)timerFired:(NSTimer *)timer ;
@end

@implementation TimerDelegateObject
- (void)timerFired:(NSTimer *)timer
{
    [_delegate wtTimerFired:self] ;
}
@end

@interface WTTimer () <WTTimerDelegate>

@property (nonatomic, strong) NSTimer *timer ;

// target and selector
@property (nonatomic, weak) id wtTarget ;
@property (nonatomic) SEL selector ;

// for NSInvocation
@property (nonatomic, strong) NSInvocation *invocation ;

@end

@implementation WTTimer

- (instancetype)initWithFireDate:(NSDate *)date
                        interval:(NSTimeInterval)seconds
                          target:(id)target
                        selector:(SEL)aSelector
                        userInfo:(id)userInfo
                         repeats:(BOOL)repeats
{
    self = [super init] ;
    if (self) {
        _timer = [[NSTimer alloc] initWithFireDate:date interval:seconds target:target selector:aSelector userInfo:userInfo repeats:repeats] ;
    }
    return self ;
}

+ (WTTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo
{
    TimerDelegateObject *delegateObj = [[TimerDelegateObject alloc] init] ;

    NSDate *dateFire = [NSDate dateWithTimeIntervalSinceNow:ti] ;
    WTTimer *timer = [[WTTimer alloc] initWithFireDate:dateFire
                                              interval:ti
                                                target:delegateObj
                                              selector:@selector(timerFired:)
                                              userInfo:nil
                                               repeats:yesOrNo] ;
    delegateObj.delegate = timer ;

    // config WTTimer
    timer.wtTarget = invocation.target ; // timer.wtTarget is weak
    invocation.target = delegateObj ;// I change the target to delegateObj, so [invocation retainArguments] won't retain the original target
    [invocation retainArguments] ;
    timer.invocation = invocation ;
    return timer ;
}

+ (WTTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo
{
    WTTimer *timer = [WTTimer timerWithTimeInterval:ti invocation:invocation repeats:yesOrNo] ;
    if (timer) {
        [[NSRunLoop currentRunLoop] addTimer:timer.timer forMode:NSDefaultRunLoopMode] ;
    }
    return timer ;
}
- (void)wtTimerFired:(TimerDelegateObject *)obj
{
    if (_wtTarget) {
        if (_invocation) {
            [_invocation invokeWithTarget:_wtTarget] ;
        } else {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
            [_wtTarget performSelector:_selector withObject:self] ;
#pragma clang diagnostic pop
        }
    } else {
        // the target is deallocated, the timer should be invalidated
        [self.timer invalidate] ;
        NSLog(@"the target is deallocated, invalidate the timer") ;
    }
}
- (NSDate *)fireDate
{
    return [_timer fireDate] ;
}
- (void)setFireDate:(NSDate *)fireDate
{
    _timer.fireDate = fireDate ;
}
- (NSTimeInterval)timeInterval
{
    return [_timer timeInterval] ;
}
- (void)fire
{
    return [_timer fire] ;
}
- (void)invalidate
{
    [_timer invalidate] ;
}
- (BOOL)isValid
{
    return [_timer isValid] ;
}
- (id)userInfo
{
    return _timer.userInfo ;
}
@end

delay执行ViewControllerself.obj = nil方法存在一个问题,_obj应该被解除分配,但事实上,它不是,我不知道为什么。除了ViewController中的obj属性外,没有强引用,但为什么它不能被释放。

注意1:如果我在[invocation retainArguments] ;中删除了这行代码:timerWithTimeInterval:invocation:repeats:,则会将其解除分配。

注意2:如果我没有在runloop中安排计时器,那么目标对象也会被解除分配。

+ (WTTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo
{
    WTTimer *timer = [WTTimer timerWithTimeInterval:ti invocation:invocation repeats:yesOrNo] ;
    if (timer) {
        // [[NSRunLoop currentRunLoop] addTimer:timer.timer forMode:NSDefaultRunLoopMode] ;
    }
    return timer ;
}

如果您对此感兴趣,请将代码发布在https://github.com/kudocc/WTTimer中。我花了一天时间试图解决这个问题,但没有好处,任何人都可以帮助我吗?谢谢你的时间。

1 个答案:

答案 0 :(得分:1)

要回答这个问题:第[_invocation invokeWithTarget:_wtTarget];行是您为TestObj目标设置超强引用的地方。

[NSInvocation invokeWithTarget:]的{​​{3}}说:

  

设置接收者的目标,将接收者的消息(带参数)发送到该目标,并设置返回值。

在我看来,如果您在-retainArguments上拨打了NSInvocation,那么您随后设置了新的targetNSInvocation的实施应该(并且确实)发布它的旧target并保留其新的。

这也解释了你在两个笔记中观察到的内容:

  

注意1:如果我在[invocation retainArguments];中删除了这行代码:timerWithTimeInterval:invocation:repeats:,则会将其解除分配。

从不致电-retainArgumentsNSInvocation将不会保留新的target

  

注意2:如果我没有在runloop中安排计时器,那么目标对象也会被释放。

如果您不安排计时器,则永远不会调用-invokeWithTarget