简体   繁体   中英

The target in NSInvocation should be deallocated but it is not

I create a weak target timer class, when target is deallocated, the timer don't fire and invalidate itself automatically. The code is like this:

// 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

There is an issue that in the delay method of ViewController when self.obj = nil executes, the _obj should be deallocated, but in fact, it isn't and I don't know why. Except for the obj property in ViewController, there is not strong reference to it, but why it can't be deallocated.

Note1 : If I remove this line of code : [invocation retainArguments] ; in timerWithTimeInterval:invocation:repeats: , it will be deallocated.

Note2: If I don't schedule the timer in runloop, the target object is deallocated too.

+ (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 ;
}

If you're interested in it, I post the code in https://github.com/kudocc/WTTimer . I have spent one day trying to figure it out but no good, can anyone help me ? Thanks for your time.

To answer the question: the line [_invocation invokeWithTarget:_wtTarget]; is where you are setting the extra strong reference to your TestObj target.

The documentation for [NSInvocation invokeWithTarget:] says:

Sets the receiver's target, sends the receiver's message (with arguments) to that target, and sets the return value.

To my mind, if you have called -retainArguments on your NSInvocation , then you subsequently set a new target , the implementation of NSInvocation should (and does) release its old target and retain its new.


This also explains what you're observing in your two notes:

Note1: If I remove this line of code: [invocation retainArguments]; in timerWithTimeInterval:invocation:repeats: , it will be deallocated.

By never calling -retainArguments , the NSInvocation will not retain its new target .

Note2: If I don't schedule the timer in runloop, the target object is deallocated too.

If you don't schedule the timer, -invokeWithTarget is never called.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM