Skip to main content

When does an associated object get released?



I'm attaching object B via associative reference to object A. Object B observes some properties of object A through KVO.





The problem is that object B seems to be deallocated after object A, meaning its too late to remove itself as a KVO observer of object A. I know this because I'm getting NSKVODeallocateBreak exceptions, followed by EXEC_BAD_ACCESS crashes in object B's dealloc.





Does anyone know why object B is deallocated after object A with OBJC_ASSOCIATION_RETAIN? Do associated objects get released after deallocation? Do they get autoreleased? Does anyone know of a way to alter this behavior?





I'm trying to add some things to a class through categories, so I can't override any existing methods (including dealloc), and I don't particularly want to mess with swizzling. I need some way to de-associate and release object B before object A gets deallocated.





EDIT - Here is the code I'm trying to get working. If the associated objects were released prior to UIImageView being completely deallocated, this would all work. The only solution I'm seeing is to swizzle in my own dealloc method, and swizzle back the original in order to call up to it. That gets really messy though.





The point of the ZSPropertyWatcher class is that KVO requires a standard callback method, and I don't want to replace UIImageView's, in case it uses one itself.





UIImageView+Loading.h







@interface UIImageView (ZSShowLoading)

@property (nonatomic) BOOL showLoadingSpinner;

@end







UIImageView+Loading.m







@implementation UIImageView (ZSShowLoading)



#define UIIMAGEVIEW_SPINNER_TAG 862353453

static char imageWatcherKey;

static char frameWatcherKey;



- (void)zsShowSpinner:(BOOL)show {

if (show) {

UIActivityIndicatorView *spinnerView = (UIActivityIndicatorView *)[self viewWithTag:UIIMAGEVIEW_SPINNER_TAG];

if (!spinnerView) {

spinnerView = [[[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge] autorelease];

spinnerView.tag = UIIMAGEVIEW_SPINNER_TAG;

[self addSubview:spinnerView];

[spinnerView startAnimating];

}



[spinnerView setEvenCenter:self.boundsCenter];

} else {

[[self viewWithTag:UIIMAGEVIEW_SPINNER_TAG] removeFromSuperview];

}

}



- (void)zsFrameChanged {

[self zsShowSpinner:!self.image];

}



- (void)zsImageChanged {

[self zsShowSpinner:!self.image];

}



- (BOOL)showLoadingSpinner {

ZSPropertyWatcher *imageWatcher = (ZSPropertyWatcher *)objc_getAssociatedObject(self, &imageWatcherKey);

return imageWatcher != nil;

}



- (void)setShowLoadingSpinner:(BOOL)aBool {

ZSPropertyWatcher *imageWatcher = nil;

ZSPropertyWatcher *frameWatcher = nil;



if (aBool) {

imageWatcher = [[[ZSPropertyWatcher alloc] initWithObject:self keyPath:@"image" delegate:self callback:@selector(zsImageChanged)] autorelease];

frameWatcher = [[[ZSPropertyWatcher alloc] initWithObject:self keyPath:@"frame" delegate:self callback:@selector(zsFrameChanged)] autorelease];



[self zsShowSpinner:!self.image];

} else {

// Remove the spinner

[self zsShowSpinner:NO];

}



objc_setAssociatedObject(

self,

&imageWatcherKey,

imageWatcher,

OBJC_ASSOCIATION_RETAIN

);



objc_setAssociatedObject(

self,

&frameWatcherKey,

frameWatcher,

OBJC_ASSOCIATION_RETAIN

);

}



@end







ZSPropertyWatcher.h







@interface ZSPropertyWatcher : NSObject {

id delegate;

SEL delegateCallback;



NSObject *observedObject;

NSString *keyPath;

}



@property (nonatomic, assign) id delegate;

@property (nonatomic, assign) SEL delegateCallback;



- (id)initWithObject:(NSObject *)anObject keyPath:(NSString *)aKeyPath delegate:(id)aDelegate callback:(SEL)aSelector;



@end







ZSPropertyWatcher.m







@interface ZSPropertyWatcher ()



@property (nonatomic, assign) NSObject *observedObject;

@property (nonatomic, copy) NSString *keyPath;



@end



@implementation ZSPropertyWatcher



@synthesize delegate, delegateCallback;

@synthesize observedObject, keyPath;



- (id)initWithObject:(NSObject *)anObject keyPath:(NSString *)aKeyPath delegate:(id)aDelegate callback:(SEL)aSelector {

if (!anObject || !aKeyPath) {

// pre-conditions

self = nil;

return self;

}



self = [super init];

if (self) {

observedObject = anObject;

keyPath = aKeyPath;

delegate = aDelegate;

delegateCallback = aSelector;



[observedObject addObserver:self forKeyPath:keyPath options:0 context:nil];

}

return self;

}



- (void)dealloc {

[observedObject removeObserver:self forKeyPath:keyPath];



[keyPath release];



[super dealloc];

}



- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {

[self.delegate performSelector:self.delegateCallback];

}



@end




Comments

  1. Even larger than your -dealloc issue is this:

    UIKit is not KVO-compliant

    No effort has been made to make UIKit classes key-value observable. If any of them are, it is entirely coincidental and is subject to break at Apple's whim. And yes, I work for Apple on the UIKit framework.

    This means that you're going to have to find another way to do this, probably by changing your view layouting slightly.

    ReplyDelete
  2. what i think is happening in your case is this:

    1) object A receives the -dealloc call, after its retain count has gone to 0;

    2) the association mechanism ensures that object B gets released (which is different from deallocated) at some point as a consequence.

    i.e., we don't know exactly at which point, but it seems likely to me that this kind of semantic difference is the cause of object B being deallocated after object A; object A -dealloc selector cannot be aware of the association, so when the last release on it is called, -dealloc is executed, and only after that the association mechanism can send a -release to object B...

    have also a look at this post.

    it also states:


    Now, when objectToBeDeallocated is deallocated, objectWeWantToBeReleasedWhenThatHappens will be sent a -release message automatically.


    I hope this helps explaining what you are experiencing.
    As to the rest, I cannot be of much help...

    EDIT: just to keep on with such an interesting speculation after the comment by DougW...

    I see the risk of having a sort of cyclic dependency if the association mechanism were "broken" when releasing object A (to keep going with your example).


    if the association-related code were executed from the release method (instead of dealloc), for each release you would check if the "owning" object (object A) has a retain count of 1; in fact, in such case you know that decreasing its retain count would trigger dealloc, so before doing that, you would first release the associated object (object B in your example);
    but what would happen in case object B were also at its turn "owning" a third object, say it C? what would happen is that at the time release is called on object B, when object B retain count is 1, C would be released;
    now, consider the case that object C were "owning" the very first one of this sequence, object A. if, when receiving the release above, C had a retain count of 1, it would first try and release its associated object, which is A;


    but the release count of A is still 1, so another release would be sent to B, which still has a retain count of 1; and so on, in a loop.



    If you, on the other hand, send the release from the -dealloc such cyclic dependency does not seem possible.

    It's pretty contrived and I am not sure that my reasoning is right, so feel free to comment on it...

    ReplyDelete
  3. objc_getAssociatedObject() for an OBJC_ASSOCIATION_RETAIN association returns an autoreleased object. Might you be calling it earlier in the same runloop cycle / autorelease pool scope as object A is deallocated? (You can probably test this quickly by changing the association to NONATOMIC).

    ReplyDelete

