Reputation: 6952
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.
Upvotes: 1
Views: 338
Reputation: 4554
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];
intimerWithTimeInterval: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.
Upvotes: 1