Post a Comment

Popular posts from this blog

Why is this Javascript much *slower* than its jQuery equivalent?

I have a HTML list of about 500 items and a "filter" box above it. I started by using jQuery to filter the list when I typed a letter (timing code added later): $('#filter').keyup( function() { var jqStart = (new Date).getTime(); var search = $(this).val().toLowerCase(); var $list = $('ul.ablist > li'); $list.each( function() { if ( $(this).text().toLowerCase().indexOf(search) === -1 ) $(this).hide(); else $(this).show(); } ); console.log('Time: ' + ((new Date).getTime() - jqStart)); } ); However, there was a couple of seconds delay after typing each letter (particularly the first letter). So I thought it may be slightly quicker if I used plain Javascript (I read recently that jQuery's each function is particularly slow). Here's my JS equivalent: document.getElementById('filter').addEventListener( 'keyup', function () { var jsStart = (new Date).getTime()...

Is it possible to have IF statement in an Echo statement in PHP

Thanks in advance. I did look at the other questions/answers that were similar and didn't find exactly what I was looking for. I'm trying to do this, am I on the right path? echo " <div id='tabs-".$match."'> <textarea id='".$match."' name='".$match."'>". if ($COLUMN_NAME === $match) { echo $FIELD_WITH_COLUMN_NAME; } else { } ."</textarea> <script type='text/javascript'> CKEDITOR.replace( '".$match."' ); </script> </div>"; I am getting the following error message in the browser: Parse error: syntax error, unexpected T_IF Please let me know if this is the right way to go about nesting an IF statement inside an echo. Thank you.